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();
}