2020-12-03 19:36:38 +11:00
using System ;
2020-05-14 22:21:19 +01:00
using System.Collections.Generic ;
2020-05-21 16:33:24 +10:00
using System.Security.Principal ;
2020-05-14 22:21:19 +01:00
using System.Threading.Tasks ;
2020-12-01 17:24:23 +11:00
using Microsoft.AspNetCore.Http ;
2020-05-14 22:21:19 +01:00
using Microsoft.AspNetCore.Identity ;
using Microsoft.Extensions.Logging ;
using Microsoft.Extensions.Options ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core ;
using Umbraco.Cms.Core.Configuration.Models ;
using Umbraco.Cms.Core.Models.ContentEditing ;
using Umbraco.Cms.Core.Models.Membership ;
using Umbraco.Cms.Core.Net ;
using Umbraco.Cms.Core.Security ;
2020-12-01 17:24:23 +11:00
using Umbraco.Core ;
2020-05-14 22:21:19 +01:00
using Umbraco.Core.Security ;
2020-05-21 16:33:24 +10:00
using Umbraco.Extensions ;
2020-05-14 22:21:19 +01:00
2020-12-01 17:24:23 +11:00
namespace Umbraco.Web.Common.Security
2020-05-14 22:21:19 +01:00
{
2020-12-04 02:21:21 +11:00
public class BackOfficeUserManager : UmbracoUserManager < BackOfficeIdentityUser , UserPasswordConfigurationSettings > , IBackOfficeUserManager
2020-05-14 22:21:19 +01:00
{
2020-12-04 01:38:36 +11:00
private readonly IHttpContextAccessor _httpContextAccessor ;
2020-05-21 15:43:33 +10:00
public BackOfficeUserManager (
IIpResolver ipResolver ,
IUserStore < BackOfficeIdentityUser > store ,
IOptions < BackOfficeIdentityOptions > optionsAccessor ,
IPasswordHasher < BackOfficeIdentityUser > passwordHasher ,
IEnumerable < IUserValidator < BackOfficeIdentityUser > > userValidators ,
IEnumerable < IPasswordValidator < BackOfficeIdentityUser > > passwordValidators ,
BackOfficeLookupNormalizer keyNormalizer ,
BackOfficeIdentityErrorDescriber errors ,
IServiceProvider services ,
2020-12-01 17:24:23 +11:00
IHttpContextAccessor httpContextAccessor ,
2020-06-22 10:08:08 +02:00
ILogger < UserManager < BackOfficeIdentityUser > > logger ,
2020-08-20 22:18:50 +01:00
IOptions < UserPasswordConfigurationSettings > passwordConfiguration )
2020-12-04 01:38:36 +11:00
: base ( ipResolver , store , optionsAccessor , passwordHasher , userValidators , passwordValidators , keyNormalizer , errors , services , logger , passwordConfiguration )
2020-05-14 22:21:19 +01:00
{
2020-12-01 17:24:23 +11:00
_httpContextAccessor = httpContextAccessor ;
2020-05-14 22:21:19 +01:00
}
2020-12-04 02:21:21 +11:00
/// <summary>
/// Gets or sets the default back office user password checker
/// </summary>
public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get ; set ; } // TODO: This isn't a good way to set this, it needs to be injected
2020-12-03 20:30:35 +11:00
/// <inheritdoc />
2020-05-14 22:21:19 +01:00
/// <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>
2020-12-04 01:38:36 +11:00
public override async Task < bool > CheckPasswordAsync ( BackOfficeIdentityUser user , string password )
2020-05-14 22:21:19 +01:00
{
if ( BackOfficeUserPasswordChecker ! = null )
{
2020-12-03 20:30:35 +11:00
BackOfficeUserPasswordCheckerResult result = await BackOfficeUserPasswordChecker . CheckPasswordAsync ( user , password ) ;
2020-05-14 22:21:19 +01:00
if ( user . HasIdentity = = false )
{
return false ;
}
2020-12-03 20:30:35 +11:00
// if the result indicates to not fallback to the default, then return true if the credentials are valid
2020-05-14 22:21:19 +01:00
if ( result ! = BackOfficeUserPasswordCheckerResult . FallbackToDefaultChecker )
{
return result = = BackOfficeUserPasswordCheckerResult . ValidCredentials ;
}
}
2020-12-03 20:30:35 +11:00
// use the default behavior
2020-05-14 22:21:19 +01:00
return await base . CheckPasswordAsync ( user , password ) ;
}
2020-06-22 10:08:08 +02:00
2020-05-14 22:21:19 +01:00
/// <summary>
2020-12-04 01:38:36 +11:00
/// 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
2020-05-14 22:21:19 +01:00
/// </summary>
2020-12-04 01:38:36 +11:00
/// <param name="user">The user</param>
/// <returns>True if the user is locked out, else false</returns>
2020-05-14 22:21:19 +01:00
/// <remarks>
2020-12-04 01:38:36 +11:00
/// 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
2020-05-14 22:21:19 +01:00
/// </remarks>
2020-12-04 01:38:36 +11:00
public override async Task < bool > IsLockedOutAsync ( BackOfficeIdentityUser user )
2020-05-14 22:21:19 +01:00
{
2020-12-03 20:30:35 +11:00
if ( user = = null )
{
2020-12-04 01:38:36 +11:00
throw new ArgumentNullException ( nameof ( user ) ) ;
2020-12-03 20:30:35 +11:00
}
2020-05-14 22:21:19 +01:00
2020-12-04 01:38:36 +11:00
if ( user . IsApproved = = false )
2020-12-01 17:24:23 +11:00
{
2020-12-04 01:38:36 +11:00
return true ;
2020-12-01 17:24:23 +11:00
}
2020-12-03 20:30:35 +11:00
2020-12-04 01:38:36 +11:00
return await base . IsLockedOutAsync ( user ) ;
2020-05-14 22:21:19 +01:00
}
2020-06-22 10:08:08 +02:00
2020-12-04 01:38:36 +11:00
public override async Task < IdentityResult > AccessFailedAsync ( BackOfficeIdentityUser user )
2020-05-14 22:21:19 +01:00
{
2020-12-04 01:38:36 +11:00
IdentityResult result = await base . AccessFailedAsync ( user ) ;
2020-05-14 22:21:19 +01:00
2020-12-04 01:38:36 +11:00
// Slightly confusing: this will return a Success if we successfully update the AccessFailed count
if ( result . Succeeded )
2020-12-03 20:30:35 +11:00
{
2020-12-04 01:38:36 +11:00
RaiseLoginFailedEvent ( _httpContextAccessor . HttpContext ? . User , user . Id ) ;
2020-12-03 20:30:35 +11:00
}
2020-05-14 22:21:19 +01:00
2020-12-04 01:38:36 +11:00
return result ;
2020-05-14 22:21:19 +01:00
}
2020-12-04 12:44:27 +11:00
public override async Task < IdentityResult > ChangePasswordWithResetAsync ( string userId , string token , string newPassword )
2020-05-14 22:21:19 +01:00
{
2020-12-04 01:38:36 +11:00
IdentityResult result = await base . ChangePasswordWithResetAsync ( userId , token , newPassword ) ;
if ( result . Succeeded )
2020-12-03 20:30:35 +11:00
{
2020-12-04 01:38:36 +11:00
RaisePasswordChangedEvent ( _httpContextAccessor . HttpContext ? . User , userId ) ;
2020-12-03 20:30:35 +11:00
}
2020-12-04 01:38:36 +11:00
return result ;
2020-05-14 22:21:19 +01:00
}
2020-12-04 01:38:36 +11:00
public override async Task < IdentityResult > ChangePasswordAsync ( BackOfficeIdentityUser user , string currentPassword , string newPassword )
2020-05-14 22:21:19 +01:00
{
2020-12-04 01:38:36 +11:00
IdentityResult result = await base . ChangePasswordAsync ( user , currentPassword , newPassword ) ;
if ( result . Succeeded )
2020-12-03 20:30:35 +11:00
{
2020-12-04 01:38:36 +11:00
RaisePasswordChangedEvent ( _httpContextAccessor . HttpContext ? . User , user . Id ) ;
2020-12-03 20:30:35 +11:00
}
2020-12-04 01:38:36 +11:00
return result ;
2020-05-14 22:21:19 +01:00
}
2020-12-03 20:30:35 +11:00
/// <inheritdoc/>
2020-12-04 01:38:36 +11:00
public override async Task < IdentityResult > SetLockoutEndDateAsync ( BackOfficeIdentityUser user , DateTimeOffset ? lockoutEnd )
2020-05-14 22:21:19 +01:00
{
2020-12-03 20:30:35 +11:00
if ( user = = null )
{
throw new ArgumentNullException ( nameof ( user ) ) ;
}
2020-05-14 22:21:19 +01:00
2020-12-03 20:30:35 +11:00
IdentityResult result = await base . SetLockoutEndDateAsync ( user , lockoutEnd ) ;
2020-05-14 22:21:19 +01:00
// The way we unlock is by setting the lockoutEnd date to the current datetime
if ( result . Succeeded & & lockoutEnd > = DateTimeOffset . UtcNow )
{
2020-12-04 01:38:36 +11:00
RaiseAccountLockedEvent ( _httpContextAccessor . HttpContext ? . User , user . Id ) ;
2020-05-14 22:21:19 +01:00
}
else
{
2020-12-01 17:24:23 +11:00
RaiseAccountUnlockedEvent ( _httpContextAccessor . HttpContext ? . User , user . Id ) ;
2020-12-03 20:30:35 +11:00
// Resets the login attempt fails back to 0 when unlock is clicked
2020-05-14 22:21:19 +01:00
await ResetAccessFailedCountAsync ( user ) ;
}
return result ;
}
2020-12-03 20:30:35 +11:00
/// <inheritdoc/>
2020-12-04 01:38:36 +11:00
public override async Task < IdentityResult > ResetAccessFailedCountAsync ( BackOfficeIdentityUser user )
2020-05-14 22:21:19 +01:00
{
2020-12-04 01:38:36 +11:00
IdentityResult result = await base . ResetAccessFailedCountAsync ( user ) ;
2020-12-03 20:30:35 +11:00
// raise the event now that it's reset
2020-12-01 17:24:23 +11:00
RaiseResetAccessFailedCountEvent ( _httpContextAccessor . HttpContext ? . User , user . Id ) ;
2020-05-14 22:21:19 +01:00
return result ;
}
2020-12-04 12:44:27 +11:00
private string GetCurrentUserId ( IPrincipal currentUser )
2020-05-14 22:21:19 +01:00
{
2020-12-03 20:30:35 +11:00
UmbracoBackOfficeIdentity umbIdentity = currentUser ? . GetUmbracoIdentity ( ) ;
2021-02-09 10:22:42 +01:00
var currentUserId = umbIdentity ? . GetUserId < string > ( ) ? ? Cms . Core . Constants . Security . SuperUserIdAsString ;
2020-10-23 14:18:53 +11:00
return currentUserId ;
}
2020-12-03 20:30:35 +11:00
2020-12-04 12:44:27 +11:00
private IdentityAuditEventArgs CreateArgs ( AuditEvent auditEvent , IPrincipal currentUser , string affectedUserId , string affectedUsername )
2020-10-23 14:18:53 +11:00
{
var currentUserId = GetCurrentUserId ( currentUser ) ;
2020-05-21 16:33:24 +10:00
var ip = IpResolver . GetCurrentRequestIpAddress ( ) ;
return new IdentityAuditEventArgs ( auditEvent , ip , currentUserId , string . Empty , affectedUserId , affectedUsername ) ;
2020-05-27 18:27:49 +10:00
}
2020-12-03 20:30:35 +11:00
2020-12-04 12:44:27 +11:00
private IdentityAuditEventArgs CreateArgs ( AuditEvent auditEvent , BackOfficeIdentityUser currentUser , string affectedUserId , string affectedUsername )
2020-05-27 18:27:49 +10:00
{
var currentUserId = currentUser . Id ;
var ip = IpResolver . GetCurrentRequestIpAddress ( ) ;
return new IdentityAuditEventArgs ( auditEvent , ip , currentUserId , string . Empty , affectedUserId , affectedUsername ) ;
}
2020-05-14 22:21:19 +01:00
2020-12-01 17:24:23 +11:00
// TODO: Review where these are raised and see if they can be simplified and either done in the this usermanager or the signin manager,
// lastly we'll resort to the authentication controller but we should try to remove all instances of that occuring
2020-12-04 12:44:27 +11:00
public void RaiseAccountLockedEvent ( IPrincipal currentUser , string userId ) = > OnAccountLocked ( CreateArgs ( AuditEvent . AccountLocked , currentUser , userId , string . Empty ) ) ;
2020-05-14 22:21:19 +01:00
2020-12-04 12:44:27 +11:00
public void RaiseAccountUnlockedEvent ( IPrincipal currentUser , string userId ) = > OnAccountUnlocked ( CreateArgs ( AuditEvent . AccountUnlocked , currentUser , userId , string . Empty ) ) ;
2020-05-14 22:21:19 +01:00
2020-12-04 12:44:27 +11:00
public void RaiseForgotPasswordRequestedEvent ( IPrincipal currentUser , string userId ) = > OnForgotPasswordRequested ( CreateArgs ( AuditEvent . ForgotPasswordRequested , currentUser , userId , string . Empty ) ) ;
2020-05-14 22:21:19 +01:00
2020-12-04 12:44:27 +11:00
public void RaiseForgotPasswordChangedSuccessEvent ( IPrincipal currentUser , string userId ) = > OnForgotPasswordChangedSuccess ( CreateArgs ( AuditEvent . ForgotPasswordChangedSuccess , currentUser , userId , string . Empty ) ) ;
2020-05-14 22:21:19 +01:00
2020-12-04 12:44:27 +11:00
public void RaiseLoginFailedEvent ( IPrincipal currentUser , string userId ) = > OnLoginFailed ( CreateArgs ( AuditEvent . LoginFailed , currentUser , userId , string . Empty ) ) ;
2020-05-14 22:21:19 +01:00
2020-12-04 12:44:27 +11:00
public void RaiseLoginRequiresVerificationEvent ( IPrincipal currentUser , string userId ) = > OnLoginRequiresVerification ( CreateArgs ( AuditEvent . LoginRequiresVerification , currentUser , userId , string . Empty ) ) ;
2020-10-23 14:18:53 +11:00
2020-12-04 12:44:27 +11:00
public void RaiseLoginSuccessEvent ( IPrincipal currentUser , string userId ) = > OnLoginSuccess ( CreateArgs ( AuditEvent . LoginSucces , currentUser , userId , string . Empty ) ) ;
2020-10-23 14:18:53 +11:00
2020-12-04 12:44:27 +11:00
public SignOutAuditEventArgs RaiseLogoutSuccessEvent ( IPrincipal currentUser , string userId )
2020-10-23 10:10:02 +11:00
{
2020-10-23 14:18:53 +11:00
var currentUserId = GetCurrentUserId ( currentUser ) ;
var args = new SignOutAuditEventArgs ( AuditEvent . LogoutSuccess , IpResolver . GetCurrentRequestIpAddress ( ) , performingUser : currentUserId , affectedUser : userId ) ;
2020-10-23 10:10:02 +11:00
OnLogoutSuccess ( args ) ;
return args ;
}
2020-12-01 17:24:23 +11:00
2020-12-04 12:44:27 +11:00
public void RaisePasswordChangedEvent ( IPrincipal currentUser , string userId ) = > OnPasswordChanged ( CreateArgs ( AuditEvent . LogoutSuccess , currentUser , userId , string . Empty ) ) ;
2020-05-21 16:33:24 +10:00
2020-12-04 12:44:27 +11:00
public void RaiseResetAccessFailedCountEvent ( IPrincipal currentUser , string userId ) = > OnResetAccessFailedCount ( CreateArgs ( AuditEvent . ResetAccessFailedCount , currentUser , userId , string . Empty ) ) ;
2020-05-14 22:21:19 +01:00
2020-10-23 14:18:53 +11:00
public UserInviteEventArgs RaiseSendingUserInvite ( IPrincipal currentUser , UserInvite invite , IUser createdUser )
{
var currentUserId = GetCurrentUserId ( currentUser ) ;
var ip = IpResolver . GetCurrentRequestIpAddress ( ) ;
var args = new UserInviteEventArgs ( ip , currentUserId , invite , createdUser ) ;
OnSendingUserInvite ( args ) ;
return args ;
}
public bool HasSendingUserInviteEventHandler = > SendingUserInvite ! = null ;
2020-12-01 18:14:37 +11:00
// TODO: These static events are problematic. Moving forward we don't want static events at all but we cannot
// have non-static events here because the user manager is a Scoped instance not a singleton
// so we'll have to deal with this a diff way i.e. refactoring how events are done entirely
public static event EventHandler < IdentityAuditEventArgs > AccountLocked ;
public static event EventHandler < IdentityAuditEventArgs > AccountUnlocked ;
public static event EventHandler < IdentityAuditEventArgs > ForgotPasswordRequested ;
public static event EventHandler < IdentityAuditEventArgs > ForgotPasswordChangedSuccess ;
public static event EventHandler < IdentityAuditEventArgs > LoginFailed ;
public static event EventHandler < IdentityAuditEventArgs > LoginRequiresVerification ;
public static event EventHandler < IdentityAuditEventArgs > LoginSuccess ;
public static event EventHandler < SignOutAuditEventArgs > LogoutSuccess ;
public static event EventHandler < IdentityAuditEventArgs > PasswordChanged ;
public static event EventHandler < IdentityAuditEventArgs > PasswordReset ;
public static event EventHandler < IdentityAuditEventArgs > ResetAccessFailedCount ;
2020-05-14 22:21:19 +01:00
2020-10-23 10:10:02 +11:00
/// <summary>
/// Raised when a user is invited
/// </summary>
2020-10-23 14:18:53 +11:00
public static event EventHandler < UserInviteEventArgs > SendingUserInvite ; // this event really has nothing to do with the user manager but was the most convenient place to put it
2020-05-25 23:15:32 +10:00
protected virtual void OnAccountLocked ( IdentityAuditEventArgs e ) = > AccountLocked ? . Invoke ( this , e ) ;
2020-05-14 22:21:19 +01:00
2020-10-23 10:10:02 +11:00
protected virtual void OnSendingUserInvite ( UserInviteEventArgs e ) = > SendingUserInvite ? . Invoke ( this , e ) ;
2020-10-23 14:18:53 +11:00
2020-05-25 23:15:32 +10:00
protected virtual void OnAccountUnlocked ( IdentityAuditEventArgs e ) = > AccountUnlocked ? . Invoke ( this , e ) ;
2020-05-14 22:21:19 +01:00
2020-05-25 23:15:32 +10:00
protected virtual void OnForgotPasswordRequested ( IdentityAuditEventArgs e ) = > ForgotPasswordRequested ? . Invoke ( this , e ) ;
2020-05-14 22:21:19 +01:00
2020-05-25 23:15:32 +10:00
protected virtual void OnForgotPasswordChangedSuccess ( IdentityAuditEventArgs e ) = > ForgotPasswordChangedSuccess ? . Invoke ( this , e ) ;
2020-05-14 22:21:19 +01:00
2020-05-25 23:15:32 +10:00
protected virtual void OnLoginFailed ( IdentityAuditEventArgs e ) = > LoginFailed ? . Invoke ( this , e ) ;
2020-05-14 22:21:19 +01:00
2020-05-25 23:15:32 +10:00
protected virtual void OnLoginRequiresVerification ( IdentityAuditEventArgs e ) = > LoginRequiresVerification ? . Invoke ( this , e ) ;
2020-05-14 22:21:19 +01:00
2020-05-25 23:15:32 +10:00
protected virtual void OnLoginSuccess ( IdentityAuditEventArgs e ) = > LoginSuccess ? . Invoke ( this , e ) ;
2020-05-14 22:21:19 +01:00
2020-10-23 14:18:53 +11:00
protected virtual void OnLogoutSuccess ( SignOutAuditEventArgs e ) = > LogoutSuccess ? . Invoke ( this , e ) ;
2020-05-14 22:21:19 +01:00
2020-05-25 23:15:32 +10:00
protected virtual void OnPasswordChanged ( IdentityAuditEventArgs e ) = > PasswordChanged ? . Invoke ( this , e ) ;
2020-05-14 22:21:19 +01:00
2020-05-25 23:15:32 +10:00
protected virtual void OnPasswordReset ( IdentityAuditEventArgs e ) = > PasswordReset ? . Invoke ( this , e ) ;
2020-05-14 22:21:19 +01:00
2020-05-25 23:15:32 +10:00
protected virtual void OnResetAccessFailedCount ( IdentityAuditEventArgs e ) = > ResetAccessFailedCount ? . Invoke ( this , e ) ;
2020-05-14 22:21:19 +01:00
}
}