From 1e915a0c42db610c2095b6faeaac1e5e005de926 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Fri, 10 Mar 2023 18:07:12 +0800 Subject: [PATCH] Add HandlePeriodicallyChangePassword --- .../Identity/AspNetCore/AbpSignInManager.cs | 26 ++++++++- .../Volo/Abp/Identity/IdentityUser.cs | 2 +- .../Volo/Abp/Identity/IdentityUserManager.cs | 18 ++++++- .../AbpResourceOwnerPasswordValidator.cs | 53 +++++++++++++++++-- .../Controllers/TokenController.Password.cs | 45 ++++++++++++++-- 5 files changed, 130 insertions(+), 14 deletions(-) diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs index 42ef9e5261..be6b74d98f 100644 --- a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs @@ -5,6 +5,9 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Volo.Abp.Identity.Settings; +using Volo.Abp.Settings; +using Volo.Abp.Timing; namespace Volo.Abp.Identity.AspNetCore; @@ -12,6 +15,10 @@ public class AbpSignInManager : SignInManager { protected AbpIdentityOptions AbpOptions { get; } + protected ISettingProvider SettingProvider { get; } + + protected IClock Clock { get; } + public AbpSignInManager( IdentityUserManager userManager, IHttpContextAccessor contextAccessor, @@ -20,8 +27,9 @@ public class AbpSignInManager : SignInManager ILogger> logger, IAuthenticationSchemeProvider schemes, IUserConfirmation confirmation, - IOptions options - ) : base( + IOptions options, + ISettingProvider settingProvider, + IClock clock) : base( userManager, contextAccessor, claimsFactory, @@ -30,6 +38,8 @@ public class AbpSignInManager : SignInManager schemes, confirmation) { + SettingProvider = settingProvider; + Clock = clock; AbpOptions = options.Value; } @@ -91,6 +101,18 @@ public class AbpSignInManager : SignInManager return SignInResult.NotAllowed; } + var forceUsersToPeriodicallyChangePassword = await SettingProvider.GetAsync(IdentitySettingNames.Password.ForceUsersToPeriodicallyChangePassword); + if (forceUsersToPeriodicallyChangePassword) + { + var passwordChangePeriodDays = await SettingProvider.GetAsync(IdentitySettingNames.Password.PasswordChangePeriodDays); + var lastPasswordChangeTime = user.LastPasswordChangeTime ?? user.CreationTime; + if (passwordChangePeriodDays > 0 && lastPasswordChangeTime.AddDays(passwordChangePeriodDays) < Clock.Now) + { + Logger.LogWarning($"The user should change password! (username: \"{user.UserName}\", id:\"{user.Id}\")"); + return SignInResult.NotAllowed; + } + } + return await base.PreSignInCheck(user); } } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUser.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUser.cs index d3ad2283d5..7b48e45a67 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUser.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUser.cs @@ -383,7 +383,7 @@ public class IdentityUser : FullAuditedAggregateRoot, IUser, IHasEntityVer ShouldChangePasswordOnNextLogin = shouldChangePasswordOnNextLogin; } - protected virtual void SetLastPasswordChangeTime(DateTime lastPasswordChangeTime) + public virtual void SetLastPasswordChangeTime(DateTime lastPasswordChangeTime) { LastPasswordChangeTime = lastPasswordChangeTime; } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs index d0520c9ef9..eb3e591f79 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs @@ -13,6 +13,7 @@ using Volo.Abp.Domain.Services; using Volo.Abp.Identity.Settings; using Volo.Abp.Settings; using Volo.Abp.Threading; +using Volo.Abp.Timing; using Volo.Abp.Uow; namespace Volo.Abp.Identity; @@ -25,6 +26,8 @@ public class IdentityUserManager : UserManager, IDomainService protected ISettingProvider SettingProvider { get; } protected ICancellationTokenProvider CancellationTokenProvider { get; } + protected IClock Clock { get; } + protected override CancellationToken CancellationToken => CancellationTokenProvider.Token; public IdentityUserManager( @@ -41,7 +44,8 @@ public class IdentityUserManager : UserManager, IDomainService ILogger logger, ICancellationTokenProvider cancellationTokenProvider, IOrganizationUnitRepository organizationUnitRepository, - ISettingProvider settingProvider) + ISettingProvider settingProvider, + IClock clock) : base( store, optionsAccessor, @@ -55,6 +59,7 @@ public class IdentityUserManager : UserManager, IDomainService { OrganizationUnitRepository = organizationUnitRepository; SettingProvider = settingProvider; + Clock = clock; RoleRepository = roleRepository; UserRepository = userRepository; CancellationTokenProvider = cancellationTokenProvider; @@ -253,4 +258,15 @@ public class IdentityUserManager : UserManager, IDomainService return await UpdateUserAsync(user); } + + protected async override Task UpdatePasswordHash(IdentityUser user, string newPassword, bool validatePassword) + { + var result = await base.UpdatePasswordHash(user, newPassword, validatePassword); + if (result.Succeeded && user.Id != default) + { + user.SetLastPasswordChangeTime(Clock.Now); + } + + return result; + } } diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidator.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidator.cs index 6e738df9be..f2129fa0c8 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidator.cs +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidator.cs @@ -17,8 +17,11 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; using Volo.Abp.Identity; +using Volo.Abp.Identity.Settings; using Volo.Abp.IdentityServer.Localization; using Volo.Abp.Security.Claims; +using Volo.Abp.Settings; +using Volo.Abp.Timing; using Volo.Abp.Uow; using Volo.Abp.Validation; using IdentityUser = Volo.Abp.Identity.IdentityUser; @@ -36,6 +39,10 @@ public class AbpResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator protected AbpIdentityOptions AbpIdentityOptions { get; } protected IOptions IdentityOptions { get; } + protected ISettingProvider SettingProvider { get; } + + protected IClock Clock { get; } + public AbpResourceOwnerPasswordValidator( UserManager userManager, SignInManager signInManager, @@ -44,7 +51,9 @@ public class AbpResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator IStringLocalizer localizer, IOptions abpIdentityOptions, IServiceScopeFactory serviceScopeFactory, - IOptions identityOptions) + IOptions identityOptions, + ISettingProvider settingProvider, + IClock clock) { UserManager = userManager; SignInManager = signInManager; @@ -54,6 +63,8 @@ public class AbpResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator ServiceScopeFactory = serviceScopeFactory; AbpIdentityOptions = abpIdentityOptions.Value; IdentityOptions = identityOptions; + SettingProvider = settingProvider; + Clock = clock; } /// @@ -129,6 +140,18 @@ public class AbpResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator return; } + var forceUsersToPeriodicallyChangePassword = await SettingProvider.GetAsync(IdentitySettingNames.Password.ForceUsersToPeriodicallyChangePassword); + if (forceUsersToPeriodicallyChangePassword) + { + var passwordChangePeriodDays = await SettingProvider.GetAsync(IdentitySettingNames.Password.PasswordChangePeriodDays); + var lastPasswordChangeTime = user.LastPasswordChangeTime ?? user.CreationTime; + if (passwordChangePeriodDays > 0 && lastPasswordChangeTime.AddDays(passwordChangePeriodDays) < Clock.Now) + { + await HandlePeriodicallyChangePasswordAsync(context, user, context.Password); + return; + } + } + errorDescription = Localizer["LoginIsNotAllowed"]; } else @@ -201,12 +224,22 @@ public class AbpResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator } protected virtual async Task HandleShouldChangePasswordOnNextLoginAsync(ResourceOwnerPasswordValidationContext context, IdentityUser user, string currentPassword) + { + await HandlerChangePasswordAsync(context, user, currentPassword, ChangePasswordType.ShouldChangePasswordOnNextLogin); + } + + protected virtual async Task HandlePeriodicallyChangePasswordAsync(ResourceOwnerPasswordValidationContext context, IdentityUser user, string currentPassword) + { + await HandlerChangePasswordAsync(context, user, currentPassword, ChangePasswordType.PeriodicallyChangePassword); + } + + protected virtual async Task HandlerChangePasswordAsync(ResourceOwnerPasswordValidationContext context, IdentityUser user, string currentPassword, ChangePasswordType changePasswordType) { var changePasswordToken = context.Request?.Raw?["ChangePasswordToken"]; var newPassword = context.Request?.Raw?["NewPassword"]; if (!changePasswordToken.IsNullOrWhiteSpace() && !currentPassword.IsNullOrWhiteSpace() && !newPassword.IsNullOrWhiteSpace()) { - if (await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, nameof(IdentityUser.ShouldChangePasswordOnNextLogin), changePasswordToken)) + if (await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, changePasswordType.ToString(), changePasswordToken)) { var changePasswordResult = await UserManager.ChangePasswordAsync(user, currentPassword, newPassword); if (changePasswordResult.Succeeded) @@ -219,7 +252,11 @@ public class AbpResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator ClientId = await FindClientIdAsync(context) }); - user.SetShouldChangePasswordOnNextLogin(false); + if (changePasswordType == ChangePasswordType.ShouldChangePasswordOnNextLogin) + { + user.SetShouldChangePasswordOnNextLogin(false); + } + await UserManager.UpdateAsync(user); await SetSuccessResultAsync(context, user); } @@ -237,12 +274,12 @@ public class AbpResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator } else { - Logger.LogInformation("Authentication failed for username: {username}, reason: {ShouldChangePasswordOnNextLogin}", context.UserName, nameof(user.ShouldChangePasswordOnNextLogin)); + Logger.LogInformation($"Authentication failed for username: {{{context.UserName}}}, reason: {{{changePasswordType.ToString()}}}"); context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, nameof(user.ShouldChangePasswordOnNextLogin), new Dictionary() { {"userId", user.Id}, - {"changePasswordToken", await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, nameof(IdentityUser.ShouldChangePasswordOnNextLogin))} + {"changePasswordToken", await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, changePasswordType.ToString())} }); await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext @@ -328,4 +365,10 @@ public class AbpResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator return Task.CompletedTask; } + + public enum ChangePasswordType + { + ShouldChangePasswordOnNextLogin, + PeriodicallyChangePassword + } } diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/TokenController.Password.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/TokenController.Password.cs index 3e9168e7fb..5ba8aa9090 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/TokenController.Password.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/TokenController.Password.cs @@ -12,7 +12,9 @@ using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; using Volo.Abp.DependencyInjection; using Volo.Abp.Identity; +using Volo.Abp.Identity.Settings; using Volo.Abp.MultiTenancy; +using Volo.Abp.Settings; using Volo.Abp.Uow; using Volo.Abp.Validation; using IdentityUser = Volo.Abp.Identity.IdentityUser; @@ -28,6 +30,8 @@ public partial class TokenController protected IOptions IdentityOptions => LazyServiceProvider.LazyGetRequiredService>(); protected IdentitySecurityLogManager IdentitySecurityLogManager => LazyServiceProvider.LazyGetRequiredService(); + protected ISettingProvider SettingProvider => LazyServiceProvider.LazyGetRequiredService(); + [UnitOfWork] protected virtual async Task HandlePasswordAsync(OpenIddictRequest request) { @@ -107,6 +111,17 @@ public partial class TokenController return await HandleShouldChangePasswordOnNextLoginAsync(request, user, request.Password); } + var forceUsersToPeriodicallyChangePassword = await SettingProvider.GetAsync(IdentitySettingNames.Password.ForceUsersToPeriodicallyChangePassword); + if (forceUsersToPeriodicallyChangePassword) + { + var passwordChangePeriodDays = await SettingProvider.GetAsync(IdentitySettingNames.Password.PasswordChangePeriodDays); + var lastPasswordChangeTime = user.LastPasswordChangeTime ?? user.CreationTime; + if (passwordChangePeriodDays > 0 && lastPasswordChangeTime.AddDays(passwordChangePeriodDays) < Clock.Now) + { + return await HandlePeriodicallyChangePasswordAsync(request, user, request.Password); + } + } + errorDescription = "You are not allowed to login! Your account is inactive or needs to confirm your email/phone number."; } else @@ -216,12 +231,22 @@ public partial class TokenController } protected virtual async Task HandleShouldChangePasswordOnNextLoginAsync(OpenIddictRequest request, IdentityUser user, string currentPassword) + { + return await HandleChangePasswordAsync(request, user, currentPassword, ChangePasswordType.ShouldChangePasswordOnNextLogin); + } + + protected virtual async Task HandlePeriodicallyChangePasswordAsync(OpenIddictRequest request, IdentityUser user, string currentPassword) + { + return await HandleChangePasswordAsync(request, user, currentPassword, ChangePasswordType.PeriodicallyChangePassword); + } + + protected virtual async Task HandleChangePasswordAsync(OpenIddictRequest request, IdentityUser user, string currentPassword, ChangePasswordType changePasswordType) { var changePasswordToken = request.GetParameter("ChangePasswordToken")?.ToString(); var newPassword = request.GetParameter("NewPassword")?.ToString(); if (!changePasswordToken.IsNullOrWhiteSpace() && !currentPassword.IsNullOrWhiteSpace() && !newPassword.IsNullOrWhiteSpace()) { - if (await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, nameof(IdentityUser.ShouldChangePasswordOnNextLogin), changePasswordToken)) + if (await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, changePasswordType.ToString(), changePasswordToken)) { var changePasswordResult = await UserManager.ChangePasswordAsync(user, currentPassword, newPassword); if (changePasswordResult.Succeeded) @@ -234,7 +259,11 @@ public partial class TokenController ClientId = request.ClientId }); - user.SetShouldChangePasswordOnNextLogin(false); + if (changePasswordType == ChangePasswordType.ShouldChangePasswordOnNextLogin) + { + user.SetShouldChangePasswordOnNextLogin(false); + } + await UserManager.UpdateAsync(user); return await SetSuccessResultAsync(request, user); } @@ -265,7 +294,7 @@ public partial class TokenController } else { - Logger.LogInformation("Authentication failed for username: {username}, reason: {ShouldChangePasswordOnNextLogin}", request.Username, nameof(user.ShouldChangePasswordOnNextLogin)); + Logger.LogInformation($"Authentication failed for username: {{{request.Username}}}, reason: {{{changePasswordType.ToString()}}}"); await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext { @@ -279,12 +308,12 @@ public partial class TokenController items: new Dictionary { [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = nameof(IdentityUser.ShouldChangePasswordOnNextLogin) + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = changePasswordType.ToString() }, parameters: new Dictionary { ["userId"] = user.Id.ToString("N"), - ["changePasswordToken"] = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, nameof(IdentityUser.ShouldChangePasswordOnNextLogin)) + ["changePasswordToken"] = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, changePasswordType.ToString()) }); return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); @@ -321,4 +350,10 @@ public partial class TokenController await UserManager.GetTwoFactorEnabledAsync(user) && (await UserManager.GetValidTwoFactorProvidersAsync(user)).Count > 0; } + + public enum ChangePasswordType + { + ShouldChangePasswordOnNextLogin, + PeriodicallyChangePassword + } }