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(); } } }