2020-05-18 12:06:26 +01: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 ;
using System.Threading.Tasks ;
using Microsoft.AspNetCore.Identity ;
using Microsoft.Extensions.Logging ;
using Microsoft.Extensions.Options ;
using Umbraco.Core.Configuration ;
2020-08-20 22:18:50 +01:00
using Umbraco.Core.Configuration.Models ;
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
using Umbraco.Net ;
2020-05-17 08:48:36 +01:00
namespace Umbraco.Core.BackOffice
2020-05-14 22:21:19 +01:00
{
2020-09-22 14:44:41 +02:00
public class BackOfficeUserManager : BackOfficeUserManager < BackOfficeIdentityUser > , IBackOfficeUserManager
2020-05-14 22:21:19 +01:00
{
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-06-22 10:08:08 +02:00
ILogger < UserManager < BackOfficeIdentityUser > > logger ,
2020-08-20 22:18:50 +01:00
IOptions < UserPasswordConfigurationSettings > passwordConfiguration )
2020-06-22 10:08:08 +02:00
: base ( ipResolver , store , optionsAccessor , passwordHasher , userValidators , passwordValidators , keyNormalizer , errors , services , logger , passwordConfiguration )
2020-05-14 22:21:19 +01:00
{
}
}
public class BackOfficeUserManager < T > : UserManager < T >
where T : BackOfficeIdentityUser
{
private PasswordGenerator _passwordGenerator ;
2020-06-22 10:08:08 +02:00
2020-05-14 22:21:19 +01:00
public BackOfficeUserManager (
IIpResolver ipResolver ,
IUserStore < T > store ,
2020-05-21 15:43:33 +10:00
IOptions < BackOfficeIdentityOptions > optionsAccessor ,
2020-05-17 07:56:59 +01:00
IPasswordHasher < T > passwordHasher ,
2020-05-14 22:21:19 +01:00
IEnumerable < IUserValidator < T > > userValidators ,
IEnumerable < IPasswordValidator < T > > passwordValidators ,
2020-05-21 15:43:33 +10:00
BackOfficeLookupNormalizer keyNormalizer ,
BackOfficeIdentityErrorDescriber errors ,
2020-05-14 22:21:19 +01:00
IServiceProvider services ,
2020-06-22 10:08:08 +02:00
ILogger < UserManager < T > > logger ,
2020-08-20 22:18:50 +01:00
IOptions < UserPasswordConfigurationSettings > passwordConfiguration )
2020-05-17 07:56:59 +01:00
: base ( store , optionsAccessor , passwordHasher , userValidators , passwordValidators , keyNormalizer , errors , services , logger )
2020-05-14 22:21:19 +01:00
{
IpResolver = ipResolver ? ? throw new ArgumentNullException ( nameof ( ipResolver ) ) ;
2020-08-20 22:18:50 +01:00
PasswordConfiguration = passwordConfiguration . Value ? ? throw new ArgumentNullException ( nameof ( passwordConfiguration ) ) ;
2020-05-14 22:21:19 +01:00
}
#region What we do not currently support
2020-06-03 12:47:40 +10:00
// We don't support an IUserClaimStore and don't need to (at least currently)
2020-05-14 22:21:19 +01:00
public override bool SupportsUserClaim = > false ;
2020-06-22 10:08:08 +02:00
2020-06-03 12:47:40 +10:00
// 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-05-14 22:21:19 +01:00
public override bool SupportsQueryableUsers = > false ;
/// <summary>
/// Developers will need to override this to support custom 2 factor auth
/// </summary>
public override bool SupportsUserTwoFactor = > false ;
2020-06-03 12:47:40 +10:00
// We haven't needed to support this yet, though might be necessary for 2FA
2020-05-14 22:21:19 +01:00
public override bool SupportsUserPhoneNumber = > false ;
2020-06-03 12:47:40 +10:00
2020-05-14 22:21:19 +01:00
#endregion
2020-05-18 12:06:26 +01:00
2020-05-21 15:43:33 +10:00
/// <summary>
/// Replace the underlying options property with our own strongly typed version
/// </summary>
public new BackOfficeIdentityOptions Options
{
get = > ( BackOfficeIdentityOptions ) base . Options ;
set = > base . Options = value ;
}
2020-05-14 22:21:19 +01:00
/// <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)
2020-05-15 15:21:15 +01:00
return new PasswordHasher < T > ( ) ;
2020-05-14 22:21:19 +01:00
}
2020-06-22 10:08:08 +02:00
2020-05-14 22:21:19 +01:00
/// <summary>
/// Gets/sets the default back office user password checker
/// </summary>
public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get ; set ; }
2020-05-18 12:06:26 +01:00
public IPasswordConfiguration PasswordConfiguration { get ; protected set ; }
2020-05-14 22:21:19 +01:00
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 ) ;
}
2020-06-22 10:08:08 +02:00
2020-05-14 22:21:19 +01:00
/// <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 ) ;
2020-05-21 16:33:24 +10:00
if ( result . Succeeded )
RaisePasswordChangedEvent ( null , userId ) ; // TODO: How can we get the current user? we have not HttpContext (netstandard), we can make our own IPrincipalAccessor?
2020-05-14 22:21:19 +01:00
return result ;
}
public override async Task < IdentityResult > ChangePasswordAsync ( T user , string currentPassword , string newPassword )
{
var result = await base . ChangePasswordAsync ( user , currentPassword , newPassword ) ;
2020-05-21 16:33:24 +10:00
if ( result . Succeeded )
RaisePasswordChangedEvent ( null , user . Id ) ; // TODO: How can we get the current user? we have not HttpContext (netstandard), we can make our own IPrincipalAccessor?
2020-05-14 22:21:19 +01:00
return result ;
}
2020-06-22 10:08:08 +02:00
2020-05-14 22:21:19 +01:00
/// <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 )
{
2020-05-21 16:33:24 +10:00
RaiseAccountLockedEvent ( null , user . Id ) ; // TODO: How can we get the current user? we have not HttpContext (netstandard), we can make our own IPrincipalAccessor?
2020-05-14 22:21:19 +01:00
}
else
{
2020-05-21 16:33:24 +10:00
RaiseAccountUnlockedEvent ( null , user . Id ) ; // TODO: How can we get the current user? we have not HttpContext (netstandard), we can make our own IPrincipalAccessor?
2020-05-14 22:21:19 +01:00
//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
2020-05-21 16:33:24 +10:00
RaiseResetAccessFailedCountEvent ( null , user . Id ) ; // TODO: How can we get the current user? we have not HttpContext (netstandard), we can make our own IPrincipalAccessor?
2020-05-14 22:21:19 +01:00
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
2020-05-21 16:33:24 +10:00
if ( result . Succeeded )
{
// TODO: This may no longer be the case in netcore, we'll need to see about that
RaiseLoginFailedEvent ( null , user . Id ) ; // TODO: How can we get the current user? we have not HttpContext (netstandard), we can make our own IPrincipalAccessor?
}
2020-05-14 22:21:19 +01:00
return result ;
}
2020-05-21 16:33:24 +10:00
private IdentityAuditEventArgs CreateArgs ( AuditEvent auditEvent , IPrincipal currentUser , int affectedUserId , string affectedUsername )
2020-05-14 22:21:19 +01:00
{
2020-05-21 16:33:24 +10:00
var umbIdentity = currentUser ? . GetUmbracoIdentity ( ) ;
2020-06-22 10:08:08 +02:00
var currentUserId = umbIdentity ? . GetUserId < int? > ( ) ? ? Constants . Security . SuperUserId ;
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
}
private IdentityAuditEventArgs CreateArgs ( AuditEvent auditEvent , BackOfficeIdentityUser currentUser , int affectedUserId , string affectedUsername )
{
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-10-19 18:48:51 +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
2020-05-27 18:27:49 +10:00
// In some cases it will be nicer/easier to not pass in IPrincipal
2020-10-19 18:48:51 +11:00
public void RaiseAccountLockedEvent ( BackOfficeIdentityUser currentUser , int userId ) = > OnAccountLocked ( CreateArgs ( AuditEvent . AccountLocked , currentUser , userId , string . Empty ) ) ;
2020-05-14 22:21:19 +01:00
2020-05-21 16:33:24 +10:00
public void RaiseAccountUnlockedEvent ( IPrincipal currentUser , int userId ) = > OnAccountUnlocked ( CreateArgs ( AuditEvent . AccountUnlocked , currentUser , userId , string . Empty ) ) ;
2020-05-14 22:21:19 +01:00
2020-05-21 16:33:24 +10:00
public void RaiseForgotPasswordRequestedEvent ( IPrincipal currentUser , int userId ) = > OnForgotPasswordRequested ( CreateArgs ( AuditEvent . ForgotPasswordRequested , currentUser , userId , string . Empty ) ) ;
2020-05-14 22:21:19 +01:00
2020-05-21 16:33:24 +10:00
public void RaiseForgotPasswordChangedSuccessEvent ( IPrincipal currentUser , int userId ) = > OnForgotPasswordChangedSuccess ( CreateArgs ( AuditEvent . ForgotPasswordChangedSuccess , currentUser , userId , string . Empty ) ) ;
2020-05-14 22:21:19 +01:00
2020-05-21 16:33:24 +10:00
public void RaiseLoginFailedEvent ( IPrincipal currentUser , int userId ) = > OnLoginFailed ( CreateArgs ( AuditEvent . LoginFailed , currentUser , userId , string . Empty ) ) ;
2020-05-14 22:21:19 +01:00
2020-05-21 16:33:24 +10:00
public void RaiseInvalidLoginAttemptEvent ( IPrincipal currentUser , string username ) = > OnLoginFailed ( CreateArgs ( AuditEvent . LoginFailed , currentUser , Constants . Security . SuperUserId , username ) ) ;
2020-05-14 22:21:19 +01:00
2020-05-21 16:33:24 +10:00
public void RaiseLoginRequiresVerificationEvent ( IPrincipal currentUser , int userId ) = > OnLoginRequiresVerification ( CreateArgs ( AuditEvent . LoginRequiresVerification , currentUser , userId , string . Empty ) ) ;
2020-10-23 10:10:02 +11:00
internal SignOutAuditEventArgs RaiseLogoutSuccessEvent ( int userId )
{
var args = new SignOutAuditEventArgs ( AuditEvent . LogoutSuccess , IpResolver . GetCurrentRequestIpAddress ( ) , affectedUser : userId ) ;
OnLogoutSuccess ( args ) ;
return args ;
}
2020-05-14 22:21:19 +01:00
2020-05-27 18:27:49 +10:00
public void RaiseLoginSuccessEvent ( BackOfficeIdentityUser currentUser , int userId ) = > OnLoginSuccess ( CreateArgs ( AuditEvent . LoginSucces , currentUser , userId , string . Empty ) ) ;
2020-05-14 22:21:19 +01:00
2020-05-21 16:33:24 +10:00
public void RaiseLogoutSuccessEvent ( IPrincipal currentUser , int userId ) = > OnLogoutSuccess ( CreateArgs ( AuditEvent . LogoutSuccess , currentUser , userId , string . Empty ) ) ;
2020-05-14 22:21:19 +01:00
2020-05-21 16:33:24 +10:00
public void RaisePasswordChangedEvent ( IPrincipal currentUser , int userId ) = > OnPasswordChanged ( CreateArgs ( AuditEvent . LogoutSuccess , currentUser , userId , string . Empty ) ) ;
public void RaiseResetAccessFailedCountEvent ( IPrincipal currentUser , int userId ) = > OnResetAccessFailedCount ( CreateArgs ( AuditEvent . ResetAccessFailedCount , currentUser , userId , string . Empty ) ) ;
2020-05-14 22:21:19 +01:00
2020-10-23 10:10:02 +11:00
internal void RaiseSendingUserInvite ( UserInviteEventArgs args ) = > OnSendingUserInvite ( args ) ;
internal bool HasSendingUserInviteEventHandler = > SendingUserInvite ! = null ;
// TODO: Not sure why these are not strongly typed events?? They should be in netcore!
2020-05-14 22:21:19 +01:00
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 ;
2020-10-23 10:10:02 +11:00
/// <summary>
/// Raised when a user is invited
/// </summary>
public static event EventHandler 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-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-05-25 23:15:32 +10:00
protected virtual void OnLogoutSuccess ( IdentityAuditEventArgs 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
}
}