using System; using System.Collections.Generic; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Owin.Security.DataProtection; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Mapping; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Net; using Umbraco.Web.Models.Identity; namespace Umbraco.Web.Security { public class BackOfficeUserManager : BackOfficeUserManager { public const string OwinMarkerKey = "Umbraco.Web.Security.Identity.BackOfficeUserManagerMarker"; public BackOfficeUserManager( IPasswordConfiguration passwordConfiguration, IIpResolver ipResolver, IUserStore store, IOptions optionsAccessor, IEnumerable> userValidators, IEnumerable> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IDataProtectionProvider dataProtectionProvider, ILogger> logger) : base(passwordConfiguration, ipResolver, store, optionsAccessor, userValidators, passwordValidators, keyNormalizer, errors, null, logger) { InitUserManager(this, dataProtectionProvider); } #region Static Create methods /// /// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager /// public static BackOfficeUserManager Create( IUserService userService, IEntityService entityService, IExternalLoginService externalLoginService, IGlobalSettings globalSettings, UmbracoMapper mapper, IPasswordConfiguration passwordConfiguration, IIpResolver ipResolver, IdentityErrorDescriber errors, IDataProtectionProvider dataProtectionProvider, ILogger> logger) { var store = new BackOfficeUserStore(userService, entityService, externalLoginService, globalSettings, mapper); return Create( passwordConfiguration, ipResolver, store, errors, dataProtectionProvider, logger); } /// /// Creates a BackOfficeUserManager instance with all default options and a custom BackOfficeUserManager instance /// public static BackOfficeUserManager Create( IPasswordConfiguration passwordConfiguration, IIpResolver ipResolver, IUserStore customUserStore, IdentityErrorDescriber errors, IDataProtectionProvider dataProtectionProvider, ILogger> logger) { var options = new IdentityOptions(); // Configure validation logic for usernames var userValidators = new List> { new BackOfficeUserValidator() }; options.User.RequireUniqueEmail = true; // Configure validation logic for passwords var passwordValidators = new List> { new PasswordValidator() }; options.Password.RequiredLength = passwordConfiguration.RequiredLength; options.Password.RequireNonAlphanumeric = passwordConfiguration.RequireNonLetterOrDigit; options.Password.RequireDigit = passwordConfiguration.RequireDigit; options.Password.RequireLowercase = passwordConfiguration.RequireLowercase; options.Password.RequireUppercase = passwordConfiguration.RequireUppercase; // Ensure Umbraco security stamp claim type is used options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier; options.ClaimsIdentity.UserNameClaimType = ClaimTypes.Name; options.ClaimsIdentity.RoleClaimType = ClaimTypes.Role; options.ClaimsIdentity.SecurityStampClaimType = Constants.Web.SecurityStampClaimType; options.Lockout.AllowedForNewUsers = true; options.Lockout.MaxFailedAccessAttempts = passwordConfiguration.MaxFailedAccessAttemptsBeforeLockout; //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. options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromDays(30); return new BackOfficeUserManager( passwordConfiguration, ipResolver, customUserStore, new OptionsWrapper(options), userValidators, passwordValidators, new NopLookupNormalizer(), errors, dataProtectionProvider, logger); } #endregion } public class BackOfficeUserManager : UserManager where T : BackOfficeIdentityUser { private PasswordGenerator _passwordGenerator; public BackOfficeUserManager( IPasswordConfiguration passwordConfiguration, IIpResolver ipResolver, IUserStore store, IOptions optionsAccessor, IEnumerable> userValidators, IEnumerable> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger) : base(store, optionsAccessor, null, userValidators, passwordValidators, keyNormalizer, errors, services, logger) { PasswordConfiguration = passwordConfiguration ?? throw new ArgumentNullException(nameof(passwordConfiguration)); IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver)); } #region What we do not currently support // TODO: We could support this - but a user claims will mostly just be what is in the auth cookie public override bool SupportsUserClaim => false; // TODO: Support this public override bool SupportsQueryableUsers => false; /// /// Developers will need to override this to support custom 2 factor auth /// public override bool SupportsUserTwoFactor => false; // TODO: Support this public override bool SupportsUserPhoneNumber => false; #endregion /// /// Initializes the user manager with the correct options /// protected void InitUserManager( BackOfficeUserManager manager, IDataProtectionProvider dataProtectionProvider) { // use a custom hasher based on our membership provider PasswordHasher = GetDefaultPasswordHasher(PasswordConfiguration); // set OWIN data protection token provider as default if (dataProtectionProvider != null) { manager.RegisterTokenProvider( TokenOptions.DefaultProvider, new OwinDataProtectorTokenProvider(dataProtectionProvider.Create("ASP.NET Identity")) { TokenLifespan = TimeSpan.FromDays(3) }); } // register ASP.NET Core Identity token providers manager.RegisterTokenProvider(TokenOptions.DefaultEmailProvider, new EmailTokenProvider()); manager.RegisterTokenProvider(TokenOptions.DefaultPhoneProvider, new PhoneNumberTokenProvider()); manager.RegisterTokenProvider(TokenOptions.DefaultAuthenticatorProvider, new AuthenticatorTokenProvider()); } /// /// Used to validate a user's session /// /// /// /// public virtual async Task ValidateSessionIdAsync(string userId, string sessionId) { var userSessionStore = Store as IUserSessionStore; //if this is not set, for backwards compat (which would be super rare), we'll just approve it if (userSessionStore == null) return true; return await userSessionStore.ValidateSessionIdAsync(userId, sessionId); } /// /// This will determine which password hasher to use based on what is defined in config /// /// protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) { //we can use the user aware password hasher (which will be the default and preferred way) return new UserAwarePasswordHasher(new PasswordSecurity(passwordConfiguration)); } /// /// Gets/sets the default back office user password checker /// public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; } public IPasswordConfiguration PasswordConfiguration { get; } public IIpResolver IpResolver { get; } /// /// Helper method to generate a password for a user based on the current password validator /// /// public string GeneratePassword() { if (_passwordGenerator == null) _passwordGenerator = new PasswordGenerator(PasswordConfiguration); var password = _passwordGenerator.GeneratePassword(); 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(T user) { if (user == null) throw new ArgumentNullException(nameof(user)); if (user.IsApproved == false) return true; return await base.IsLockedOutAsync(user); } #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); } /// /// This is a special method that will reset the password but will raise the Password Changed event instead of the reset event /// /// /// /// /// /// /// We use this because in the back office the only way an admin can change another user's password without first knowing their password /// is to generate a token and reset it, however, when we do this we want to track a password change, not a password reset /// public async Task ChangePasswordWithResetAsync(int userId, string token, string newPassword) { var user = await base.FindByIdAsync(userId.ToString()); var result = await base.ResetPasswordAsync(user, token, newPassword); if (result.Succeeded) RaisePasswordChangedEvent(userId); return result; } public override async Task ChangePasswordAsync(T user, string currentPassword, string newPassword) { var result = await base.ChangePasswordAsync(user, currentPassword, newPassword); if (result.Succeeded) RaisePasswordChangedEvent(user.Id); return result; } /// /// Override to determine how to hash the password /// /// /// /// /// protected override async Task VerifyPasswordAsync(IUserPasswordStore store, T user, string password) { var userAwarePasswordHasher = PasswordHasher; if (userAwarePasswordHasher == null) return await base.VerifyPasswordAsync(store, user, password); var hash = await store.GetPasswordHashAsync(user, CancellationToken.None); return userAwarePasswordHasher.VerifyHashedPassword(user, hash, password); } /// /// 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 UpdatePasswordHash(T user, string newPassword, bool validatePassword) { user.LastPasswordChangeDateUtc = DateTime.UtcNow; if (validatePassword) { var validate = await ValidatePasswordAsync(user, newPassword); if (!validate.Succeeded) { return validate; } } var passwordStore = Store as IUserPasswordStore; if (passwordStore == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserPasswordStore<>)); var hash = newPassword != null ? PasswordHasher.HashPassword(user, newPassword) : null; await passwordStore.SetPasswordHashAsync(user, hash, CancellationToken); await UpdateSecurityStampInternal(user); return IdentityResult.Success; } /// /// This is copied from the underlying .NET base class since they decided to not expose it /// /// /// private async Task UpdateSecurityStampInternal(T user) { if (SupportsUserSecurityStamp == false) return; await GetSecurityStore().SetSecurityStampAsync(user, NewSecurityStamp(), CancellationToken.None); } /// /// This is copied from the underlying .NET base class since they decided 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 decided to not expose it /// /// private static string NewSecurityStamp() { return Guid.NewGuid().ToString(); } #endregion public override async Task SetLockoutEndDateAsync(T user, DateTimeOffset? lockoutEnd) { if (user == null) throw new ArgumentNullException(nameof(user)); var result = await base.SetLockoutEndDateAsync(user, lockoutEnd); // The way we unlock is by setting the lockoutEnd date to the current datetime if (result.Succeeded && lockoutEnd >= DateTimeOffset.UtcNow) { RaiseAccountLockedEvent(user.Id); } else { RaiseAccountUnlockedEvent(user.Id); //Resets the login attempt fails back to 0 when unlock is clicked await ResetAccessFailedCountAsync(user); } return result; } public override async Task ResetAccessFailedCountAsync(T user) { if (user == null) throw new ArgumentNullException(nameof(user)); var lockoutStore = (IUserLockoutStore)Store; var accessFailedCount = await GetAccessFailedCountAsync(user); if (accessFailedCount == 0) return IdentityResult.Success; await lockoutStore.ResetAccessFailedCountAsync(user, CancellationToken.None); //raise the event now that it's reset RaiseResetAccessFailedCountEvent(user.Id); return await UpdateAsync(user); } /// /// Overrides the Microsoft ASP.NET user management method /// /// /// /// returns a Async Task /// /// /// Doesn't set fail attempts back to 0 /// public override async Task AccessFailedAsync(T user) { if (user == null) throw new ArgumentNullException(nameof(user)); var lockoutStore = Store as IUserLockoutStore; if (lockoutStore == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserLockoutStore<>)); var count = await lockoutStore.IncrementAccessFailedCountAsync(user, CancellationToken.None); if (count >= Options.Lockout.MaxFailedAccessAttempts) { await lockoutStore.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.Add(Options.Lockout.DefaultLockoutTimeSpan), CancellationToken.None); //NOTE: in normal aspnet identity this would do set the number of failed attempts back to 0 //here we are persisting the value for the back office } var result = await UpdateAsync(user); //Slightly confusing: this will return a Success if we successfully update the AccessFailed count if (result.Succeeded) RaiseLoginFailedEvent(user.Id); return result; } internal void RaiseAccountLockedEvent(int userId) { OnAccountLocked(new IdentityAuditEventArgs(AuditEvent.AccountLocked, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } internal void RaiseAccountUnlockedEvent(int userId) { OnAccountUnlocked(new IdentityAuditEventArgs(AuditEvent.AccountUnlocked, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } internal void RaiseForgotPasswordRequestedEvent(int userId) { OnForgotPasswordRequested(new IdentityAuditEventArgs(AuditEvent.ForgotPasswordRequested, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } internal void RaiseForgotPasswordChangedSuccessEvent(int userId) { OnForgotPasswordChangedSuccess(new IdentityAuditEventArgs(AuditEvent.ForgotPasswordChangedSuccess, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } internal void RaiseLoginFailedEvent(int userId) { OnLoginFailed(new IdentityAuditEventArgs(AuditEvent.LoginFailed, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } internal void RaiseInvalidLoginAttemptEvent(string username) { OnLoginFailed(new IdentityAuditEventArgs(AuditEvent.LoginFailed, IpResolver.GetCurrentRequestIpAddress(), username, string.Format("Attempted login for username '{0}' failed", username))); } internal void RaiseLoginRequiresVerificationEvent(int userId) { OnLoginRequiresVerification(new IdentityAuditEventArgs(AuditEvent.LoginRequiresVerification, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } internal void RaiseLoginSuccessEvent(int userId) { OnLoginSuccess(new IdentityAuditEventArgs(AuditEvent.LoginSucces, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } internal void RaiseLogoutSuccessEvent(int userId) { OnLogoutSuccess(new IdentityAuditEventArgs(AuditEvent.LogoutSuccess, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } internal void RaisePasswordChangedEvent(int userId) { OnPasswordChanged(new IdentityAuditEventArgs(AuditEvent.PasswordChanged, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } internal void RaiseResetAccessFailedCountEvent(int userId) { OnResetAccessFailedCount(new IdentityAuditEventArgs(AuditEvent.ResetAccessFailedCount, IpResolver.GetCurrentRequestIpAddress(), affectedUser: 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); } } }