From 20acbd1525102f073f9b801255bbf5976a5eecd1 Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 2 Jun 2026 11:28:26 +0800 Subject: [PATCH] Address Copilot review on #25525 --- docs/en/modules/identity/token-providers.md | 6 +++--- .../AspNetCore/AbpDefaultTokenProvider.cs | 3 ++- .../AspNetCore/AbpDefaultTokenProvider_Tests.cs | 17 +++++++++-------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/en/modules/identity/token-providers.md b/docs/en/modules/identity/token-providers.md index 609d376b71..326af4f77c 100644 --- a/docs/en/modules/identity/token-providers.md +++ b/docs/en/modules/identity/token-providers.md @@ -15,7 +15,7 @@ ABP replaces the `Default`, `Email`, and `Phone` provider registrations with sin | Provider key | Provider | Default | Used by | | --- | --- | --- | --- | -| `TokenOptions.DefaultProvider` (`"Default"`) | `AbpDefaultTokenProvider` | 10 minutes | Generic challenge tokens (e.g. `RequiresTwoFactor`, `ShouldChangePassword`) issued by IdentityServer / OpenIddict password flow endpoints | +| `TokenOptions.DefaultProvider` (`"Default"`) | `AbpDefaultTokenProvider` | 10 minutes | Generic challenge tokens (e.g. `RequiresTwoFactor`, `ShouldChangePasswordOnNextLogin`, `PeriodicallyChangePassword`) issued by IdentityServer / OpenIddict password flow endpoints | | `AbpPasswordResetTokenProvider.ProviderName` (`"AbpPasswordReset"`) | `AbpPasswordResetTokenProvider` | 2 hours | `UserManager.GeneratePasswordResetTokenAsync` / `ResetPasswordAsync` | | `AbpEmailConfirmationTokenProvider.ProviderName` (`"AbpEmailConfirmation"`) | `AbpEmailConfirmationTokenProvider` | 2 hours | `UserManager.GenerateEmailConfirmationTokenAsync` / `ConfirmEmailAsync` | | `AbpChangeEmailTokenProvider.ProviderName` (`"AbpChangeEmail"`) | `AbpChangeEmailTokenProvider` | 2 hours | `UserManager.GenerateChangeEmailTokenAsync` / `ChangeEmailAsync` | @@ -54,7 +54,7 @@ The DataProtector-based providers (`AbpDefaultTokenProvider`, `AbpPasswordResetT This has the following effects: - **Generating a new token invalidates the previous one** for the same `(user, provider, purpose)`. Multiple requests in flight will only let the most recent token complete. -- **Per-purpose isolation.** The stored hash key includes the purpose, so a `RequiresTwoFactor` token and a `ShouldChangePassword` token issued under the same `"Default"` provider do not invalidate each other. +- **Per-purpose isolation.** The stored hash key includes the purpose, so a `RequiresTwoFactor` token and a `ShouldChangePasswordOnNextLogin` token issued under the same `"Default"` provider do not invalidate each other. - **`SecurityStamp` rotation invalidates every issued token.** This is inherited from the base `DataProtectorTokenProvider` and is unchanged. - **Validation never throws on data corruption.** A non-hex stored hash returns `false` from `ValidateAsync` instead of propagating a `FormatException`. @@ -104,7 +104,7 @@ await UserManager.RemoveLinkUserTokenAsync(user, customPurpose); Each method removes the stored hash under `"[AbpSingleActiveToken]"` for the corresponding purpose. Validation afterwards returns `false` even if the token blob itself is still within its DataProtector lifespan and the `SecurityStamp` is unchanged. -For tokens issued by `AbpDefaultTokenProvider` (e.g. `RequiresTwoFactor`, `ShouldChangePassword`), call `UserManager.RemoveAuthenticationTokenAsync` directly: +For tokens issued by `AbpDefaultTokenProvider` (e.g. `RequiresTwoFactor`, `ShouldChangePasswordOnNextLogin`, `PeriodicallyChangePassword`), call `UserManager.RemoveAuthenticationTokenAsync` directly: ```csharp await UserManager.RemoveAuthenticationTokenAsync( diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpDefaultTokenProvider.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpDefaultTokenProvider.cs index 55e874aaaa..e86a258441 100644 --- a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpDefaultTokenProvider.cs +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpDefaultTokenProvider.cs @@ -11,7 +11,8 @@ namespace Volo.Abp.Identity.AspNetCore; /// Replaces ASP.NET Identity's default /// registered under ("Default"). Used by callers such /// as the IdentityServer / OpenIddict token endpoints to issue short-lived challenge tokens -/// (RequiresTwoFactor, ShouldChangePassword) and consumed back by Account / SendSecurityCode. +/// (RequiresTwoFactor, ShouldChangePasswordOnNextLogin, PeriodicallyChangePassword) +/// and consumed back by Account / SendSecurityCode. /// Enforces, per purpose, a single active token to be valid. /// public class AbpDefaultTokenProvider : AbpSingleActiveTokenProvider diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpDefaultTokenProvider_Tests.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpDefaultTokenProvider_Tests.cs index 9f719a663e..ee39f38456 100644 --- a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpDefaultTokenProvider_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpDefaultTokenProvider_Tests.cs @@ -12,6 +12,11 @@ public class AbpDefaultTokenProvider_Tests : AbpSingleActiveTokenProviderTestBas { private const string TestPurpose = nameof(SignInResult.RequiresTwoFactor); + // Matches ChangePasswordType.ShouldChangePasswordOnNextLogin.ToString() in + // AbpResourceOwnerPasswordValidator (Volo.Abp.IdentityServer.Domain). Hard-coded + // here to avoid taking a project dependency on the IdentityServer module. + private const string ChangePasswordPurpose = "ShouldChangePasswordOnNextLogin"; + protected override Task GenerateTokenAsync(IdentityUser user) => UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, TestPurpose); @@ -46,18 +51,16 @@ public class AbpDefaultTokenProvider_Tests : AbpSingleActiveTokenProviderTestBas [Fact] public async Task Tokens_For_Different_Purposes_Should_Be_Independent() { - const string changePasswordPurpose = "ShouldChangePasswordOnNextLogin"; - using (var uow = UnitOfWorkManager.Begin()) { var user = await UserRepository.GetAsync(TestData.UserJohnId); var twoFactorToken = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, TestPurpose); - var changePasswordToken = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, changePasswordPurpose); + var changePasswordToken = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, ChangePasswordPurpose); user = await UserRepository.GetAsync(TestData.UserJohnId); (await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, TestPurpose, twoFactorToken)).ShouldBeTrue(); - (await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, changePasswordPurpose, changePasswordToken)).ShouldBeTrue(); + (await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, ChangePasswordPurpose, changePasswordToken)).ShouldBeTrue(); await uow.CompleteAsync(); } @@ -66,13 +69,11 @@ public class AbpDefaultTokenProvider_Tests : AbpSingleActiveTokenProviderTestBas [Fact] public async Task Regenerating_Same_Purpose_Should_Invalidate_Only_That_Purpose() { - const string changePasswordPurpose = "ShouldChangePasswordOnNextLogin"; - using (var uow = UnitOfWorkManager.Begin()) { var user = await UserRepository.GetAsync(TestData.UserJohnId); var firstTwoFactorToken = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, TestPurpose); - var changePasswordToken = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, changePasswordPurpose); + var changePasswordToken = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, ChangePasswordPurpose); user = await UserRepository.GetAsync(TestData.UserJohnId); var secondTwoFactorToken = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, TestPurpose); @@ -81,7 +82,7 @@ public class AbpDefaultTokenProvider_Tests : AbpSingleActiveTokenProviderTestBas (await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, TestPurpose, firstTwoFactorToken)).ShouldBeFalse(); (await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, TestPurpose, secondTwoFactorToken)).ShouldBeTrue(); - (await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, changePasswordPurpose, changePasswordToken)).ShouldBeTrue(); + (await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, ChangePasswordPurpose, changePasswordToken)).ShouldBeTrue(); await uow.CompleteAsync(); }