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 1f2d679df5..befbc5b5e7 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 @@ -125,7 +125,7 @@ public class AbpResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator if (user.ShouldChangePasswordOnNextLogin) { - context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, nameof(user.ShouldChangePasswordOnNextLogin)); + await HandleShouldChangePasswordOnNextLoginAsync(context, user); return; } @@ -200,6 +200,62 @@ public class AbpResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator } } + protected virtual async Task HandleShouldChangePasswordOnNextLoginAsync(ResourceOwnerPasswordValidationContext context, IdentityUser user) + { + var changePasswordToken = context.Request?.Raw?["ChangePasswordToken"]; + var currentPassword = context.Request?.Raw?["CurrentPassword"]; + var newPassword = context.Request?.Raw?["NewPassword"]; + if (!changePasswordToken.IsNullOrWhiteSpace() && !currentPassword.IsNullOrWhiteSpace() && !newPassword.IsNullOrWhiteSpace()) + { + if (await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, nameof(IdentityUser.ShouldChangePasswordOnNextLogin), changePasswordToken)) + { + var changePasswordResult = await UserManager.ChangePasswordAsync(user, currentPassword, newPassword); + if (changePasswordResult.Succeeded) + { + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext + { + Identity = IdentityServerSecurityLogIdentityConsts.IdentityServer, + Action = IdentitySecurityLogActionConsts.ChangePassword, + UserName = context.UserName, + ClientId = await FindClientIdAsync(context) + }); + + user.SetShouldChangePasswordOnNextLogin(false); + await UserManager.UpdateAsync(user); + await SetSuccessResultAsync(context, user); + } + else + { + Logger.LogInformation("ChangePassword failed for username: {username}, reason: {changePasswordResult}", context.UserName, changePasswordResult); + context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, changePasswordResult.Errors.Select(x => x.Description).JoinAsString(", ")); + } + } + else + { + Logger.LogInformation("Authentication failed for username: {username}, reason: InvalidAuthenticatorCode", context.UserName); + context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, Localizer["InvalidAuthenticatorCode"]); + } + } + else + { + Logger.LogInformation("Authentication failed for username: {username}, reason: {ShouldChangePasswordOnNextLogin}", context.UserName, nameof(user.ShouldChangePasswordOnNextLogin)); + context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, nameof(user.ShouldChangePasswordOnNextLogin), + new Dictionary() + { + {"userId", user.Id}, + {"changePasswordToken", await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, nameof(IdentityUser.ShouldChangePasswordOnNextLogin))} + }); + + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext + { + Identity = IdentityServerSecurityLogIdentityConsts.IdentityServer, + Action = IdentityServerSecurityLogActionConsts.LoginNotAllowed, + UserName = context.UserName, + ClientId = await FindClientIdAsync(context) + }); + } + } + protected virtual async Task SetSuccessResultAsync(ResourceOwnerPasswordValidationContext context, IdentityUser user) { var sub = await UserManager.GetUserIdAsync(user); 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 6ffe3acec3..1ce8d32381 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 @@ -104,11 +104,7 @@ public partial class TokenController if (user.ShouldChangePasswordOnNextLogin) { - return Forbid( - new AuthenticationProperties(items: new Dictionary { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = nameof(user.ShouldChangePasswordOnNextLogin) - }), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + return await HandleShouldChangePasswordOnNextLoginAsync(request, user); } errorDescription = "You are not allowed to login! Your account is inactive or needs to confirm your email/phone number."; @@ -219,6 +215,83 @@ public partial class TokenController } } + protected virtual async Task HandleShouldChangePasswordOnNextLoginAsync(OpenIddictRequest request, IdentityUser user) + { + var changePasswordToken = request.GetParameter("ChangePasswordToken")?.ToString(); + var currentPassword = request.GetParameter("CurrentPassword")?.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)) + { + var changePasswordResult = await UserManager.ChangePasswordAsync(user, currentPassword, newPassword); + if (changePasswordResult.Succeeded) + { + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext + { + Identity = OpenIddictSecurityLogIdentityConsts.OpenIddict, + Action = IdentitySecurityLogActionConsts.ChangePassword, + UserName = request.Username, + ClientId = request.ClientId + }); + + user.SetShouldChangePasswordOnNextLogin(false); + await UserManager.UpdateAsync(user); + return await SetSuccessResultAsync(request, user); + } + else + { + Logger.LogInformation("ChangePassword failed for username: {username}, reason: {changePasswordResult}", request.Username, changePasswordResult.Errors.Select(x => x.Description).JoinAsString(", ")); + + var properties = new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = changePasswordResult.Errors.Select(x => x.Description).JoinAsString(", ") + }); + return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + } + else + { + Logger.LogInformation("Authentication failed for username: {username}, reason: InvalidAuthenticatorCode", request.Username); + + var properties = new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "Invalid authenticator code!" + }); + + return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + } + else + { + Logger.LogInformation("Authentication failed for username: {username}, reason: {ShouldChangePasswordOnNextLogin}", request.Username, nameof(user.ShouldChangePasswordOnNextLogin)); + + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext + { + Identity = OpenIddictSecurityLogIdentityConsts.OpenIddict, + Action = OpenIddictSecurityLogActionConsts.LoginNotAllowed, + UserName = request.Username, + ClientId = request.ClientId + }); + + var properties = new AuthenticationProperties( + items: new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = nameof(SignInResult.RequiresTwoFactor) + }, + parameters: new Dictionary + { + ["userId"] = user.Id.ToString("N"), + ["changePasswordToken"] = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, nameof(IdentityUser.ShouldChangePasswordOnNextLogin)) + }); + + return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + } + protected virtual async Task SetSuccessResultAsync(OpenIddictRequest request, IdentityUser user) { // Create a new ClaimsPrincipal containing the claims that