using System; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Web.Security; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin.Security.DataProtection; using Umbraco.Core.Models.Identity; 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) { } public BackOfficeUserManager( IUserStore store, IdentityFactoryOptions options, MembershipProviderBase membershipProvider) : base(store) { if (options == null) throw new ArgumentNullException("options");; InitUserManager(this, membershipProvider, options); } #region Static Create methods /// /// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager /// /// /// /// /// /// public static BackOfficeUserManager Create( IdentityFactoryOptions options, IUserService userService, IExternalLoginService externalLoginService, MembershipProviderBase membershipProvider) { 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, externalLoginService, membershipProvider)); manager.InitUserManager(manager, membershipProvider, options); 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) { var manager = new BackOfficeUserManager(customUserStore, options, membershipProvider); return manager; } #endregion /// /// Initializes the user manager with the correct options /// /// /// /// /// protected void InitUserManager( BackOfficeUserManager manager, MembershipProviderBase membershipProvider, IdentityFactoryOptions options) { //NOTE: This method is mostly here for backwards compat base.InitUserManager(manager, membershipProvider, options.DataProtectionProvider); } } /// /// 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 /// /// Initializes the user manager with the correct options /// /// /// /// The for the users called UsersMembershipProvider /// /// /// protected void InitUserManager( BackOfficeUserManager manager, MembershipProviderBase membershipProvider, IDataProtectionProvider dataProtectionProvider) { // Configure validation logic for usernames manager.UserValidator = new UserValidator(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(); //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; } #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 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; } } //use the default behavior return await base.CheckPasswordAsync(user, password); } public override Task ChangePasswordAsync(int userId, string currentPassword, string newPassword) { return base.ChangePasswordAsync(userId, currentPassword, newPassword); } /// /// 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 } }