mirror of https://github.com/abpframework/abp.git
13 changed files with 868 additions and 0 deletions
@ -0,0 +1,25 @@ |
|||
using Microsoft.AspNetCore.DataProtection; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.Identity; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
/// <summary>
|
|||
/// Change email token provider that enforces only the most recently issued
|
|||
/// token to be valid, with a configurable expiration period.
|
|||
/// </summary>
|
|||
public class AbpChangeEmailTokenProvider : AbpSingleActiveTokenProvider |
|||
{ |
|||
public const string ProviderName = "AbpChangeEmail"; |
|||
|
|||
public AbpChangeEmailTokenProvider( |
|||
IDataProtectionProvider dataProtectionProvider, |
|||
IOptions<AbpChangeEmailTokenProviderOptions> options, |
|||
ILogger<DataProtectorTokenProvider<IdentityUser>> logger, |
|||
IIdentityUserRepository userRepository) |
|||
: base(dataProtectionProvider, options, logger, userRepository) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
using System; |
|||
using Microsoft.AspNetCore.Identity; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
public class AbpChangeEmailTokenProviderOptions : DataProtectionTokenProviderOptions |
|||
{ |
|||
public AbpChangeEmailTokenProviderOptions() |
|||
{ |
|||
Name = AbpChangeEmailTokenProvider.ProviderName; |
|||
TokenLifespan = TimeSpan.FromHours(2); |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
using Microsoft.AspNetCore.DataProtection; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.Identity; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
/// <summary>
|
|||
/// Email confirmation token provider that enforces only the most recently issued
|
|||
/// token to be valid, with a configurable expiration period.
|
|||
/// Token reuse is bounded by the token expiry and the single-active policy:
|
|||
/// generating a new token overwrites the stored hash, invalidating all previous tokens.
|
|||
/// <para>
|
|||
/// Unlike password-reset and change-email flows, <see cref="Microsoft.AspNetCore.Identity.UserManager{TUser}.ConfirmEmailAsync"/>
|
|||
/// does <b>not</b> update the security stamp, so the token hash is NOT automatically
|
|||
/// invalidated after a successful confirmation. Callers that require single-use semantics
|
|||
/// must explicitly revoke the hash after confirmation:
|
|||
/// <code>
|
|||
/// var result = await userManager.ConfirmEmailAsync(user, token);
|
|||
/// if (result.Succeeded)
|
|||
/// await userManager.RemoveEmailConfirmationTokenAsync(user);
|
|||
/// </code>
|
|||
/// </para>
|
|||
/// </summary>
|
|||
public class AbpEmailConfirmationTokenProvider : AbpSingleActiveTokenProvider |
|||
{ |
|||
public const string ProviderName = "AbpEmailConfirmation"; |
|||
|
|||
public AbpEmailConfirmationTokenProvider( |
|||
IDataProtectionProvider dataProtectionProvider, |
|||
IOptions<AbpEmailConfirmationTokenProviderOptions> options, |
|||
ILogger<DataProtectorTokenProvider<IdentityUser>> logger, |
|||
IIdentityUserRepository userRepository) |
|||
: base(dataProtectionProvider, options, logger, userRepository) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
using System; |
|||
using Microsoft.AspNetCore.Identity; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
public class AbpEmailConfirmationTokenProviderOptions : DataProtectionTokenProviderOptions |
|||
{ |
|||
public AbpEmailConfirmationTokenProviderOptions() |
|||
{ |
|||
Name = AbpEmailConfirmationTokenProvider.ProviderName; |
|||
TokenLifespan = TimeSpan.FromHours(2); |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
using Microsoft.AspNetCore.DataProtection; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.Identity; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
/// <summary>
|
|||
/// Password reset token provider that enforces only the most recently issued
|
|||
/// token to be valid, with a configurable expiration period.
|
|||
/// </summary>
|
|||
public class AbpPasswordResetTokenProvider : AbpSingleActiveTokenProvider |
|||
{ |
|||
public const string ProviderName = "AbpPasswordReset"; |
|||
|
|||
public AbpPasswordResetTokenProvider( |
|||
IDataProtectionProvider dataProtectionProvider, |
|||
IOptions<AbpPasswordResetTokenProviderOptions> options, |
|||
ILogger<DataProtectorTokenProvider<IdentityUser>> logger, |
|||
IIdentityUserRepository userRepository) |
|||
: base(dataProtectionProvider, options, logger, userRepository) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
using System; |
|||
using Microsoft.AspNetCore.Identity; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
public class AbpPasswordResetTokenProviderOptions : DataProtectionTokenProviderOptions |
|||
{ |
|||
public AbpPasswordResetTokenProviderOptions() |
|||
{ |
|||
Name = AbpPasswordResetTokenProvider.ProviderName; |
|||
TokenLifespan = TimeSpan.FromHours(2); |
|||
} |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
using System; |
|||
using System.Security.Cryptography; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.DataProtection; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.Identity; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
/// <summary>
|
|||
/// Base class for ABP token providers that enforce a "single active token" policy:
|
|||
/// generating a new token automatically invalidates all previously issued tokens.
|
|||
/// Token validity is enforced by SecurityStamp verification (via the base class) and
|
|||
/// by the stored hash, which is overwritten each time a new token is generated.
|
|||
/// </summary>
|
|||
public abstract class AbpSingleActiveTokenProvider : DataProtectorTokenProvider<IdentityUser> |
|||
{ |
|||
public const string TokenHashSuffix = "_TokenHash"; |
|||
|
|||
protected IIdentityUserRepository UserRepository { get; } |
|||
|
|||
protected AbpSingleActiveTokenProvider( |
|||
IDataProtectionProvider dataProtectionProvider, |
|||
IOptions<DataProtectionTokenProviderOptions> options, |
|||
ILogger<DataProtectorTokenProvider<IdentityUser>> logger, |
|||
IIdentityUserRepository userRepository) |
|||
: base(dataProtectionProvider, options, logger) |
|||
{ |
|||
UserRepository = userRepository; |
|||
} |
|||
|
|||
public override async Task<string> GenerateAsync(string purpose, UserManager<IdentityUser> manager, IdentityUser user) |
|||
{ |
|||
var token = await base.GenerateAsync(purpose, manager, user); |
|||
|
|||
await UserRepository.EnsureCollectionLoadedAsync(user, u => u.Tokens); |
|||
var tokenHash = ComputeSha256Hash(token); |
|||
user.SetToken(Options.Name, purpose + TokenHashSuffix, tokenHash); |
|||
|
|||
await manager.UpdateAsync(user); |
|||
|
|||
return token; |
|||
} |
|||
|
|||
public override async Task<bool> ValidateAsync(string purpose, string token, UserManager<IdentityUser> manager, IdentityUser user) |
|||
{ |
|||
if (!await base.ValidateAsync(purpose, token, manager, user)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
await UserRepository.EnsureCollectionLoadedAsync(user, u => u.Tokens); |
|||
|
|||
var storedHash = user.FindToken(Options.Name, purpose + TokenHashSuffix)?.Value; |
|||
if (storedHash == null) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var inputHash = ComputeSha256Hash(token); |
|||
return string.Equals(storedHash, inputHash, StringComparison.Ordinal); |
|||
} |
|||
|
|||
protected virtual string ComputeSha256Hash(string input) |
|||
{ |
|||
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input)); |
|||
return Convert.ToHexString(bytes); |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Identity; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
/// <summary>
|
|||
/// Provides extension methods on <see cref="IdentityUserManager"/> for invalidating
|
|||
/// single-active tokens managed by <see cref="AbpSingleActiveTokenProvider"/>.
|
|||
/// These helpers live in the AspNetCore layer because they depend on
|
|||
/// <see cref="AbpSingleActiveTokenProvider.TokenHashSuffix"/>.
|
|||
/// </summary>
|
|||
public static class IdentityUserManagerSingleActiveTokenExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Removes the stored password-reset token hash for <paramref name="user"/>,
|
|||
/// immediately invalidating any previously issued password-reset token.
|
|||
/// </summary>
|
|||
public static Task<IdentityResult> RemovePasswordResetTokenAsync(this IdentityUserManager manager, IdentityUser user) |
|||
{ |
|||
var name = UserManager<IdentityUser>.ResetPasswordTokenPurpose + AbpSingleActiveTokenProvider.TokenHashSuffix; |
|||
return manager.RemoveAuthenticationTokenAsync(user, manager.Options.Tokens.PasswordResetTokenProvider, name); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes the stored email-confirmation token hash for <paramref name="user"/>,
|
|||
/// immediately invalidating any previously issued email-confirmation token.
|
|||
/// </summary>
|
|||
public static Task<IdentityResult> RemoveEmailConfirmationTokenAsync(this IdentityUserManager manager, IdentityUser user) |
|||
{ |
|||
var name = UserManager<IdentityUser>.ConfirmEmailTokenPurpose + AbpSingleActiveTokenProvider.TokenHashSuffix; |
|||
return manager.RemoveAuthenticationTokenAsync(user, manager.Options.Tokens.EmailConfirmationTokenProvider, name); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes the stored change-email token hash for <paramref name="user"/>,
|
|||
/// immediately invalidating any previously issued change-email token for <paramref name="newEmail"/>.
|
|||
/// </summary>
|
|||
public static Task<IdentityResult> RemoveChangeEmailTokenAsync(this IdentityUserManager manager, IdentityUser user, string newEmail) |
|||
{ |
|||
var name = UserManager<IdentityUser>.GetChangeEmailTokenPurpose(newEmail) + AbpSingleActiveTokenProvider.TokenHashSuffix; |
|||
return manager.RemoveAuthenticationTokenAsync(user, manager.Options.Tokens.ChangeEmailTokenProvider, name); |
|||
} |
|||
} |
|||
@ -0,0 +1,175 @@ |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.Extensions.Options; |
|||
using Shouldly; |
|||
using Volo.Abp.Uow; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
public class AbpChangeEmailTokenProvider_Tests : AbpIdentityAspNetCoreTestBase |
|||
{ |
|||
private const string NewEmail = "newemail@example.com"; |
|||
|
|||
protected IIdentityUserRepository UserRepository { get; } |
|||
protected IdentityUserManager UserManager { get; } |
|||
protected IdentityTestData TestData { get; } |
|||
protected IUnitOfWorkManager UnitOfWorkManager { get; } |
|||
|
|||
public AbpChangeEmailTokenProvider_Tests() |
|||
{ |
|||
UserRepository = GetRequiredService<IIdentityUserRepository>(); |
|||
UserManager = GetRequiredService<IdentityUserManager>(); |
|||
TestData = GetRequiredService<IdentityTestData>(); |
|||
UnitOfWorkManager = GetRequiredService<IUnitOfWorkManager>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void AbpChangeEmailTokenProvider_Should_Be_Registered() |
|||
{ |
|||
var identityOptions = GetRequiredService<IOptions<IdentityOptions>>().Value; |
|||
|
|||
identityOptions.Tokens.ProviderMap.ShouldContainKey(AbpChangeEmailTokenProvider.ProviderName); |
|||
identityOptions.Tokens.ProviderMap[AbpChangeEmailTokenProvider.ProviderName].ProviderType |
|||
.ShouldBe(typeof(AbpChangeEmailTokenProvider)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void ChangeEmailTokenProvider_Should_Be_Configured_As_Abp() |
|||
{ |
|||
var identityOptions = GetRequiredService<IOptions<IdentityOptions>>().Value; |
|||
|
|||
identityOptions.Tokens.ChangeEmailTokenProvider.ShouldBe(AbpChangeEmailTokenProvider.ProviderName); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Generate_And_Verify_Change_Email_Token_Should_Succeed() |
|||
{ |
|||
using (var uow = UnitOfWorkManager.Begin()) |
|||
{ |
|||
var john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
|
|||
var token = await UserManager.GenerateChangeEmailTokenAsync(john, NewEmail); |
|||
token.ShouldNotBeNullOrEmpty(); |
|||
|
|||
var isValid = await UserManager.VerifyUserTokenAsync( |
|||
john, |
|||
UserManager.Options.Tokens.ChangeEmailTokenProvider, |
|||
UserManager<IdentityUser>.GetChangeEmailTokenPurpose(NewEmail), |
|||
token); |
|||
|
|||
isValid.ShouldBeTrue(); |
|||
|
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Invalid_Token_Should_Fail_Verification() |
|||
{ |
|||
using (var uow = UnitOfWorkManager.Begin()) |
|||
{ |
|||
var john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
|
|||
await UserManager.GenerateChangeEmailTokenAsync(john, NewEmail); |
|||
|
|||
var isValid = await UserManager.VerifyUserTokenAsync( |
|||
john, |
|||
UserManager.Options.Tokens.ChangeEmailTokenProvider, |
|||
UserManager<IdentityUser>.GetChangeEmailTokenPurpose(NewEmail), |
|||
"invalid-token-value"); |
|||
|
|||
isValid.ShouldBeFalse(); |
|||
|
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Second_Token_Should_Invalidate_First_Token() |
|||
{ |
|||
using (var uow = UnitOfWorkManager.Begin()) |
|||
{ |
|||
var john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
|
|||
var firstToken = await UserManager.GenerateChangeEmailTokenAsync(john, NewEmail); |
|||
|
|||
john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
var secondToken = await UserManager.GenerateChangeEmailTokenAsync(john, NewEmail); |
|||
|
|||
john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
|
|||
var firstTokenValid = await UserManager.VerifyUserTokenAsync( |
|||
john, |
|||
UserManager.Options.Tokens.ChangeEmailTokenProvider, |
|||
UserManager<IdentityUser>.GetChangeEmailTokenPurpose(NewEmail), |
|||
firstToken); |
|||
|
|||
var secondTokenValid = await UserManager.VerifyUserTokenAsync( |
|||
john, |
|||
UserManager.Options.Tokens.ChangeEmailTokenProvider, |
|||
UserManager<IdentityUser>.GetChangeEmailTokenPurpose(NewEmail), |
|||
secondToken); |
|||
|
|||
firstTokenValid.ShouldBeFalse(); |
|||
secondTokenValid.ShouldBeTrue(); |
|||
|
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Token_Should_Become_Invalid_After_Email_Change() |
|||
{ |
|||
using (var uow = UnitOfWorkManager.Begin()) |
|||
{ |
|||
var john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
|
|||
var token = await UserManager.GenerateChangeEmailTokenAsync(john, NewEmail); |
|||
|
|||
john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
var result = await UserManager.ChangeEmailAsync(john, NewEmail, token); |
|||
result.Succeeded.ShouldBeTrue(); |
|||
|
|||
// SecurityStamp has changed after the email change, so the old token must be invalid.
|
|||
john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
var isValid = await UserManager.VerifyUserTokenAsync( |
|||
john, |
|||
UserManager.Options.Tokens.ChangeEmailTokenProvider, |
|||
UserManager<IdentityUser>.GetChangeEmailTokenPurpose(NewEmail), |
|||
token); |
|||
|
|||
isValid.ShouldBeFalse(); |
|||
|
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Token_Hash_Should_Persist_Across_UnitOfWork_Boundaries() |
|||
{ |
|||
string token; |
|||
|
|||
// UoW 1: generate the token; UpdateAsync inside GenerateAsync must persist the hash.
|
|||
using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) |
|||
{ |
|||
var john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
token = await UserManager.GenerateChangeEmailTokenAsync(john, NewEmail); |
|||
await uow.CompleteAsync(); |
|||
} |
|||
|
|||
// UoW 2: validate using a fresh DbContext to confirm the hash was written to the DB.
|
|||
using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) |
|||
{ |
|||
var john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
var isValid = await UserManager.VerifyUserTokenAsync( |
|||
john, |
|||
UserManager.Options.Tokens.ChangeEmailTokenProvider, |
|||
UserManager<IdentityUser>.GetChangeEmailTokenPurpose(NewEmail), |
|||
token); |
|||
|
|||
isValid.ShouldBeTrue(); |
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,178 @@ |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.Extensions.Options; |
|||
using Shouldly; |
|||
using Volo.Abp.Uow; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
public class AbpEmailConfirmationTokenProvider_Tests : AbpIdentityAspNetCoreTestBase |
|||
{ |
|||
protected IIdentityUserRepository UserRepository { get; } |
|||
protected IdentityUserManager UserManager { get; } |
|||
protected IdentityTestData TestData { get; } |
|||
protected IUnitOfWorkManager UnitOfWorkManager { get; } |
|||
|
|||
public AbpEmailConfirmationTokenProvider_Tests() |
|||
{ |
|||
UserRepository = GetRequiredService<IIdentityUserRepository>(); |
|||
UserManager = GetRequiredService<IdentityUserManager>(); |
|||
TestData = GetRequiredService<IdentityTestData>(); |
|||
UnitOfWorkManager = GetRequiredService<IUnitOfWorkManager>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void AbpEmailConfirmationTokenProvider_Should_Be_Registered() |
|||
{ |
|||
var identityOptions = GetRequiredService<IOptions<IdentityOptions>>().Value; |
|||
|
|||
identityOptions.Tokens.ProviderMap.ShouldContainKey(AbpEmailConfirmationTokenProvider.ProviderName); |
|||
identityOptions.Tokens.ProviderMap[AbpEmailConfirmationTokenProvider.ProviderName].ProviderType |
|||
.ShouldBe(typeof(AbpEmailConfirmationTokenProvider)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void EmailConfirmationTokenProvider_Should_Be_Configured_As_Abp() |
|||
{ |
|||
var identityOptions = GetRequiredService<IOptions<IdentityOptions>>().Value; |
|||
|
|||
identityOptions.Tokens.EmailConfirmationTokenProvider.ShouldBe(AbpEmailConfirmationTokenProvider.ProviderName); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Generate_And_Verify_Email_Confirmation_Token_Should_Succeed() |
|||
{ |
|||
using (var uow = UnitOfWorkManager.Begin()) |
|||
{ |
|||
var john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
|
|||
var token = await UserManager.GenerateEmailConfirmationTokenAsync(john); |
|||
token.ShouldNotBeNullOrEmpty(); |
|||
|
|||
var isValid = await UserManager.VerifyUserTokenAsync( |
|||
john, |
|||
UserManager.Options.Tokens.EmailConfirmationTokenProvider, |
|||
UserManager<IdentityUser>.ConfirmEmailTokenPurpose, |
|||
token); |
|||
|
|||
isValid.ShouldBeTrue(); |
|||
|
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Invalid_Token_Should_Fail_Verification() |
|||
{ |
|||
using (var uow = UnitOfWorkManager.Begin()) |
|||
{ |
|||
var john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
|
|||
await UserManager.GenerateEmailConfirmationTokenAsync(john); |
|||
|
|||
var isValid = await UserManager.VerifyUserTokenAsync( |
|||
john, |
|||
UserManager.Options.Tokens.EmailConfirmationTokenProvider, |
|||
UserManager<IdentityUser>.ConfirmEmailTokenPurpose, |
|||
"invalid-token-value"); |
|||
|
|||
isValid.ShouldBeFalse(); |
|||
|
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Second_Token_Should_Invalidate_First_Token() |
|||
{ |
|||
using (var uow = UnitOfWorkManager.Begin()) |
|||
{ |
|||
var john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
|
|||
var firstToken = await UserManager.GenerateEmailConfirmationTokenAsync(john); |
|||
|
|||
john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
var secondToken = await UserManager.GenerateEmailConfirmationTokenAsync(john); |
|||
|
|||
john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
|
|||
var firstTokenValid = await UserManager.VerifyUserTokenAsync( |
|||
john, |
|||
UserManager.Options.Tokens.EmailConfirmationTokenProvider, |
|||
UserManager<IdentityUser>.ConfirmEmailTokenPurpose, |
|||
firstToken); |
|||
|
|||
var secondTokenValid = await UserManager.VerifyUserTokenAsync( |
|||
john, |
|||
UserManager.Options.Tokens.EmailConfirmationTokenProvider, |
|||
UserManager<IdentityUser>.ConfirmEmailTokenPurpose, |
|||
secondToken); |
|||
|
|||
firstTokenValid.ShouldBeFalse(); |
|||
secondTokenValid.ShouldBeTrue(); |
|||
|
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Token_Should_Become_Invalid_After_Email_Confirmation_With_Explicit_Revocation() |
|||
{ |
|||
using (var uow = UnitOfWorkManager.Begin()) |
|||
{ |
|||
var john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
|
|||
var token = await UserManager.GenerateEmailConfirmationTokenAsync(john); |
|||
|
|||
var result = await UserManager.ConfirmEmailAsync(john, token); |
|||
result.Succeeded.ShouldBeTrue(); |
|||
|
|||
// ConfirmEmailAsync does NOT update SecurityStamp, so the hash is not
|
|||
// automatically invalidated. Callers that require single-use semantics must
|
|||
// explicitly revoke the stored token hash after a successful confirmation.
|
|||
john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
var removeResult = await UserManager.RemoveEmailConfirmationTokenAsync(john); |
|||
removeResult.Succeeded.ShouldBeTrue(); |
|||
|
|||
john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
var isValid = await UserManager.VerifyUserTokenAsync( |
|||
john, |
|||
UserManager.Options.Tokens.EmailConfirmationTokenProvider, |
|||
UserManager<IdentityUser>.ConfirmEmailTokenPurpose, |
|||
token); |
|||
|
|||
isValid.ShouldBeFalse(); |
|||
|
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Token_Hash_Should_Persist_Across_UnitOfWork_Boundaries() |
|||
{ |
|||
string token; |
|||
|
|||
// UoW 1: generate the token; UpdateAsync inside GenerateAsync must persist the hash.
|
|||
using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) |
|||
{ |
|||
var john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
token = await UserManager.GenerateEmailConfirmationTokenAsync(john); |
|||
await uow.CompleteAsync(); |
|||
} |
|||
|
|||
// UoW 2: validate using a fresh DbContext to confirm the hash was written to the DB.
|
|||
using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) |
|||
{ |
|||
var john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
var isValid = await UserManager.VerifyUserTokenAsync( |
|||
john, |
|||
UserManager.Options.Tokens.EmailConfirmationTokenProvider, |
|||
UserManager<IdentityUser>.ConfirmEmailTokenPurpose, |
|||
token); |
|||
|
|||
isValid.ShouldBeTrue(); |
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,173 @@ |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.Extensions.Options; |
|||
using Shouldly; |
|||
using Volo.Abp.Uow; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
public class AbpPasswordResetTokenProvider_Tests : AbpIdentityAspNetCoreTestBase |
|||
{ |
|||
protected IIdentityUserRepository UserRepository { get; } |
|||
protected IdentityUserManager UserManager { get; } |
|||
protected IdentityTestData TestData { get; } |
|||
protected IUnitOfWorkManager UnitOfWorkManager { get; } |
|||
|
|||
public AbpPasswordResetTokenProvider_Tests() |
|||
{ |
|||
UserRepository = GetRequiredService<IIdentityUserRepository>(); |
|||
UserManager = GetRequiredService<IdentityUserManager>(); |
|||
TestData = GetRequiredService<IdentityTestData>(); |
|||
UnitOfWorkManager = GetRequiredService<IUnitOfWorkManager>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void AbpPasswordResetTokenProvider_Should_Be_Registered() |
|||
{ |
|||
var identityOptions = GetRequiredService<IOptions<IdentityOptions>>().Value; |
|||
|
|||
identityOptions.Tokens.ProviderMap.ShouldContainKey(AbpPasswordResetTokenProvider.ProviderName); |
|||
identityOptions.Tokens.ProviderMap[AbpPasswordResetTokenProvider.ProviderName].ProviderType |
|||
.ShouldBe(typeof(AbpPasswordResetTokenProvider)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void PasswordResetTokenProvider_Should_Be_Configured_As_Abp() |
|||
{ |
|||
var identityOptions = GetRequiredService<IOptions<IdentityOptions>>().Value; |
|||
|
|||
identityOptions.Tokens.PasswordResetTokenProvider.ShouldBe(AbpPasswordResetTokenProvider.ProviderName); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Generate_And_Verify_Password_Reset_Token_Should_Succeed() |
|||
{ |
|||
using (var uow = UnitOfWorkManager.Begin()) |
|||
{ |
|||
var john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
|
|||
var token = await UserManager.GeneratePasswordResetTokenAsync(john); |
|||
token.ShouldNotBeNullOrEmpty(); |
|||
|
|||
var isValid = await UserManager.VerifyUserTokenAsync( |
|||
john, |
|||
UserManager.Options.Tokens.PasswordResetTokenProvider, |
|||
UserManager<IdentityUser>.ResetPasswordTokenPurpose, |
|||
token); |
|||
|
|||
isValid.ShouldBeTrue(); |
|||
|
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Invalid_Token_Should_Fail_Verification() |
|||
{ |
|||
using (var uow = UnitOfWorkManager.Begin()) |
|||
{ |
|||
var john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
|
|||
await UserManager.GeneratePasswordResetTokenAsync(john); |
|||
|
|||
var isValid = await UserManager.VerifyUserTokenAsync( |
|||
john, |
|||
UserManager.Options.Tokens.PasswordResetTokenProvider, |
|||
UserManager<IdentityUser>.ResetPasswordTokenPurpose, |
|||
"invalid-token-value"); |
|||
|
|||
isValid.ShouldBeFalse(); |
|||
|
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Second_Token_Should_Invalidate_First_Token() |
|||
{ |
|||
using (var uow = UnitOfWorkManager.Begin()) |
|||
{ |
|||
var john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
|
|||
var firstToken = await UserManager.GeneratePasswordResetTokenAsync(john); |
|||
|
|||
john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
var secondToken = await UserManager.GeneratePasswordResetTokenAsync(john); |
|||
|
|||
john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
|
|||
var firstTokenValid = await UserManager.VerifyUserTokenAsync( |
|||
john, |
|||
UserManager.Options.Tokens.PasswordResetTokenProvider, |
|||
UserManager<IdentityUser>.ResetPasswordTokenPurpose, |
|||
firstToken); |
|||
|
|||
var secondTokenValid = await UserManager.VerifyUserTokenAsync( |
|||
john, |
|||
UserManager.Options.Tokens.PasswordResetTokenProvider, |
|||
UserManager<IdentityUser>.ResetPasswordTokenPurpose, |
|||
secondToken); |
|||
|
|||
firstTokenValid.ShouldBeFalse(); |
|||
secondTokenValid.ShouldBeTrue(); |
|||
|
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Token_Should_Become_Invalid_After_Password_Reset() |
|||
{ |
|||
using (var uow = UnitOfWorkManager.Begin()) |
|||
{ |
|||
var john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
|
|||
var token = await UserManager.GeneratePasswordResetTokenAsync(john); |
|||
|
|||
john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
var result = await UserManager.ResetPasswordAsync(john, token, "1q2w3E*NewP@ss!"); |
|||
result.Succeeded.ShouldBeTrue(); |
|||
|
|||
// SecurityStamp has changed after reset, so the old token must be invalid.
|
|||
john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
var isValid = await UserManager.VerifyUserTokenAsync( |
|||
john, |
|||
UserManager.Options.Tokens.PasswordResetTokenProvider, |
|||
UserManager<IdentityUser>.ResetPasswordTokenPurpose, |
|||
token); |
|||
|
|||
isValid.ShouldBeFalse(); |
|||
|
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Token_Hash_Should_Persist_Across_UnitOfWork_Boundaries() |
|||
{ |
|||
string token; |
|||
|
|||
// UoW 1: generate the token; UpdateAsync inside GenerateAsync must persist the hash.
|
|||
using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) |
|||
{ |
|||
var john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
token = await UserManager.GeneratePasswordResetTokenAsync(john); |
|||
await uow.CompleteAsync(); |
|||
} |
|||
|
|||
// UoW 2: validate using a fresh DbContext to confirm the hash was written to the DB.
|
|||
using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) |
|||
{ |
|||
var john = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
var isValid = await UserManager.VerifyUserTokenAsync( |
|||
john, |
|||
UserManager.Options.Tokens.PasswordResetTokenProvider, |
|||
UserManager<IdentityUser>.ResetPasswordTokenPurpose, |
|||
token); |
|||
|
|||
isValid.ShouldBeTrue(); |
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,89 @@ |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Shouldly; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.Uow; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
public class IdentityUserManagerSingleActiveTokenExtensions_Tests : AbpIdentityAspNetCoreTestBase |
|||
{ |
|||
private const string NewEmail = "newemail@example.com"; |
|||
|
|||
protected IIdentityUserRepository UserRepository { get; } |
|||
protected IdentityUserManager UserManager { get; } |
|||
protected IdentityTestData TestData { get; } |
|||
protected IUnitOfWorkManager UnitOfWorkManager { get; } |
|||
|
|||
public IdentityUserManagerSingleActiveTokenExtensions_Tests() |
|||
{ |
|||
UserRepository = GetRequiredService<IIdentityUserRepository>(); |
|||
UserManager = GetRequiredService<IdentityUserManager>(); |
|||
TestData = GetRequiredService<IdentityTestData>(); |
|||
UnitOfWorkManager = GetRequiredService<IUnitOfWorkManager>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Remove_PasswordReset_TokenHash() |
|||
{ |
|||
using (var uow = UnitOfWorkManager.Begin()) |
|||
{ |
|||
var user = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
var providerName = UserManager.Options.Tokens.PasswordResetTokenProvider; |
|||
var tokenKey = UserManager<IdentityUser>.ResetPasswordTokenPurpose + AbpSingleActiveTokenProvider.TokenHashSuffix; |
|||
|
|||
await UserManager.SetAuthenticationTokenAsync(user, providerName, tokenKey, "hash-value"); |
|||
(await UserManager.GetAuthenticationTokenAsync(user, providerName, tokenKey)).ShouldNotBeNull(); |
|||
|
|||
var result = await UserManager.RemovePasswordResetTokenAsync(user); |
|||
result.Succeeded.ShouldBeTrue(); |
|||
|
|||
(await UserManager.GetAuthenticationTokenAsync(user, providerName, tokenKey)).ShouldBeNull(); |
|||
|
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Remove_EmailConfirmation_TokenHash() |
|||
{ |
|||
using (var uow = UnitOfWorkManager.Begin()) |
|||
{ |
|||
var user = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
var providerName = UserManager.Options.Tokens.EmailConfirmationTokenProvider; |
|||
var tokenKey = UserManager<IdentityUser>.ConfirmEmailTokenPurpose + AbpSingleActiveTokenProvider.TokenHashSuffix; |
|||
|
|||
await UserManager.SetAuthenticationTokenAsync(user, providerName, tokenKey, "hash-value"); |
|||
(await UserManager.GetAuthenticationTokenAsync(user, providerName, tokenKey)).ShouldNotBeNull(); |
|||
|
|||
var result = await UserManager.RemoveEmailConfirmationTokenAsync(user); |
|||
result.Succeeded.ShouldBeTrue(); |
|||
|
|||
(await UserManager.GetAuthenticationTokenAsync(user, providerName, tokenKey)).ShouldBeNull(); |
|||
|
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Remove_ChangeEmail_TokenHash() |
|||
{ |
|||
using (var uow = UnitOfWorkManager.Begin()) |
|||
{ |
|||
var user = await UserRepository.GetAsync(TestData.UserJohnId); |
|||
var providerName = UserManager.Options.Tokens.ChangeEmailTokenProvider; |
|||
var tokenKey = UserManager<IdentityUser>.GetChangeEmailTokenPurpose(NewEmail) + AbpSingleActiveTokenProvider.TokenHashSuffix; |
|||
|
|||
await UserManager.SetAuthenticationTokenAsync(user, providerName, tokenKey, "hash-value"); |
|||
(await UserManager.GetAuthenticationTokenAsync(user, providerName, tokenKey)).ShouldNotBeNull(); |
|||
|
|||
var result = await UserManager.RemoveChangeEmailTokenAsync(user, NewEmail); |
|||
result.Succeeded.ShouldBeTrue(); |
|||
|
|||
(await UserManager.GetAuthenticationTokenAsync(user, providerName, tokenKey)).ShouldBeNull(); |
|||
|
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue