2020-12-04 01:38:36 +11:00
using System ;
using System.Collections.Generic ;
2020-12-09 18:36:39 +00:00
using System.Linq ;
2020-12-04 01:38:36 +11:00
using System.Threading ;
using System.Threading.Tasks ;
using Microsoft.AspNetCore.Identity ;
using Microsoft.Extensions.Logging ;
using Microsoft.Extensions.Options ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.Configuration ;
using Umbraco.Cms.Core.Net ;
2020-12-04 01:38:36 +11:00
2021-02-15 12:01:12 +01:00
namespace Umbraco.Cms.Core.Security
2020-12-04 01:38:36 +11:00
{
/// <summary>
/// Abstract class for Umbraco User Managers for back office users or front-end members
/// </summary>
2020-12-04 02:21:21 +11:00
/// <typeparam name="TUser">The type of user</typeparam>
/// /// <typeparam name="TPasswordConfig">The type password config</typeparam>
public abstract class UmbracoUserManager < TUser , TPasswordConfig > : UserManager < TUser >
where TUser : UmbracoIdentityUser
2020-12-04 12:44:27 +11:00
where TPasswordConfig : class , IPasswordConfiguration , new ( )
2020-12-04 01:38:36 +11:00
{
private PasswordGenerator _passwordGenerator ;
/// <summary>
2020-12-04 02:21:21 +11:00
/// Initializes a new instance of the <see cref="UmbracoUserManager{T, TPasswordConfig}"/> class.
2020-12-04 01:38:36 +11:00
/// </summary>
public UmbracoUserManager (
IIpResolver ipResolver ,
2020-12-04 02:21:21 +11:00
IUserStore < TUser > store ,
IOptions < IdentityOptions > optionsAccessor ,
IPasswordHasher < TUser > passwordHasher ,
IEnumerable < IUserValidator < TUser > > userValidators ,
IEnumerable < IPasswordValidator < TUser > > passwordValidators ,
IdentityErrorDescriber errors ,
2020-12-04 01:38:36 +11:00
IServiceProvider services ,
2020-12-04 02:21:21 +11:00
ILogger < UserManager < TUser > > logger ,
2021-09-27 09:58:44 +02:00
IOptionsMonitor < TPasswordConfig > passwordConfiguration )
2021-02-09 13:45:08 +00:00
: base ( store , optionsAccessor , passwordHasher , userValidators , passwordValidators , new NoopLookupNormalizer ( ) , errors , services , logger )
2020-12-04 01:38:36 +11:00
{
IpResolver = ipResolver ? ? throw new ArgumentNullException ( nameof ( ipResolver ) ) ;
2021-09-27 09:58:44 +02:00
PasswordConfiguration = passwordConfiguration . CurrentValue ? ? throw new ArgumentNullException ( nameof ( passwordConfiguration ) ) ;
passwordConfiguration . OnChange ( x = > PasswordConfiguration = x ) ;
2020-12-04 01:38:36 +11:00
}
2020-12-04 12:52:25 +11:00
/// <inheritdoc />
public override bool SupportsUserClaim = > false ; // We don't support an IUserClaimStore and don't need to (at least currently)
2020-12-04 01:38:36 +11:00
2020-12-04 12:52:25 +11:00
/// <inheritdoc />
public override bool SupportsQueryableUsers = > false ; // It would be nice to support this but we don't need to currently and that would require IQueryable support for our user service/repository
2020-12-04 01:38:36 +11:00
/// <summary>
/// Developers will need to override this to support custom 2 factor auth
/// </summary>
2020-12-04 12:52:25 +11:00
/// <inheritdoc />
2020-12-04 01:38:36 +11:00
public override bool SupportsUserTwoFactor = > false ;
2020-12-04 12:52:25 +11:00
/// <inheritdoc />
public override bool SupportsUserPhoneNumber = > false ; // We haven't needed to support this yet, though might be necessary for 2FA
/// <summary>
/// Gets the password configuration
/// </summary>
2021-09-27 09:58:44 +02:00
public IPasswordConfiguration PasswordConfiguration { get ; private set ; }
2020-12-04 12:52:25 +11:00
/// <summary>
/// Gets the IP resolver
/// </summary>
public IIpResolver IpResolver { get ; }
2020-12-04 01:38:36 +11:00
/// <summary>
/// Used to validate a user's session
/// </summary>
/// <param name="userId">The user id</param>
2021-01-11 14:24:49 +00:00
/// <param name="sessionId">The session id</param>
/// <returns>True if the session is valid, else false</returns>
2020-12-04 01:38:36 +11:00
public virtual async Task < bool > ValidateSessionIdAsync ( string userId , string sessionId )
{
// if this is not set, for backwards compat (which would be super rare), we'll just approve it
2020-12-04 02:21:21 +11:00
// TODO: This should be removed after members supports this
2021-05-11 11:29:40 +02:00
if ( Store is not IUserSessionStore < TUser > userSessionStore )
2020-12-04 01:38:36 +11:00
{
return true ;
}
return await userSessionStore . ValidateSessionIdAsync ( userId , sessionId ) ;
}
/// <summary>
/// Helper method to generate a password for a user based on the current password validator
/// </summary>
/// <returns>The generated password</returns>
public string GeneratePassword ( )
{
2021-01-29 16:43:50 +00:00
_passwordGenerator ? ? = new PasswordGenerator ( PasswordConfiguration ) ;
2020-12-04 01:38:36 +11:00
2021-01-29 16:43:50 +00:00
string password = _passwordGenerator . GeneratePassword ( ) ;
2020-12-04 01:38:36 +11:00
return password ;
}
2021-09-27 09:58:44 +02:00
2020-12-09 18:36:39 +00:00
/// <summary>
/// Used to validate the password without an identity user
/// Validation code is based on the default ValidatePasswordAsync code
/// Should return <see cref="IdentityResult.Success"/> if validation is successful
/// </summary>
/// <param name="password">The password.</param>
/// <returns>A <see cref="IdentityResult"/> representing whether validation was successful.</returns>
public async Task < IdentityResult > ValidatePasswordAsync ( string password )
{
var errors = new List < IdentityError > ( ) ;
var isValid = true ;
foreach ( IPasswordValidator < TUser > v in PasswordValidators )
{
IdentityResult result = await v . ValidateAsync ( this , null , password ) ;
if ( ! result . Succeeded )
{
if ( result . Errors . Any ( ) )
{
errors . AddRange ( result . Errors ) ;
}
isValid = false ;
}
}
if ( ! isValid )
{
Logger . LogWarning ( 14 , "Password validation failed: {errors}." , string . Join ( ";" , errors . Select ( e = > e . Code ) ) ) ;
return IdentityResult . Failed ( errors . ToArray ( ) ) ;
}
return IdentityResult . Success ;
}
2020-12-04 01:38:36 +11:00
/// <inheritdoc />
2020-12-04 02:21:21 +11:00
public override async Task < bool > CheckPasswordAsync ( TUser user , string password )
2020-12-04 01:38:36 +11:00
{
// 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">The userId</param>
/// <param name="token">The reset password token</param>
/// <param name="newPassword">The new password to set it to</param>
/// <returns>The <see cref="IdentityResult"/></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>
2020-12-04 12:44:27 +11:00
public virtual async Task < IdentityResult > ChangePasswordWithResetAsync ( string userId , string token , string newPassword )
2020-12-04 01:38:36 +11:00
{
2020-12-04 12:44:27 +11:00
TUser user = await FindByIdAsync ( userId ) ;
2020-12-04 01:38:36 +11:00
if ( user = = null )
{
throw new InvalidOperationException ( "Could not find user" ) ;
}
2020-12-04 12:44:27 +11:00
IdentityResult result = await ResetPasswordAsync ( user , token , newPassword ) ;
2020-12-04 01:38:36 +11:00
return result ;
}
/// <inheritdoc/>
2020-12-04 02:21:21 +11:00
public override async Task < IdentityResult > SetLockoutEndDateAsync ( TUser user , DateTimeOffset ? lockoutEnd )
2020-12-04 01:38:36 +11:00
{
if ( user = = null )
{
throw new ArgumentNullException ( nameof ( user ) ) ;
}
IdentityResult result = await base . SetLockoutEndDateAsync ( user , lockoutEnd ) ;
// The way we unlock is by setting the lockoutEnd date to the current datetime
if ( ! result . Succeeded | | lockoutEnd < DateTimeOffset . UtcNow )
{
// Resets the login attempt fails back to 0 when unlock is clicked
await ResetAccessFailedCountAsync ( user ) ;
}
return result ;
}
/// <inheritdoc/>
2020-12-04 02:21:21 +11:00
public override async Task < IdentityResult > ResetAccessFailedCountAsync ( TUser user )
2020-12-04 01:38:36 +11:00
{
if ( user = = null )
{
throw new ArgumentNullException ( nameof ( user ) ) ;
}
2020-12-04 02:21:21 +11:00
var lockoutStore = ( IUserLockoutStore < TUser > ) Store ;
2020-12-04 01:38:36 +11:00
var accessFailedCount = await GetAccessFailedCountAsync ( user ) ;
if ( accessFailedCount = = 0 )
{
return IdentityResult . Success ;
}
await lockoutStore . ResetAccessFailedCountAsync ( user , CancellationToken . None ) ;
return await UpdateAsync ( user ) ;
}
/// <summary>
/// Overrides the Microsoft ASP.NET user management method
/// </summary>
/// <inheritdoc/>
2020-12-04 02:21:21 +11:00
public override async Task < IdentityResult > AccessFailedAsync ( TUser user )
2020-12-04 01:38:36 +11:00
{
if ( user = = null )
{
throw new ArgumentNullException ( nameof ( user ) ) ;
}
2021-05-11 11:29:40 +02:00
if ( Store is not IUserLockoutStore < TUser > lockoutStore )
2020-12-04 01:38:36 +11:00
{
throw new NotSupportedException ( "The current user store does not implement " + typeof ( IUserLockoutStore < > ) ) ;
}
var count = await lockoutStore . IncrementAccessFailedCountAsync ( user , CancellationToken . None ) ;
if ( count > = Options . Lockout . MaxFailedAccessAttempts )
{
await lockoutStore . SetLockoutEndDateAsync ( user , DateTimeOffset . UtcNow . Add ( Options . Lockout . DefaultLockoutTimeSpan ) , CancellationToken . None ) ;
// NOTE: in normal aspnet identity this would do set the number of failed attempts back to 0
// here we are persisting the value for the back office
}
IdentityResult result = await UpdateAsync ( user ) ;
return result ;
}
2021-05-11 11:29:40 +02:00
/// <inheritdoc/>
public async Task < bool > ValidateCredentialsAsync ( string username , string password )
{
TUser user = await FindByNameAsync ( username ) ;
if ( user = = null )
{
return false ;
}
if ( Store is not IUserPasswordStore < TUser > userPasswordStore )
{
throw new NotSupportedException ( "The current user store does not implement " + typeof ( IUserPasswordStore < > ) ) ;
}
var hash = await userPasswordStore . GetPasswordHashAsync ( user , new CancellationToken ( ) ) ;
return await VerifyPasswordAsync ( userPasswordStore , user , password ) = = PasswordVerificationResult . Success ;
}
2020-12-04 01:38:36 +11:00
}
}