using System;
using System.ComponentModel;
using System.Configuration.Provider;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.Security;
using System.Web;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security.DataProtection;
using Umbraco.Core.Auditing;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
namespace Umbraco.Core.Security
{
///
/// Default back office user manager
///
public class BackOfficeUserManager : BackOfficeUserManager
{
public const string OwinMarkerKey = "Umbraco.Web.Security.Identity.BackOfficeUserManagerMarker";
public BackOfficeUserManager(IUserStore store)
: base(store)
{
}
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Use the constructor specifying all dependencies instead")]
public BackOfficeUserManager(
IUserStore store,
IdentityFactoryOptions options,
MembershipProviderBase membershipProvider)
: this(store, options, membershipProvider, UmbracoConfig.For.UmbracoSettings().Content)
{
}
public BackOfficeUserManager(
IUserStore store,
IdentityFactoryOptions options,
MembershipProviderBase membershipProvider,
IContentSection contentSectionConfig)
: base(store)
{
if (options == null) throw new ArgumentNullException("options");
InitUserManager(this, membershipProvider, contentSectionConfig, options);
}
#region Static Create methods
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Use the overload specifying all dependencies instead")]
public static BackOfficeUserManager Create(
IdentityFactoryOptions options,
IUserService userService,
IExternalLoginService externalLoginService,
MembershipProviderBase membershipProvider)
{
return Create(options, userService,
ApplicationContext.Current.Services.EntityService,
externalLoginService, membershipProvider,
UmbracoConfig.For.UmbracoSettings().Content);
}
///
/// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager
///
///
///
///
///
///
///
///
public static BackOfficeUserManager Create(
IdentityFactoryOptions options,
IUserService userService,
IEntityService entityService,
IExternalLoginService externalLoginService,
MembershipProviderBase membershipProvider,
IContentSection contentSectionConfig)
{
if (options == null) throw new ArgumentNullException("options");
if (userService == null) throw new ArgumentNullException("userService");
if (externalLoginService == null) throw new ArgumentNullException("externalLoginService");
var manager = new BackOfficeUserManager(
new BackOfficeUserStore(userService, entityService, externalLoginService, membershipProvider));
manager.InitUserManager(manager, membershipProvider, contentSectionConfig, options);
return manager;
}
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Use the overload specifying all dependencies instead")]
public static BackOfficeUserManager Create(
IdentityFactoryOptions options,
BackOfficeUserStore customUserStore,
MembershipProviderBase membershipProvider)
{
var manager = new BackOfficeUserManager(customUserStore, options, membershipProvider);
return manager;
}
///
/// Creates a BackOfficeUserManager instance with all default options and a custom BackOfficeUserManager instance
///
///
///
///
///
///
public static BackOfficeUserManager Create(
IdentityFactoryOptions options,
BackOfficeUserStore customUserStore,
MembershipProviderBase membershipProvider,
IContentSection contentSectionConfig)
{
var manager = new BackOfficeUserManager(customUserStore, options, membershipProvider, contentSectionConfig);
return manager;
}
#endregion
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Use the overload specifying all dependencies instead")]
protected void InitUserManager(
BackOfficeUserManager manager,
MembershipProviderBase membershipProvider,
IdentityFactoryOptions options)
{
InitUserManager(manager, membershipProvider, UmbracoConfig.For.UmbracoSettings().Content, options);
}
///
/// Initializes the user manager with the correct options
///
///
///
///
///
///
protected void InitUserManager(
BackOfficeUserManager manager,
MembershipProviderBase membershipProvider,
IContentSection contentSectionConfig,
IdentityFactoryOptions options)
{
//NOTE: This method is mostly here for backwards compat
base.InitUserManager(manager, membershipProvider, options.DataProtectionProvider, contentSectionConfig);
}
}
///
/// Generic Back office user manager
///
public class BackOfficeUserManager : UserManager
where T : BackOfficeIdentityUser
{
public BackOfficeUserManager(IUserStore store)
: base(store)
{
}
#region What we support do not currently
//NOTE: Not sure if we really want/need to ever support this
public override bool SupportsUserClaim
{
get { return false; }
}
//TODO: Support this
public override bool SupportsQueryableUsers
{
get { return false; }
}
///
/// Developers will need to override this to support custom 2 factor auth
///
public override bool SupportsUserTwoFactor
{
get { return false; }
}
//TODO: Support this
public override bool SupportsUserPhoneNumber
{
get { return false; }
}
#endregion
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Use the overload specifying all dependencies instead")]
protected void InitUserManager(
BackOfficeUserManager manager,
MembershipProviderBase membershipProvider,
IDataProtectionProvider dataProtectionProvider)
{
InitUserManager(manager, membershipProvider, dataProtectionProvider, UmbracoConfig.For.UmbracoSettings().Content);
}
///
/// Initializes the user manager with the correct options
///
///
///
/// The for the users called UsersMembershipProvider
///
///
///
///
protected void InitUserManager(
BackOfficeUserManager manager,
MembershipProviderBase membershipProvider,
IDataProtectionProvider dataProtectionProvider,
IContentSection contentSectionConfig)
{
// Configure validation logic for usernames
manager.UserValidator = new BackOfficeUserValidator(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = true
};
// Configure validation logic for passwords
manager.PasswordValidator = new MembershipProviderPasswordValidator(membershipProvider);
//use a custom hasher based on our membership provider
manager.PasswordHasher = GetDefaultPasswordHasher(membershipProvider);
if (dataProtectionProvider != null)
{
manager.UserTokenProvider = new DataProtectorTokenProvider(dataProtectionProvider.Create("ASP.NET Identity"));
}
manager.UserLockoutEnabledByDefault = true;
manager.MaxFailedAccessAttemptsBeforeLockout = membershipProvider.MaxInvalidPasswordAttempts;
//NOTE: This just needs to be in the future, we currently don't support a lockout timespan, it's either they are locked
// or they are not locked, but this determines what is set on the account lockout date which corresponds to whether they are
// locked out or not.
manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromDays(30);
//custom identity factory for creating the identity object for which we auth against in the back office
manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory();
manager.EmailService = new EmailService(
contentSectionConfig.NotificationEmailAddress,
new EmailSender());
//NOTE: Not implementing these, if people need custom 2 factor auth, they'll need to implement their own UserStore to suport it
//// Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user
//// You can write your own provider and plug in here.
//manager.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider
//{
// MessageFormat = "Your security code is: {0}"
//});
//manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider
//{
// Subject = "Security Code",
// BodyFormat = "Your security code is: {0}"
//});
//manager.SmsService = new SmsService();
}
///
/// This will determine which password hasher to use based on what is defined in config
///
///
protected virtual IPasswordHasher GetDefaultPasswordHasher(MembershipProviderBase provider)
{
//if the current user membership provider is unkown (this would be rare), then return the default password hasher
if (provider.IsUmbracoUsersProvider() == false)
return new PasswordHasher();
//if the configured provider has legacy features enabled, then return the membership provider password hasher
if (provider.AllowManuallyChangingPassword || provider.DefaultUseLegacyEncoding)
return new MembershipProviderPasswordHasher(provider);
//we can use the user aware password hasher (which will be the default and preferred way)
return new UserAwareMembershipProviderPasswordHasher(provider);
}
///
/// Gets/sets the default back office user password checker
///
public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; }
///
/// Helper method to generate a password for a user based on the current password validator
///
///
public string GeneratePassword()
{
var passwordValidator = PasswordValidator as PasswordValidator;
if (passwordValidator == null)
{
var membershipPasswordHasher = PasswordHasher as IMembershipProviderPasswordHasher;
//get the real password validator, this should not be null but in some very rare cases it could be, in which case
//we need to create a default password validator to use since we have no idea what it actually is or what it's rules are
//this is an Edge Case!
passwordValidator = PasswordValidator as PasswordValidator
?? (membershipPasswordHasher != null
? new MembershipProviderPasswordValidator(membershipPasswordHasher.MembershipProvider)
: new PasswordValidator());
}
var password = Membership.GeneratePassword(
passwordValidator.RequiredLength,
passwordValidator.RequireNonLetterOrDigit ? 2 : 0);
var random = new Random();
var passwordChars = password.ToCharArray();
if (passwordValidator.RequireDigit && passwordChars.ContainsAny(Enumerable.Range(48, 58).Select(x => (char)x)))
password += Convert.ToChar(random.Next(48, 58)); // 0-9
if (passwordValidator.RequireLowercase && passwordChars.ContainsAny(Enumerable.Range(97, 123).Select(x => (char)x)))
password += Convert.ToChar(random.Next(97, 123)); // a-z
if (passwordValidator.RequireUppercase && passwordChars.ContainsAny(Enumerable.Range(65, 91).Select(x => (char)x)))
password += Convert.ToChar(random.Next(65, 91)); // A-Z
if (passwordValidator.RequireNonLetterOrDigit && passwordChars.ContainsAny(Enumerable.Range(33, 48).Select(x => (char)x)))
password += Convert.ToChar(random.Next(33, 48)); // symbols !"#$%&'()*+,-./
return password;
}
///
/// Override to check the user approval value as well as the user lock out date, by default this only checks the user's locked out date
///
///
///
///
/// In the ASP.NET Identity world, there is only one value for being locked out, in Umbraco we have 2 so when checking this for Umbraco we need to check both values
///
public override async Task IsLockedOutAsync(int userId)
{
var user = await FindByIdAsync(userId);
if (user == null)
throw new InvalidOperationException("No user found by id " + userId);
if (user.IsApproved == false)
return true;
return await base.IsLockedOutAsync(userId);
}
#region Overrides for password logic
///
/// Logic used to validate a username and password
///
///
///
///
///
/// By default this uses the standard ASP.Net Identity approach which is:
/// * Get password store
/// * Call VerifyPasswordAsync with the password store + user + password
/// * Uses the PasswordHasher.VerifyHashedPassword to compare the stored password
///
/// In some cases people want simple custom control over the username/password check, for simplicity
/// sake, developers would like the users to simply validate against an LDAP directory but the user
/// data remains stored inside of Umbraco.
/// See: http://issues.umbraco.org/issue/U4-7032 for the use cases.
///
/// We've allowed this check to be overridden with a simple callback so that developers don't actually
/// have to implement/override this class.
///
public override async Task CheckPasswordAsync(T user, string password)
{
if (BackOfficeUserPasswordChecker != null)
{
var result = await BackOfficeUserPasswordChecker.CheckPasswordAsync(user, password);
if (user.HasIdentity == false)
{
return false;
}
//if the result indicates to not fallback to the default, then return true if the credentials are valid
if (result != BackOfficeUserPasswordCheckerResult.FallbackToDefaultChecker)
{
return result == BackOfficeUserPasswordCheckerResult.ValidCredentials;
}
}
//we cannot proceed if the user passed in does not have an identity
if (user.HasIdentity == false)
return false;
//use the default behavior
return await base.CheckPasswordAsync(user, password);
}
public override Task ChangePasswordAsync(int userId, string currentPassword, string newPassword)
{
var result = base.ChangePasswordAsync(userId, currentPassword, newPassword);
if (result.Result.Succeeded)
RaisePasswordChangedEvent(userId);
return result;
}
///
/// Override to determine how to hash the password
///
///
///
///
///
protected override async Task VerifyPasswordAsync(IUserPasswordStore store, T user, string password)
{
var userAwarePasswordHasher = PasswordHasher as IUserAwarePasswordHasher;
if (userAwarePasswordHasher == null)
return await base.VerifyPasswordAsync(store, user, password);
var hash = await store.GetPasswordHashAsync(user);
return userAwarePasswordHasher.VerifyHashedPassword(user, hash, password) != PasswordVerificationResult.Failed;
}
///
/// Override to determine how to hash the password
///
///
///
///
///
///
/// This method is called anytime the password needs to be hashed for storage (i.e. including when reset password is used)
///
protected override async Task UpdatePassword(IUserPasswordStore passwordStore, T user, string newPassword)
{
var userAwarePasswordHasher = PasswordHasher as IUserAwarePasswordHasher;
if (userAwarePasswordHasher == null)
return await base.UpdatePassword(passwordStore, user, newPassword);
var result = await PasswordValidator.ValidateAsync(newPassword);
if (result.Succeeded == false)
return result;
await passwordStore.SetPasswordHashAsync(user, userAwarePasswordHasher.HashPassword(user, newPassword));
await UpdateSecurityStampInternal(user);
return IdentityResult.Success;
}
///
/// This is copied from the underlying .NET base class since they decied to not expose it
///
///
///
private async Task UpdateSecurityStampInternal(BackOfficeIdentityUser user)
{
if (SupportsUserSecurityStamp == false)
return;
await GetSecurityStore().SetSecurityStampAsync(user, NewSecurityStamp());
}
///
/// This is copied from the underlying .NET base class since they decied to not expose it
///
///
private IUserSecurityStampStore GetSecurityStore()
{
var store = Store as IUserSecurityStampStore;
if (store == null)
throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>));
return store;
}
///
/// This is copied from the underlying .NET base class since they decied to not expose it
///
///
private static string NewSecurityStamp()
{
return Guid.NewGuid().ToString();
}
#endregion
public override Task SetLockoutEndDateAsync(int userId, DateTimeOffset lockoutEnd)
{
var result = base.SetLockoutEndDateAsync(userId, lockoutEnd);
// The way we unlock is by setting the lockoutEnd date to the current datetime
if (result.Result.Succeeded && lockoutEnd >= DateTimeOffset.UtcNow)
RaiseAccountLockedEvent(userId);
else
RaiseAccountUnlockedEvent(userId);
return result;
}
public override async Task ResetAccessFailedCountAsync(int userId)
{
var lockoutStore = (IUserLockoutStore)Store;
var user = await FindByIdAsync(userId);
if (user == null)
throw new InvalidOperationException("No user found by user id " + userId);
var accessFailedCount = await GetAccessFailedCountAsync(user.Id);
if (accessFailedCount == 0)
return IdentityResult.Success;
await lockoutStore.ResetAccessFailedCountAsync(user);
//raise the event now that it's reset
RaiseResetAccessFailedCountEvent(userId);
return await UpdateAsync(user);
}
public override Task AccessFailedAsync(int userId)
{
var result = base.AccessFailedAsync(userId);
//Slightly confusing: this will return a Success if we successfully update the AccessFailed count
if (result.Result.Succeeded)
RaiseLoginFailedEvent(userId);
return result;
}
internal void RaiseAccountLockedEvent(int userId)
{
OnAccountLocked(new IdentityAuditEventArgs(AuditEvent.AccountLocked, GetCurrentRequestIpAddress(), userId));
}
internal void RaiseAccountUnlockedEvent(int userId)
{
OnAccountUnlocked(new IdentityAuditEventArgs(AuditEvent.AccountUnlocked, GetCurrentRequestIpAddress(), userId));
}
internal void RaiseForgotPasswordRequestedEvent(int userId)
{
OnForgotPasswordRequested(new IdentityAuditEventArgs(AuditEvent.ForgotPasswordRequested, GetCurrentRequestIpAddress(), userId));
}
internal void RaiseForgotPasswordChangedSuccessEvent(int userId)
{
OnForgotPasswordChangedSuccess(new IdentityAuditEventArgs(AuditEvent.ForgotPasswordChangedSuccess, GetCurrentRequestIpAddress(), userId));
}
internal void RaiseLoginFailedEvent(int userId)
{
OnLoginFailed(new IdentityAuditEventArgs(AuditEvent.LoginFailed, GetCurrentRequestIpAddress(), userId));
}
internal void RaiseInvalidLoginAttemptEvent(string username)
{
OnLoginFailed(new IdentityAuditEventArgs(AuditEvent.LoginFailed, GetCurrentRequestIpAddress(), username, string.Format("Attempted login for username '{0}' failed", username)));
}
internal void RaiseLoginRequiresVerificationEvent(int userId)
{
OnLoginRequiresVerification(new IdentityAuditEventArgs(AuditEvent.LoginRequiresVerification, GetCurrentRequestIpAddress(), userId));
}
internal void RaiseLoginSuccessEvent(int userId)
{
OnLoginSuccess(new IdentityAuditEventArgs(AuditEvent.LoginSucces, GetCurrentRequestIpAddress(), userId));
}
internal void RaiseLogoutSuccessEvent(int userId)
{
OnLogoutSuccess(new IdentityAuditEventArgs(AuditEvent.LogoutSuccess, GetCurrentRequestIpAddress(), userId));
}
internal void RaisePasswordChangedEvent(int userId)
{
OnPasswordChanged(new IdentityAuditEventArgs(AuditEvent.PasswordChanged, GetCurrentRequestIpAddress(), userId));
}
internal void RaisePasswordResetEvent(int userId)
{
OnPasswordReset(new IdentityAuditEventArgs(AuditEvent.PasswordReset, GetCurrentRequestIpAddress(), userId));
}
internal void RaiseResetAccessFailedCountEvent(int userId)
{
OnResetAccessFailedCount(new IdentityAuditEventArgs(AuditEvent.ResetAccessFailedCount, GetCurrentRequestIpAddress(), userId));
}
public static event EventHandler AccountLocked;
public static event EventHandler AccountUnlocked;
public static event EventHandler ForgotPasswordRequested;
public static event EventHandler ForgotPasswordChangedSuccess;
public static event EventHandler LoginFailed;
public static event EventHandler LoginRequiresVerification;
public static event EventHandler LoginSuccess;
public static event EventHandler LogoutSuccess;
public static event EventHandler PasswordChanged;
public static event EventHandler PasswordReset;
public static event EventHandler ResetAccessFailedCount;
protected virtual void OnAccountLocked(IdentityAuditEventArgs e)
{
if (AccountLocked != null) AccountLocked(this, e);
}
protected virtual void OnAccountUnlocked(IdentityAuditEventArgs e)
{
if (AccountUnlocked != null) AccountUnlocked(this, e);
}
protected virtual void OnForgotPasswordRequested(IdentityAuditEventArgs e)
{
if (ForgotPasswordRequested != null) ForgotPasswordRequested(this, e);
}
protected virtual void OnForgotPasswordChangedSuccess(IdentityAuditEventArgs e)
{
if (ForgotPasswordChangedSuccess != null) ForgotPasswordChangedSuccess(this, e);
}
protected virtual void OnLoginFailed(IdentityAuditEventArgs e)
{
if (LoginFailed != null) LoginFailed(this, e);
}
protected virtual void OnLoginRequiresVerification(IdentityAuditEventArgs e)
{
if (LoginRequiresVerification != null) LoginRequiresVerification(this, e);
}
protected virtual void OnLoginSuccess(IdentityAuditEventArgs e)
{
if (LoginSuccess != null) LoginSuccess(this, e);
}
protected virtual void OnLogoutSuccess(IdentityAuditEventArgs e)
{
if (LogoutSuccess != null) LogoutSuccess(this, e);
}
protected virtual void OnPasswordChanged(IdentityAuditEventArgs e)
{
if (PasswordChanged != null) PasswordChanged(this, e);
}
protected virtual void OnPasswordReset(IdentityAuditEventArgs e)
{
if (PasswordReset != null) PasswordReset(this, e);
}
protected virtual void OnResetAccessFailedCount(IdentityAuditEventArgs e)
{
if (ResetAccessFailedCount != null) ResetAccessFailedCount(this, e);
}
///
/// Returns the current request IP address for logging if there is one
///
///
protected virtual string GetCurrentRequestIpAddress()
{
//TODO: inject a service to get this value, we should not be relying on the old HttpContext.Current especially in the ASP.NET Identity world.
var httpContext = HttpContext.Current == null ? (HttpContextBase)null : new HttpContextWrapper(HttpContext.Current);
return httpContext.GetCurrentRequestIpAddress();
}
}
}