Browse Source

Address Copilot review on #25525

pull/25525/head
maliming 4 months ago
parent
commit
20acbd1525
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 6
      docs/en/modules/identity/token-providers.md
  2. 3
      modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpDefaultTokenProvider.cs
  3. 17
      modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpDefaultTokenProvider_Tests.cs

6
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(

3
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 <see cref="DataProtectorTokenProvider{IdentityUser}"/>
/// registered under <see cref="TokenOptions.DefaultProvider"/> ("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.
/// </summary>
public class AbpDefaultTokenProvider : AbpSingleActiveTokenProvider

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

Loading…
Cancel
Save