More migration of back office identity. Blocked by password validator
This commit is contained in:
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// This will return the current back office identity if the IPrincipal is the correct type
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
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<UmbracoBackOfficeIdentity>().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;
|
||||
}
|
||||
}
|
||||
}
|
||||
561
src/Umbraco.Web.BackOffice/Identity/BackOfficeUserManager.cs
Normal file
561
src/Umbraco.Web.BackOffice/Identity/BackOfficeUserManager.cs
Normal file
@@ -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<BackOfficeIdentityUser>
|
||||
{
|
||||
public const string OwinMarkerKey = "Umbraco.Web.Security.Identity.BackOfficeUserManagerMarker";
|
||||
|
||||
public BackOfficeUserManager(
|
||||
IPasswordConfiguration passwordConfiguration,
|
||||
IIpResolver ipResolver,
|
||||
IUserStore<BackOfficeIdentityUser> store,
|
||||
IOptions<IdentityOptions> optionsAccessor,
|
||||
IEnumerable<IUserValidator<BackOfficeIdentityUser>> userValidators,
|
||||
IEnumerable<IPasswordValidator<BackOfficeIdentityUser>> passwordValidators,
|
||||
ILookupNormalizer keyNormalizer,
|
||||
IdentityErrorDescriber errors,
|
||||
ILogger<UserManager<BackOfficeIdentityUser>> logger)
|
||||
: base(passwordConfiguration, ipResolver, store, optionsAccessor, userValidators, passwordValidators, keyNormalizer, errors, null, logger)
|
||||
{
|
||||
InitUserManager(this);
|
||||
}
|
||||
|
||||
#region Static Create methods
|
||||
|
||||
/// <summary>
|
||||
/// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager
|
||||
/// </summary>
|
||||
public static BackOfficeUserManager Create(
|
||||
IUserService userService,
|
||||
IEntityService entityService,
|
||||
IExternalLoginService externalLoginService,
|
||||
IGlobalSettings globalSettings,
|
||||
UmbracoMapper mapper,
|
||||
IPasswordConfiguration passwordConfiguration,
|
||||
IIpResolver ipResolver,
|
||||
IdentityErrorDescriber errors,
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
ILogger<UserManager<BackOfficeIdentityUser>> logger)
|
||||
{
|
||||
var store = new BackOfficeUserStore(userService, entityService, externalLoginService, globalSettings, mapper);
|
||||
|
||||
return Create(
|
||||
passwordConfiguration,
|
||||
ipResolver,
|
||||
store,
|
||||
errors,
|
||||
dataProtectionProvider,
|
||||
logger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a BackOfficeUserManager instance with all default options and a custom BackOfficeUserManager instance
|
||||
/// </summary>
|
||||
public static BackOfficeUserManager Create(
|
||||
IPasswordConfiguration passwordConfiguration,
|
||||
IIpResolver ipResolver,
|
||||
IUserStore<BackOfficeIdentityUser> customUserStore,
|
||||
IdentityErrorDescriber errors,
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
ILogger<UserManager<BackOfficeIdentityUser>> logger)
|
||||
{
|
||||
var options = new IdentityOptions();
|
||||
|
||||
// Configure validation logic for usernames
|
||||
var userValidators = new List<UserValidator<BackOfficeIdentityUser>> { new BackOfficeUserValidator<BackOfficeIdentityUser>() };
|
||||
options.User.RequireUniqueEmail = true;
|
||||
|
||||
// Configure validation logic for passwords
|
||||
var passwordValidators = new List<IPasswordValidator<BackOfficeIdentityUser>> { new PasswordValidator<BackOfficeIdentityUser>() };
|
||||
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<IdentityOptions>(options),
|
||||
userValidators,
|
||||
passwordValidators,
|
||||
new NopLookupNormalizer(),
|
||||
errors,
|
||||
logger);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class BackOfficeUserManager<T> : UserManager<T>
|
||||
where T : BackOfficeIdentityUser
|
||||
{
|
||||
private PasswordGenerator _passwordGenerator;
|
||||
|
||||
public BackOfficeUserManager(
|
||||
IPasswordConfiguration passwordConfiguration,
|
||||
IIpResolver ipResolver,
|
||||
IUserStore<T> store,
|
||||
IOptions<IdentityOptions> optionsAccessor,
|
||||
IEnumerable<IUserValidator<T>> userValidators,
|
||||
IEnumerable<IPasswordValidator<T>> passwordValidators,
|
||||
ILookupNormalizer keyNormalizer,
|
||||
IdentityErrorDescriber errors,
|
||||
IServiceProvider services,
|
||||
ILogger<UserManager<T>> 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;
|
||||
|
||||
/// <summary>
|
||||
/// Developers will need to override this to support custom 2 factor auth
|
||||
/// </summary>
|
||||
public override bool SupportsUserTwoFactor => false;
|
||||
|
||||
// TODO: Support this
|
||||
public override bool SupportsUserPhoneNumber => false;
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the user manager with the correct options
|
||||
/// </summary>
|
||||
protected void InitUserManager(BackOfficeUserManager<T> manager)
|
||||
{
|
||||
// use a custom hasher based on our membership provider
|
||||
PasswordHasher = GetDefaultPasswordHasher(PasswordConfiguration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to validate a user's session
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="sessionId"></param>
|
||||
/// <returns></returns>
|
||||
public virtual async Task<bool> ValidateSessionIdAsync(string userId, string sessionId)
|
||||
{
|
||||
var userSessionStore = Store as IUserSessionStore<T>;
|
||||
//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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This will determine which password hasher to use based on what is defined in config
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual IPasswordHasher<T> GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration)
|
||||
{
|
||||
//we can use the user aware password hasher (which will be the default and preferred way)
|
||||
return new UserAwarePasswordHasher<T>(new PasswordSecurity(passwordConfiguration));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets/sets the default back office user password checker
|
||||
/// </summary>
|
||||
public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; }
|
||||
public IPasswordConfiguration PasswordConfiguration { get; }
|
||||
public IIpResolver IpResolver { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to generate a password for a user based on the current password validator
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public string GeneratePassword()
|
||||
{
|
||||
if (_passwordGenerator == null) _passwordGenerator = new PasswordGenerator(PasswordConfiguration);
|
||||
var password = _passwordGenerator.GeneratePassword();
|
||||
return password;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// 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
|
||||
/// </remarks>
|
||||
public override async Task<bool> 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
|
||||
|
||||
/// <summary>
|
||||
/// Logic used to validate a username and password
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="password"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public override async Task<bool> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is a special method that will reset the password but will raise the Password Changed event instead of the reset event
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <param name="newPassword"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// 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
|
||||
/// </remarks>
|
||||
public async Task<IdentityResult> 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<IdentityResult> ChangePasswordAsync(T user, string currentPassword, string newPassword)
|
||||
{
|
||||
var result = await base.ChangePasswordAsync(user, currentPassword, newPassword);
|
||||
if (result.Succeeded) RaisePasswordChangedEvent(user.Id);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override to determine how to hash the password
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="newPassword"></param>
|
||||
/// <param name="validatePassword"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// This method is called anytime the password needs to be hashed for storage (i.e. including when reset password is used)
|
||||
/// </remarks>
|
||||
protected override async Task<IdentityResult> 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<T>;
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is copied from the underlying .NET base class since they decided to not expose it
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
private async Task UpdateSecurityStampInternal(T user)
|
||||
{
|
||||
if (SupportsUserSecurityStamp == false) return;
|
||||
await GetSecurityStore().SetSecurityStampAsync(user, NewSecurityStamp(), CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is copied from the underlying .NET base class since they decided to not expose it
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private IUserSecurityStampStore<T> GetSecurityStore()
|
||||
{
|
||||
var store = Store as IUserSecurityStampStore<T>;
|
||||
if (store == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>));
|
||||
return store;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is copied from the underlying .NET base class since they decided to not expose it
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static string NewSecurityStamp()
|
||||
{
|
||||
return Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public override async Task<IdentityResult> 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<IdentityResult> ResetAccessFailedCountAsync(T user)
|
||||
{
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
var lockoutStore = (IUserLockoutStore<T>)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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the Microsoft ASP.NET user management method
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns>
|
||||
/// returns a Async Task<IdentityResult />
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// Doesn't set fail attempts back to 0
|
||||
/// </remarks>
|
||||
public override async Task<IdentityResult> AccessFailedAsync(T user)
|
||||
{
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
var lockoutStore = Store as IUserLockoutStore<T>;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Umbraco.Web.BackOffice.Identity
|
||||
{
|
||||
/// <summary>
|
||||
/// The result returned from the IBackOfficeUserPasswordChecker
|
||||
/// </summary>
|
||||
public enum BackOfficeUserPasswordCheckerResult
|
||||
{
|
||||
ValidCredentials,
|
||||
InvalidCredentials,
|
||||
FallbackToDefaultChecker
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Umbraco.Web.BackOffice.Identity
|
||||
{
|
||||
public class BackOfficeUserValidator<T> : UserValidator<T>
|
||||
where T : BackOfficeIdentityUser
|
||||
{
|
||||
public override async Task<IdentityResult> ValidateAsync(UserManager<T> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Umbraco.Web.BackOffice.Identity
|
||||
{
|
||||
/// <summary>
|
||||
/// Used by the BackOfficeUserManager to check the username/password which allows for developers to more easily
|
||||
/// set the logic for this procedure.
|
||||
/// </summary>
|
||||
public interface IBackOfficeUserPasswordChecker
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks a password for a user
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="password"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// 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
|
||||
/// </remarks>
|
||||
Task<BackOfficeUserPasswordCheckerResult> CheckPasswordAsync(BackOfficeIdentityUser user, string password);
|
||||
}
|
||||
}
|
||||
132
src/Umbraco.Web.BackOffice/Identity/IdentityAuditEventArgs.cs
Normal file
132
src/Umbraco.Web.BackOffice/Identity/IdentityAuditEventArgs.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
|
||||
namespace Umbraco.Web.BackOffice.Identity
|
||||
{
|
||||
/// <summary>
|
||||
/// This class is used by events raised from the BackofficeUserManager
|
||||
/// </summary>
|
||||
public class IdentityAuditEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The action that got triggered from the audit event
|
||||
/// </summary>
|
||||
public AuditEvent Action { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current date/time in UTC format
|
||||
/// </summary>
|
||||
public DateTime DateTimeUtc { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The source IP address of the user performing the action
|
||||
/// </summary>
|
||||
public string IpAddress { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user affected by the event raised
|
||||
/// </summary>
|
||||
public int AffectedUser { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// If a user is performing an action on a different user, then this will be set. Otherwise it will be -1
|
||||
/// </summary>
|
||||
public int PerformingUser { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// An optional comment about the action being logged
|
||||
/// </summary>
|
||||
public string Comment { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// This property is always empty except in the LoginFailed event for an unknown user trying to login
|
||||
/// </summary>
|
||||
public string Username { get; private set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="ipAddress"></param>
|
||||
/// <param name="comment"></param>
|
||||
/// <param name="performingUser"></param>
|
||||
/// <param name="affectedUser"></param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance without a performing or affected user (the id will be set to -1)
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="ipAddress"></param>
|
||||
/// <param name="username"></param>
|
||||
/// <param name="comment"></param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current logged in backoffice user's Id logging if there is one
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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
|
||||
}
|
||||
}
|
||||
14
src/Umbraco.Web.BackOffice/Identity/NopLookupNormalizer.cs
Normal file
14
src/Umbraco.Web.BackOffice/Identity/NopLookupNormalizer.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Umbraco.Web.BackOffice.Identity
|
||||
{
|
||||
/// <summary>
|
||||
/// No-op lookup normalizer to maintain compatibility with ASP.NET Identity 2
|
||||
/// </summary>
|
||||
public class NopLookupNormalizer : ILookupNormalizer
|
||||
{
|
||||
public string NormalizeName(string name) => name;
|
||||
|
||||
public string NormalizeEmail(string email) => email;
|
||||
}
|
||||
}
|
||||
223
src/Umbraco.Web.BackOffice/Identity/PasswordSecurity.cs
Normal file
223
src/Umbraco.Web.BackOffice/Identity/PasswordSecurity.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles password hashing and formatting
|
||||
/// </summary>
|
||||
public class PasswordSecurity
|
||||
{
|
||||
public IPasswordConfiguration PasswordConfiguration { get; }
|
||||
public PasswordGenerator _generator;
|
||||
public ConfiguredPasswordValidator _validator;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="passwordConfiguration"></param>
|
||||
public PasswordSecurity(IPasswordConfiguration passwordConfiguration)
|
||||
{
|
||||
PasswordConfiguration = passwordConfiguration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the password passes validation rules
|
||||
/// </summary>
|
||||
/// <param name="password"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<Attempt<IEnumerable<string>>> IsValidPasswordAsync(string password)
|
||||
{
|
||||
if (_validator == null)
|
||||
_validator = new ConfiguredPasswordValidator(PasswordConfiguration);
|
||||
var result = await _validator.ValidateAsync(password);
|
||||
if (result.Succeeded)
|
||||
return Attempt<IEnumerable<string>>.Succeed();
|
||||
|
||||
return Attempt<IEnumerable<string>>.Fail(result.Errors);
|
||||
}
|
||||
|
||||
public string GeneratePassword()
|
||||
{
|
||||
if (_generator == null)
|
||||
_generator = new PasswordGenerator(PasswordConfiguration);
|
||||
return _generator.GeneratePassword();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a hashed password value used to store in a data store
|
||||
/// </summary>
|
||||
/// <param name="password"></param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="hashedPassword"></param>
|
||||
/// <param name="salt"></param>
|
||||
/// <returns></returns>
|
||||
public string FormatPasswordForStorage(string hashedPassword, string salt)
|
||||
{
|
||||
return salt + hashedPassword;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hashes a password with a given salt
|
||||
/// </summary>
|
||||
/// <param name="pass"></param>
|
||||
/// <param name="salt"></param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies if the password matches the expected hash+salt of the stored password string
|
||||
/// </summary>
|
||||
/// <param name="password">The password.</param>
|
||||
/// <param name="dbPassword">The value of the password stored in a data store.</param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new password hash and a new salt
|
||||
/// </summary>
|
||||
/// <param name="newPassword"></param>
|
||||
/// <param name="salt"></param>
|
||||
/// <returns></returns>
|
||||
public string HashNewPassword(string newPassword, out string salt)
|
||||
{
|
||||
salt = GenerateSalt();
|
||||
return HashPassword(newPassword, salt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses out the hashed password and the salt from the stored password string value
|
||||
/// </summary>
|
||||
/// <param name="storedString"></param>
|
||||
/// <param name="salt">returns the salt</param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the hash algorithm to use
|
||||
/// </summary>
|
||||
/// <param name="password"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes the password.
|
||||
/// </summary>
|
||||
/// <param name="password">The password.</param>
|
||||
/// <returns>The encoded password.</returns>
|
||||
private string LegacyEncodePassword(string password)
|
||||
{
|
||||
var hashAlgorith = GetHashAlgorithm(password);
|
||||
var encodedPassword = Convert.ToBase64String(hashAlgorith.ComputeHash(Encoding.Unicode.GetBytes(password)));
|
||||
return encodedPassword;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
232
src/Umbraco.Web.BackOffice/Identity/UmbracoBackOfficeIdentity.cs
Normal file
232
src/Umbraco.Web.BackOffice/Identity/UmbracoBackOfficeIdentity.cs
Normal file
@@ -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
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// A custom user identity for the Umbraco backoffice
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
[Serializable]
|
||||
public class UmbracoBackOfficeIdentity : ClaimsIdentity
|
||||
{
|
||||
public static UmbracoBackOfficeIdentity FromClaimsIdentity(ClaimsIdentity identity)
|
||||
{
|
||||
return new UmbracoBackOfficeIdentity(identity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new UmbracoBackOfficeIdentity
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="username"></param>
|
||||
/// <param name="realName"></param>
|
||||
/// <param name="startContentNodes"></param>
|
||||
/// <param name="startMediaNodes"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <param name="sessionId"></param>
|
||||
/// <param name="securityStamp"></param>
|
||||
/// <param name="allowedApps"></param>
|
||||
/// <param name="roles"></param>
|
||||
public UmbracoBackOfficeIdentity(int userId, string username, string realName,
|
||||
IEnumerable<int> startContentNodes, IEnumerable<int> startMediaNodes, string culture,
|
||||
string sessionId, string securityStamp, IEnumerable<string> allowedApps, IEnumerable<string> roles)
|
||||
: base(Enumerable.Empty<Claim>(), 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new UmbracoBackOfficeIdentity
|
||||
/// </summary>
|
||||
/// <param name="childIdentity">
|
||||
/// The original identity created by the ClaimsIdentityFactory
|
||||
/// </param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="username"></param>
|
||||
/// <param name="realName"></param>
|
||||
/// <param name="startContentNodes"></param>
|
||||
/// <param name="startMediaNodes"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <param name="sessionId"></param>
|
||||
/// <param name="securityStamp"></param>
|
||||
/// <param name="allowedApps"></param>
|
||||
/// <param name="roles"></param>
|
||||
public UmbracoBackOfficeIdentity(ClaimsIdentity childIdentity,
|
||||
int userId, string username, string realName,
|
||||
IEnumerable<int> startContentNodes, IEnumerable<int> startMediaNodes, string culture,
|
||||
string sessionId, string securityStamp, IEnumerable<string> allowedApps, IEnumerable<string> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a back office identity based on an existing claims identity
|
||||
/// </summary>
|
||||
/// <param name="identity"></param>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the required claim types for a back office identity
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This does not include the role claim type or allowed apps type since that is a collection and in theory could be empty
|
||||
/// </remarks>
|
||||
public static IEnumerable<string> RequiredBackOfficeIdentityClaimTypes => new[]
|
||||
{
|
||||
ClaimTypes.NameIdentifier, //id
|
||||
ClaimTypes.Name, //username
|
||||
ClaimTypes.GivenName,
|
||||
Constants.Security.StartContentNodeIdClaimType,
|
||||
Constants.Security.StartMediaNodeIdClaimType,
|
||||
ClaimTypes.Locality,
|
||||
Constants.Security.SessionIdClaimType,
|
||||
Constants.Web.SecurityStampClaimType
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Adds claims based on the ctor data
|
||||
/// </summary>
|
||||
private void AddRequiredClaims(int userId, string username, string realName,
|
||||
IEnumerable<int> startContentNodes, IEnumerable<int> startMediaNodes, string culture,
|
||||
string sessionId, string securityStamp, IEnumerable<string> allowedApps, IEnumerable<string> 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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Gets the type of authenticated identity.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The type of authenticated identity. This property always returns "UmbracoBackOffice".
|
||||
/// </returns>
|
||||
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();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Umbraco.Web.BackOffice.Identity
|
||||
{
|
||||
public class UserAwarePasswordHasher<T> : IPasswordHasher<T>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user