diff --git a/src/Umbraco.Web.BackOffice/Extensions/ClaimsPrincipalExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/ClaimsPrincipalExtensions.cs
new file mode 100644
index 0000000000..833522c2c1
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Extensions/ClaimsPrincipalExtensions.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Linq;
+using System.Security.Claims;
+using System.Security.Principal;
+using Umbraco.Core;
+using Umbraco.Web.BackOffice.Identity;
+
+namespace Umbraco.Extensions
+{
+ public static class ClaimsPrincipalExtensions
+ {
+ ///
+ /// This will return the current back office identity if the IPrincipal is the correct type
+ ///
+ ///
+ ///
+ public static UmbracoBackOfficeIdentity GetUmbracoIdentity(this IPrincipal user)
+ {
+ //If it's already a UmbracoBackOfficeIdentity
+ if (user.Identity is UmbracoBackOfficeIdentity backOfficeIdentity) return backOfficeIdentity;
+
+ //Check if there's more than one identity assigned and see if it's a UmbracoBackOfficeIdentity and use that
+ if (user is ClaimsPrincipal claimsPrincipal)
+ {
+ backOfficeIdentity = claimsPrincipal.Identities.OfType().FirstOrDefault();
+ if (backOfficeIdentity != null) return backOfficeIdentity;
+ }
+
+ //Otherwise convert to a UmbracoBackOfficeIdentity if it's auth'd and has the back office session
+ if (user.Identity is ClaimsIdentity claimsIdentity && claimsIdentity.IsAuthenticated && claimsIdentity.HasClaim(x => x.Type == Constants.Security.SessionIdClaimType))
+ {
+ try
+ {
+ return UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity);
+ }
+ catch (InvalidOperationException)
+ {
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserManager.cs b/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserManager.cs
new file mode 100644
index 0000000000..79da59fce5
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserManager.cs
@@ -0,0 +1,561 @@
+using System;
+using System.Collections.Generic;
+using System.Security.Claims;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Umbraco.Core;
+using Umbraco.Core.Configuration;
+using Umbraco.Core.Mapping;
+using Umbraco.Core.Security;
+using Umbraco.Core.Services;
+using Umbraco.Net;
+
+namespace Umbraco.Web.BackOffice.Identity
+{
+ 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,
+ ILogger> logger)
+ : base(passwordConfiguration, ipResolver, store, optionsAccessor, userValidators, passwordValidators, keyNormalizer, errors, null, logger)
+ {
+ InitUserManager(this);
+ }
+
+ #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,
+ 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)
+ {
+ // use a custom hasher based on our membership provider
+ PasswordHasher = GetDefaultPasswordHasher(PasswordConfiguration);
+ }
+
+ ///
+ /// 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());
+ if (user == null) throw new InvalidOperationException("Could not find user");
+
+ 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
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// 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);
+ }
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserPasswordCheckerResult.cs b/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserPasswordCheckerResult.cs
new file mode 100644
index 0000000000..72c0707115
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserPasswordCheckerResult.cs
@@ -0,0 +1,12 @@
+namespace Umbraco.Web.BackOffice.Identity
+{
+ ///
+ /// The result returned from the IBackOfficeUserPasswordChecker
+ ///
+ public enum BackOfficeUserPasswordCheckerResult
+ {
+ ValidCredentials,
+ InvalidCredentials,
+ FallbackToDefaultChecker
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserValidator.cs b/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserValidator.cs
new file mode 100644
index 0000000000..f80d8b90db
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserValidator.cs
@@ -0,0 +1,19 @@
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+
+namespace Umbraco.Web.BackOffice.Identity
+{
+ public class BackOfficeUserValidator : UserValidator
+ where T : BackOfficeIdentityUser
+ {
+ public override async Task ValidateAsync(UserManager manager, T user)
+ {
+ // Don't validate if the user's email or username hasn't changed otherwise it's just wasting SQL queries.
+ if (user.IsPropertyDirty("Email") || user.IsPropertyDirty("UserName"))
+ {
+ return await base.ValidateAsync(manager, user);
+ }
+ return IdentityResult.Success;
+ }
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Identity/IBackOfficeUserPasswordChecker.cs b/src/Umbraco.Web.BackOffice/Identity/IBackOfficeUserPasswordChecker.cs
new file mode 100644
index 0000000000..b94e261506
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Identity/IBackOfficeUserPasswordChecker.cs
@@ -0,0 +1,24 @@
+using System.Threading.Tasks;
+
+namespace Umbraco.Web.BackOffice.Identity
+{
+ ///
+ /// Used by the BackOfficeUserManager to check the username/password which allows for developers to more easily
+ /// set the logic for this procedure.
+ ///
+ public interface IBackOfficeUserPasswordChecker
+ {
+ ///
+ /// Checks a password for a user
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// This will allow a developer to auto-link a local account which is required if the user queried doesn't exist locally.
+ /// The user parameter will always contain the username, if the user doesn't exist locally, the other properties will not be filled in.
+ /// A developer can then create a local account by filling in the properties and using UserManager.CreateAsync
+ ///
+ Task CheckPasswordAsync(BackOfficeIdentityUser user, string password);
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Identity/IdentityAuditEventArgs.cs b/src/Umbraco.Web.BackOffice/Identity/IdentityAuditEventArgs.cs
new file mode 100644
index 0000000000..cb0b04ef56
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Identity/IdentityAuditEventArgs.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Threading;
+using Umbraco.Extensions;
+
+
+namespace Umbraco.Web.BackOffice.Identity
+{
+ ///
+ /// This class is used by events raised from the BackofficeUserManager
+ ///
+ public class IdentityAuditEventArgs : EventArgs
+ {
+ ///
+ /// The action that got triggered from the audit event
+ ///
+ public AuditEvent Action { get; private set; }
+
+ ///
+ /// Current date/time in UTC format
+ ///
+ public DateTime DateTimeUtc { get; private set; }
+
+ ///
+ /// The source IP address of the user performing the action
+ ///
+ public string IpAddress { get; private set; }
+
+ ///
+ /// The user affected by the event raised
+ ///
+ public int AffectedUser { get; private set; }
+
+ ///
+ /// If a user is performing an action on a different user, then this will be set. Otherwise it will be -1
+ ///
+ public int PerformingUser { get; private set; }
+
+ ///
+ /// An optional comment about the action being logged
+ ///
+ public string Comment { get; private set; }
+
+ ///
+ /// This property is always empty except in the LoginFailed event for an unknown user trying to login
+ ///
+ public string Username { get; private set; }
+
+
+ ///
+ /// Default constructor
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string comment = null, int performingUser = -1, int affectedUser = -1)
+ {
+ DateTimeUtc = DateTime.UtcNow;
+ Action = action;
+
+ IpAddress = ipAddress;
+ Comment = comment;
+ AffectedUser = affectedUser;
+
+ PerformingUser = performingUser == -1
+ ? GetCurrentRequestBackofficeUserId()
+ : performingUser;
+ }
+
+ ///
+ /// Creates an instance without a performing or affected user (the id will be set to -1)
+ ///
+ ///
+ ///
+ ///
+ ///
+ public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string username, string comment)
+ {
+ DateTimeUtc = DateTime.UtcNow;
+ Action = action;
+
+ IpAddress = ipAddress;
+ Username = username;
+ Comment = comment;
+
+ PerformingUser = -1;
+ }
+
+ public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string username, string comment, int performingUser)
+ {
+ DateTimeUtc = DateTime.UtcNow;
+ Action = action;
+
+ IpAddress = ipAddress;
+ Username = username;
+ Comment = comment;
+
+ PerformingUser = performingUser == -1
+ ? GetCurrentRequestBackofficeUserId()
+ : performingUser;
+ }
+
+ ///
+ /// Returns the current logged in backoffice user's Id logging if there is one
+ ///
+ ///
+ protected int GetCurrentRequestBackofficeUserId()
+ {
+ var userId = -1;
+ var backOfficeIdentity = Thread.CurrentPrincipal.GetUmbracoIdentity();
+ if (backOfficeIdentity != null)
+ int.TryParse(backOfficeIdentity.Id.ToString(), out userId);
+ return userId;
+ }
+ }
+
+ public enum AuditEvent
+ {
+ AccountLocked,
+ AccountUnlocked,
+ ForgotPasswordRequested,
+ ForgotPasswordChangedSuccess,
+ LoginFailed,
+ LoginRequiresVerification,
+ LoginSucces,
+ LogoutSuccess,
+ PasswordChanged,
+ PasswordReset,
+ ResetAccessFailedCount
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Identity/NopLookupNormalizer.cs b/src/Umbraco.Web.BackOffice/Identity/NopLookupNormalizer.cs
new file mode 100644
index 0000000000..c846498098
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Identity/NopLookupNormalizer.cs
@@ -0,0 +1,14 @@
+using Microsoft.AspNetCore.Identity;
+
+namespace Umbraco.Web.BackOffice.Identity
+{
+ ///
+ /// No-op lookup normalizer to maintain compatibility with ASP.NET Identity 2
+ ///
+ public class NopLookupNormalizer : ILookupNormalizer
+ {
+ public string NormalizeName(string name) => name;
+
+ public string NormalizeEmail(string email) => email;
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Identity/PasswordSecurity.cs b/src/Umbraco.Web.BackOffice/Identity/PasswordSecurity.cs
new file mode 100644
index 0000000000..49405d0c5a
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Identity/PasswordSecurity.cs
@@ -0,0 +1,223 @@
+using System;
+using System.Collections.Generic;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+using Umbraco.Core;
+using Umbraco.Core.Configuration;
+using Umbraco.Core.Security;
+
+namespace Umbraco.Web.BackOffice.Identity
+{
+ ///
+ /// Handles password hashing and formatting
+ ///
+ public class PasswordSecurity
+ {
+ public IPasswordConfiguration PasswordConfiguration { get; }
+ public PasswordGenerator _generator;
+ public ConfiguredPasswordValidator _validator;
+
+ ///
+ /// Constructor
+ ///
+ ///
+ public PasswordSecurity(IPasswordConfiguration passwordConfiguration)
+ {
+ PasswordConfiguration = passwordConfiguration;
+ }
+
+ ///
+ /// Checks if the password passes validation rules
+ ///
+ ///
+ ///
+ public async Task>> IsValidPasswordAsync(string password)
+ {
+ if (_validator == null)
+ _validator = new ConfiguredPasswordValidator(PasswordConfiguration);
+ var result = await _validator.ValidateAsync(password);
+ if (result.Succeeded)
+ return Attempt>.Succeed();
+
+ return Attempt>.Fail(result.Errors);
+ }
+
+ public string GeneratePassword()
+ {
+ if (_generator == null)
+ _generator = new PasswordGenerator(PasswordConfiguration);
+ return _generator.GeneratePassword();
+ }
+
+ ///
+ /// Returns a hashed password value used to store in a data store
+ ///
+ ///
+ ///
+ public string HashPasswordForStorage(string password)
+ {
+ if (string.IsNullOrWhiteSpace(password))
+ throw new ArgumentException("password cannot be empty", nameof(password));
+
+ string salt;
+ var hashed = HashNewPassword(password, out salt);
+ return FormatPasswordForStorage(hashed, salt);
+ }
+
+ ///
+ /// If the password format is a hashed keyed algorithm then we will pre-pend the salt used to hash the password
+ /// to the hashed password itself.
+ ///
+ ///
+ ///
+ ///
+ public string FormatPasswordForStorage(string hashedPassword, string salt)
+ {
+ return salt + hashedPassword;
+ }
+
+ ///
+ /// Hashes a password with a given salt
+ ///
+ ///
+ ///
+ ///
+ public string HashPassword(string pass, string salt)
+ {
+ //This is the correct way to implement this (as per the sql membership provider)
+
+ var bytes = Encoding.Unicode.GetBytes(pass);
+ var saltBytes = Convert.FromBase64String(salt);
+ byte[] inArray;
+
+ var hashAlgorithm = GetHashAlgorithm(pass);
+ var algorithm = hashAlgorithm as KeyedHashAlgorithm;
+ if (algorithm != null)
+ {
+ var keyedHashAlgorithm = algorithm;
+ if (keyedHashAlgorithm.Key.Length == saltBytes.Length)
+ {
+ //if the salt bytes is the required key length for the algorithm, use it as-is
+ keyedHashAlgorithm.Key = saltBytes;
+ }
+ else if (keyedHashAlgorithm.Key.Length < saltBytes.Length)
+ {
+ //if the salt bytes is too long for the required key length for the algorithm, reduce it
+ var numArray2 = new byte[keyedHashAlgorithm.Key.Length];
+ Buffer.BlockCopy(saltBytes, 0, numArray2, 0, numArray2.Length);
+ keyedHashAlgorithm.Key = numArray2;
+ }
+ else
+ {
+ //if the salt bytes is too short for the required key length for the algorithm, extend it
+ var numArray2 = new byte[keyedHashAlgorithm.Key.Length];
+ var dstOffset = 0;
+ while (dstOffset < numArray2.Length)
+ {
+ var count = Math.Min(saltBytes.Length, numArray2.Length - dstOffset);
+ Buffer.BlockCopy(saltBytes, 0, numArray2, dstOffset, count);
+ dstOffset += count;
+ }
+ keyedHashAlgorithm.Key = numArray2;
+ }
+ inArray = keyedHashAlgorithm.ComputeHash(bytes);
+ }
+ else
+ {
+ var buffer = new byte[saltBytes.Length + bytes.Length];
+ Buffer.BlockCopy(saltBytes, 0, buffer, 0, saltBytes.Length);
+ Buffer.BlockCopy(bytes, 0, buffer, saltBytes.Length, bytes.Length);
+ inArray = hashAlgorithm.ComputeHash(buffer);
+ }
+
+ return Convert.ToBase64String(inArray);
+ }
+
+ ///
+ /// Verifies if the password matches the expected hash+salt of the stored password string
+ ///
+ /// The password.
+ /// The value of the password stored in a data store.
+ ///
+ public bool VerifyPassword(string password, string dbPassword)
+ {
+ if (string.IsNullOrWhiteSpace(dbPassword)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(dbPassword));
+
+ if (dbPassword.StartsWith(Constants.Security.EmptyPasswordPrefix))
+ return false;
+
+ var storedHashedPass = ParseStoredHashPassword(dbPassword, out var salt);
+ var hashed = HashPassword(password, salt);
+ return storedHashedPass == hashed;
+ }
+
+ ///
+ /// Create a new password hash and a new salt
+ ///
+ ///
+ ///
+ ///
+ public string HashNewPassword(string newPassword, out string salt)
+ {
+ salt = GenerateSalt();
+ return HashPassword(newPassword, salt);
+ }
+
+ ///
+ /// Parses out the hashed password and the salt from the stored password string value
+ ///
+ ///
+ /// returns the salt
+ ///
+ public string ParseStoredHashPassword(string storedString, out string salt)
+ {
+ if (string.IsNullOrWhiteSpace(storedString)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(storedString));
+
+ var saltLen = GenerateSalt();
+ salt = storedString.Substring(0, saltLen.Length);
+ return storedString.Substring(saltLen.Length);
+ }
+
+ public static string GenerateSalt()
+ {
+ var numArray = new byte[16];
+ new RNGCryptoServiceProvider().GetBytes(numArray);
+ return Convert.ToBase64String(numArray);
+ }
+
+ ///
+ /// Return the hash algorithm to use
+ ///
+ ///
+ ///
+ public HashAlgorithm GetHashAlgorithm(string password)
+ {
+ if (PasswordConfiguration.HashAlgorithmType.IsNullOrWhiteSpace())
+ throw new InvalidOperationException("No hash algorithm type specified");
+
+ var alg = HashAlgorithm.Create(PasswordConfiguration.HashAlgorithmType);
+ if (alg == null)
+ throw new InvalidOperationException($"The hash algorithm specified {PasswordConfiguration.HashAlgorithmType} cannot be resolved");
+
+ return alg;
+ }
+
+ ///
+ /// Encodes the password.
+ ///
+ /// The password.
+ /// The encoded password.
+ private string LegacyEncodePassword(string password)
+ {
+ var hashAlgorith = GetHashAlgorithm(password);
+ var encodedPassword = Convert.ToBase64String(hashAlgorith.ComputeHash(Encoding.Unicode.GetBytes(password)));
+ return encodedPassword;
+ }
+
+
+
+
+
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Identity/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Web.BackOffice/Identity/UmbracoBackOfficeIdentity.cs
new file mode 100644
index 0000000000..ca3e6d742f
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Identity/UmbracoBackOfficeIdentity.cs
@@ -0,0 +1,232 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using Umbraco.Core;
+
+namespace Umbraco.Web.BackOffice.Identity
+{
+
+ ///
+ /// A custom user identity for the Umbraco backoffice
+ ///
+ ///
+ /// This inherits from FormsIdentity for backwards compatibility reasons since we still support the forms auth cookie, in v8 we can
+ /// change over to 'pure' asp.net identity and just inherit from ClaimsIdentity.
+ ///
+ [Serializable]
+ public class UmbracoBackOfficeIdentity : ClaimsIdentity
+ {
+ public static UmbracoBackOfficeIdentity FromClaimsIdentity(ClaimsIdentity identity)
+ {
+ return new UmbracoBackOfficeIdentity(identity);
+ }
+
+ ///
+ /// Creates a new UmbracoBackOfficeIdentity
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public UmbracoBackOfficeIdentity(int userId, string username, string realName,
+ IEnumerable startContentNodes, IEnumerable startMediaNodes, string culture,
+ string sessionId, string securityStamp, IEnumerable allowedApps, IEnumerable roles)
+ : base(Enumerable.Empty(), Constants.Security.BackOfficeAuthenticationType) //this ctor is used to ensure the IsAuthenticated property is true
+ {
+ if (allowedApps == null) throw new ArgumentNullException(nameof(allowedApps));
+ if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username));
+ if (string.IsNullOrWhiteSpace(realName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(realName));
+ if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture));
+ if (string.IsNullOrWhiteSpace(sessionId)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(sessionId));
+ if (string.IsNullOrWhiteSpace(securityStamp)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(securityStamp));
+ AddRequiredClaims(userId, username, realName, startContentNodes, startMediaNodes, culture, sessionId, securityStamp, allowedApps, roles);
+ }
+
+ ///
+ /// Creates a new UmbracoBackOfficeIdentity
+ ///
+ ///
+ /// The original identity created by the ClaimsIdentityFactory
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public UmbracoBackOfficeIdentity(ClaimsIdentity childIdentity,
+ int userId, string username, string realName,
+ IEnumerable startContentNodes, IEnumerable startMediaNodes, string culture,
+ string sessionId, string securityStamp, IEnumerable allowedApps, IEnumerable roles)
+ : base(childIdentity.Claims, Constants.Security.BackOfficeAuthenticationType)
+ {
+ if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username));
+ if (string.IsNullOrWhiteSpace(realName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(realName));
+ if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture));
+ if (string.IsNullOrWhiteSpace(sessionId)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(sessionId));
+ if (string.IsNullOrWhiteSpace(securityStamp)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(securityStamp));
+ Actor = childIdentity;
+ AddRequiredClaims(userId, username, realName, startContentNodes, startMediaNodes, culture, sessionId, securityStamp, allowedApps, roles);
+ }
+
+ ///
+ /// Create a back office identity based on an existing claims identity
+ ///
+ ///
+ private UmbracoBackOfficeIdentity(ClaimsIdentity identity)
+ : base(identity.Claims, Constants.Security.BackOfficeAuthenticationType)
+ {
+ Actor = identity;
+
+ //validate that all claims exist
+ foreach (var t in RequiredBackOfficeIdentityClaimTypes)
+ {
+ //if the identity doesn't have the claim, or the claim value is null
+ if (identity.HasClaim(x => x.Type == t) == false || identity.HasClaim(x => x.Type == t && x.Value.IsNullOrWhiteSpace()))
+ {
+ throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since the required claim " + t + " is missing");
+ }
+ }
+ }
+
+ public const string Issuer = Constants.Security.BackOfficeAuthenticationType;
+
+ ///
+ /// Returns the required claim types for a back office identity
+ ///
+ ///
+ /// This does not include the role claim type or allowed apps type since that is a collection and in theory could be empty
+ ///
+ public static IEnumerable RequiredBackOfficeIdentityClaimTypes => new[]
+ {
+ ClaimTypes.NameIdentifier, //id
+ ClaimTypes.Name, //username
+ ClaimTypes.GivenName,
+ Constants.Security.StartContentNodeIdClaimType,
+ Constants.Security.StartMediaNodeIdClaimType,
+ ClaimTypes.Locality,
+ Constants.Security.SessionIdClaimType,
+ Constants.Web.SecurityStampClaimType
+ };
+
+ ///
+ /// Adds claims based on the ctor data
+ ///
+ private void AddRequiredClaims(int userId, string username, string realName,
+ IEnumerable startContentNodes, IEnumerable startMediaNodes, string culture,
+ string sessionId, string securityStamp, IEnumerable allowedApps, IEnumerable roles)
+ {
+ //This is the id that 'identity' uses to check for the user id
+ if (HasClaim(x => x.Type == ClaimTypes.NameIdentifier) == false)
+ AddClaim(new Claim(ClaimTypes.NameIdentifier, userId.ToInvariantString(), ClaimValueTypes.Integer32, Issuer, Issuer, this));
+
+ if (HasClaim(x => x.Type == ClaimTypes.Name) == false)
+ AddClaim(new Claim(ClaimTypes.Name, username, ClaimValueTypes.String, Issuer, Issuer, this));
+
+ if (HasClaim(x => x.Type == ClaimTypes.GivenName) == false)
+ AddClaim(new Claim(ClaimTypes.GivenName, realName, ClaimValueTypes.String, Issuer, Issuer, this));
+
+ if (HasClaim(x => x.Type == Constants.Security.StartContentNodeIdClaimType) == false && startContentNodes != null)
+ {
+ foreach (var startContentNode in startContentNodes)
+ {
+ AddClaim(new Claim(Constants.Security.StartContentNodeIdClaimType, startContentNode.ToInvariantString(), ClaimValueTypes.Integer32, Issuer, Issuer, this));
+ }
+ }
+
+ if (HasClaim(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) == false && startMediaNodes != null)
+ {
+ foreach (var startMediaNode in startMediaNodes)
+ {
+ AddClaim(new Claim(Constants.Security.StartMediaNodeIdClaimType, startMediaNode.ToInvariantString(), ClaimValueTypes.Integer32, Issuer, Issuer, this));
+ }
+ }
+
+ if (HasClaim(x => x.Type == ClaimTypes.Locality) == false)
+ AddClaim(new Claim(ClaimTypes.Locality, culture, ClaimValueTypes.String, Issuer, Issuer, this));
+
+ if (HasClaim(x => x.Type == Constants.Security.SessionIdClaimType) == false && SessionId.IsNullOrWhiteSpace() == false)
+ AddClaim(new Claim(Constants.Security.SessionIdClaimType, sessionId, ClaimValueTypes.String, Issuer, Issuer, this));
+
+ //The security stamp claim is also required... this is because this claim type is hard coded
+ // by the SecurityStampValidator, see: https://katanaproject.codeplex.com/workitem/444
+ if (HasClaim(x => x.Type == Constants.Web.SecurityStampClaimType) == false)
+ AddClaim(new Claim(Constants.Web.SecurityStampClaimType, securityStamp, ClaimValueTypes.String, Issuer, Issuer, this));
+
+ //Add each app as a separate claim
+ if (HasClaim(x => x.Type == Constants.Security.AllowedApplicationsClaimType) == false && allowedApps != null)
+ {
+ foreach (var application in allowedApps)
+ {
+ AddClaim(new Claim(Constants.Security.AllowedApplicationsClaimType, application, ClaimValueTypes.String, Issuer, Issuer, this));
+ }
+ }
+
+ //Claims are added by the ClaimsIdentityFactory because our UserStore supports roles, however this identity might
+ // not be made with that factory if it was created with a different ticket so perform the check
+ if (HasClaim(x => x.Type == DefaultRoleClaimType) == false && roles != null)
+ {
+ //manually add them
+ foreach (var roleName in roles)
+ {
+ AddClaim(new Claim(RoleClaimType, roleName, ClaimValueTypes.String, Issuer, Issuer, this));
+ }
+ }
+
+ }
+
+ ///
+ ///
+ /// Gets the type of authenticated identity.
+ ///
+ ///
+ /// The type of authenticated identity. This property always returns "UmbracoBackOffice".
+ ///
+ public override string AuthenticationType => Issuer;
+
+ private int[] _startContentNodes;
+ public int[] StartContentNodes => _startContentNodes ?? (_startContentNodes = FindAll(x => x.Type == Constants.Security.StartContentNodeIdClaimType).Select(app => int.TryParse(app.Value, out var i) ? i : default).Where(x => x != default).ToArray());
+
+ private int[] _startMediaNodes;
+ public int[] StartMediaNodes => _startMediaNodes ?? (_startMediaNodes = FindAll(x => x.Type == Constants.Security.StartMediaNodeIdClaimType).Select(app => int.TryParse(app.Value, out var i) ? i : default).Where(x => x != default).ToArray());
+
+ private string[] _allowedApplications;
+ public string[] AllowedApplications => _allowedApplications ?? (_allowedApplications = FindAll(x => x.Type == Constants.Security.AllowedApplicationsClaimType).Select(app => app.Value).ToArray());
+
+ public int Id => int.Parse(this.FindFirstValue(ClaimTypes.NameIdentifier));
+
+ public string RealName => this.FindFirstValue(ClaimTypes.GivenName);
+
+ public string Username => this.FindFirstValue(ClaimTypes.Name);
+
+ public string Culture => this.FindFirstValue(ClaimTypes.Locality);
+
+ public string SessionId
+ {
+ get => this.FindFirstValue(Constants.Security.SessionIdClaimType);
+ set
+ {
+ var existing = FindFirst(Constants.Security.SessionIdClaimType);
+ if (existing != null)
+ TryRemoveClaim(existing);
+ AddClaim(new Claim(Constants.Security.SessionIdClaimType, value, ClaimValueTypes.String, Issuer, Issuer, this));
+ }
+ }
+
+ public string SecurityStamp => this.FindFirstValue(Constants.Web.SecurityStampClaimType);
+
+ public string[] Roles => this.FindAll(x => x.Type == DefaultRoleClaimType).Select(role => role.Value).ToArray();
+
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Identity/UserAwarePasswordHasher.cs b/src/Umbraco.Web.BackOffice/Identity/UserAwarePasswordHasher.cs
new file mode 100644
index 0000000000..fb2b1e71a0
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Identity/UserAwarePasswordHasher.cs
@@ -0,0 +1,38 @@
+using Microsoft.AspNetCore.Identity;
+
+namespace Umbraco.Web.BackOffice.Identity
+{
+ public class UserAwarePasswordHasher : IPasswordHasher
+ where T : BackOfficeIdentityUser
+ {
+ private readonly PasswordSecurity _passwordSecurity;
+
+ public UserAwarePasswordHasher(PasswordSecurity passwordSecurity)
+ {
+ _passwordSecurity = passwordSecurity;
+ }
+
+ public string HashPassword(string password)
+ {
+ return _passwordSecurity.HashPasswordForStorage(password);
+ }
+
+ public string HashPassword(T user, string password)
+ {
+ // TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089
+ //NOTE: For now this just falls back to the hashing we are currently using
+
+ return HashPassword(password);
+ }
+
+ public PasswordVerificationResult VerifyHashedPassword(T user, string hashedPassword, string providedPassword)
+ {
+ // TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089
+ //NOTE: For now this just falls back to the hashing we are currently using
+
+ return _passwordSecurity.VerifyPassword(providedPassword, hashedPassword)
+ ? PasswordVerificationResult.Success
+ : PasswordVerificationResult.Failed;
+ }
+ }
+}