using System; using System.Collections.Generic; using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Core; using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; using Umbraco.Extensions; using Umbraco.Net; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Common.Security { public class BackOfficeUserManager : BackOfficeUserManager, IBackOfficeUserManager { public BackOfficeUserManager( IIpResolver ipResolver, IUserStore store, IOptions optionsAccessor, IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, BackOfficeLookupNormalizer keyNormalizer, BackOfficeIdentityErrorDescriber errors, IServiceProvider services, IHttpContextAccessor httpContextAccessor, ILogger> logger, IOptions passwordConfiguration) : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, httpContextAccessor, logger, passwordConfiguration) { } } public class BackOfficeUserManager : UserManager where T : BackOfficeIdentityUser { private PasswordGenerator _passwordGenerator; private readonly IHttpContextAccessor _httpContextAccessor; public BackOfficeUserManager( IIpResolver ipResolver, IUserStore store, IOptions optionsAccessor, IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, BackOfficeLookupNormalizer keyNormalizer, BackOfficeIdentityErrorDescriber errors, IServiceProvider services, IHttpContextAccessor httpContextAccessor, ILogger> logger, IOptions passwordConfiguration) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) { IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver)); _httpContextAccessor = httpContextAccessor; PasswordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); } // We don't support an IUserClaimStore and don't need to (at least currently) public override bool SupportsUserClaim => false; // It would be nice to support this but we don't need to currently and that would require IQueryable support for our user service/repository public override bool SupportsQueryableUsers => false; /// /// Developers will need to override this to support custom 2 factor auth /// public override bool SupportsUserTwoFactor => false; // We haven't needed to support this yet, though might be necessary for 2FA public override bool SupportsUserPhoneNumber => false; /// /// Replace the underlying options property with our own strongly typed version /// public new BackOfficeIdentityOptions Options { get => (BackOfficeIdentityOptions)base.Options; set => base.Options = value; } /// /// Used to validate a user's session /// /// The user id /// The sesion id /// True if the sesion is valid, else false 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 /// /// The /// An protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) => new PasswordHasher(); /// /// Gets/sets the default back office user password checker /// public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; } public IPasswordConfiguration PasswordConfiguration { get; protected set; } public IIpResolver IpResolver { get; } /// /// Helper method to generate a password for a user based on the current password validator /// /// The generated password 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 /// /// The user /// True if the user is locked out, else false /// /// 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); } /// /// 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) { BackOfficeUserPasswordCheckerResult 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 /// /// The userId /// The reset password token /// The new password to set it to /// The /// /// 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) { T user = await FindByIdAsync(userId.ToString()); if (user == null) { throw new InvalidOperationException("Could not find user"); } IdentityResult result = await base.ResetPasswordAsync(user, token, newPassword); if (result.Succeeded) { RaisePasswordChangedEvent(_httpContextAccessor.HttpContext?.User, userId); } return result; } public override async Task ChangePasswordAsync(T user, string currentPassword, string newPassword) { IdentityResult result = await base.ChangePasswordAsync(user, currentPassword, newPassword); if (result.Succeeded) { RaisePasswordChangedEvent(_httpContextAccessor.HttpContext?.User, user.Id); } return result; } /// /// Override to determine how to hash the password /// /// protected override async Task UpdatePasswordHash(T user, string newPassword, bool validatePassword) { user.LastPasswordChangeDateUtc = DateTime.UtcNow; if (validatePassword) { IdentityResult 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() => Guid.NewGuid().ToString(); /// public override async Task SetLockoutEndDateAsync(T user, DateTimeOffset? lockoutEnd) { if (user == null) { throw new ArgumentNullException(nameof(user)); } IdentityResult 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(_httpContextAccessor.HttpContext?.User, user.Id); } else { RaiseAccountUnlockedEvent(_httpContextAccessor.HttpContext?.User, 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(_httpContextAccessor.HttpContext?.User, user.Id); return await UpdateAsync(user); } /// /// Overrides the Microsoft ASP.NET user management method /// /// 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 } IdentityResult result = await UpdateAsync(user); // Slightly confusing: this will return a Success if we successfully update the AccessFailed count if (result.Succeeded) { RaiseLoginFailedEvent(_httpContextAccessor.HttpContext?.User, user.Id); } return result; } private int GetCurrentUserId(IPrincipal currentUser) { UmbracoBackOfficeIdentity umbIdentity = currentUser?.GetUmbracoIdentity(); var currentUserId = umbIdentity?.GetUserId() ?? Core.Constants.Security.SuperUserId; return currentUserId; } private IdentityAuditEventArgs CreateArgs(AuditEvent auditEvent, IPrincipal currentUser, int affectedUserId, string affectedUsername) { var currentUserId = GetCurrentUserId(currentUser); var ip = IpResolver.GetCurrentRequestIpAddress(); return new IdentityAuditEventArgs(auditEvent, ip, currentUserId, string.Empty, affectedUserId, affectedUsername); } private IdentityAuditEventArgs CreateArgs(AuditEvent auditEvent, BackOfficeIdentityUser currentUser, int affectedUserId, string affectedUsername) { var currentUserId = currentUser.Id; var ip = IpResolver.GetCurrentRequestIpAddress(); return new IdentityAuditEventArgs(auditEvent, ip, currentUserId, string.Empty, affectedUserId, affectedUsername); } // TODO: Review where these are raised and see if they can be simplified and either done in the this usermanager or the signin manager, // lastly we'll resort to the authentication controller but we should try to remove all instances of that occuring public void RaiseAccountLockedEvent(IPrincipal currentUser, int userId) => OnAccountLocked(CreateArgs(AuditEvent.AccountLocked, currentUser, userId, string.Empty)); public void RaiseAccountUnlockedEvent(IPrincipal currentUser, int userId) => OnAccountUnlocked(CreateArgs(AuditEvent.AccountUnlocked, currentUser, userId, string.Empty)); public void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, int userId) => OnForgotPasswordRequested(CreateArgs(AuditEvent.ForgotPasswordRequested, currentUser, userId, string.Empty)); public void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, int userId) => OnForgotPasswordChangedSuccess(CreateArgs(AuditEvent.ForgotPasswordChangedSuccess, currentUser, userId, string.Empty)); public void RaiseLoginFailedEvent(IPrincipal currentUser, int userId) => OnLoginFailed(CreateArgs(AuditEvent.LoginFailed, currentUser, userId, string.Empty)); public void RaiseLoginRequiresVerificationEvent(IPrincipal currentUser, int userId) => OnLoginRequiresVerification(CreateArgs(AuditEvent.LoginRequiresVerification, currentUser, userId, string.Empty)); public void RaiseLoginSuccessEvent(IPrincipal currentUser, int userId) => OnLoginSuccess(CreateArgs(AuditEvent.LoginSucces, currentUser, userId, string.Empty)); public SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, int userId) { var currentUserId = GetCurrentUserId(currentUser); var args = new SignOutAuditEventArgs(AuditEvent.LogoutSuccess, IpResolver.GetCurrentRequestIpAddress(), performingUser: currentUserId, affectedUser: userId); OnLogoutSuccess(args); return args; } public void RaisePasswordChangedEvent(IPrincipal currentUser, int userId) => OnPasswordChanged(CreateArgs(AuditEvent.LogoutSuccess, currentUser, userId, string.Empty)); public void RaiseResetAccessFailedCountEvent(IPrincipal currentUser, int userId) => OnResetAccessFailedCount(CreateArgs(AuditEvent.ResetAccessFailedCount, currentUser, userId, string.Empty)); public UserInviteEventArgs RaiseSendingUserInvite(IPrincipal currentUser, UserInvite invite, IUser createdUser) { var currentUserId = GetCurrentUserId(currentUser); var ip = IpResolver.GetCurrentRequestIpAddress(); var args = new UserInviteEventArgs(ip, currentUserId, invite, createdUser); OnSendingUserInvite(args); return args; } public bool HasSendingUserInviteEventHandler => SendingUserInvite != null; // TODO: These static events are problematic. Moving forward we don't want static events at all but we cannot // have non-static events here because the user manager is a Scoped instance not a singleton // so we'll have to deal with this a diff way i.e. refactoring how events are done entirely 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; /// /// Raised when a user is invited /// public static event EventHandler SendingUserInvite; // this event really has nothing to do with the user manager but was the most convenient place to put it protected virtual void OnAccountLocked(IdentityAuditEventArgs e) => AccountLocked?.Invoke(this, e); protected virtual void OnSendingUserInvite(UserInviteEventArgs e) => SendingUserInvite?.Invoke(this, e); protected virtual void OnAccountUnlocked(IdentityAuditEventArgs e) => AccountUnlocked?.Invoke(this, e); protected virtual void OnForgotPasswordRequested(IdentityAuditEventArgs e) => ForgotPasswordRequested?.Invoke(this, e); protected virtual void OnForgotPasswordChangedSuccess(IdentityAuditEventArgs e) => ForgotPasswordChangedSuccess?.Invoke(this, e); protected virtual void OnLoginFailed(IdentityAuditEventArgs e) => LoginFailed?.Invoke(this, e); protected virtual void OnLoginRequiresVerification(IdentityAuditEventArgs e) => LoginRequiresVerification?.Invoke(this, e); protected virtual void OnLoginSuccess(IdentityAuditEventArgs e) => LoginSuccess?.Invoke(this, e); protected virtual void OnLogoutSuccess(SignOutAuditEventArgs e) => LogoutSuccess?.Invoke(this, e); protected virtual void OnPasswordChanged(IdentityAuditEventArgs e) => PasswordChanged?.Invoke(this, e); protected virtual void OnPasswordReset(IdentityAuditEventArgs e) => PasswordReset?.Invoke(this, e); protected virtual void OnResetAccessFailedCount(IdentityAuditEventArgs e) => ResetAccessFailedCount?.Invoke(this, e); } }