Browse Source

Merge pull request #25450 from abpframework/enhance-link-user

Enhance LinkUserTokenProvider with single-active policy and consent helpers
pull/25517/head
Alper Ebiçoğlu 5 months ago
committed by GitHub
parent
commit
c02d7effce
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 13
      modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpLinkUserTokenProviderOptions.cs
  2. 19
      modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/IdentityUserManagerSingleActiveTokenExtensions.cs
  3. 19
      modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/LinkUserTokenProvider.cs
  4. 4
      modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/LinkUserTokenProviderConsts.cs
  5. 54
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityLinkUserManager.cs
  6. 108
      modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/LinkUserTokenProvider_Tests.cs
  7. 95
      modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityLinkUserManager_Tests.cs

13
modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpLinkUserTokenProviderOptions.cs

@ -0,0 +1,13 @@
using System;
using Microsoft.AspNetCore.Identity;
namespace Volo.Abp.Identity.AspNetCore;
public class AbpLinkUserTokenProviderOptions : DataProtectionTokenProviderOptions
{
public AbpLinkUserTokenProviderOptions()
{
Name = LinkUserTokenProviderConsts.LinkUserTokenProviderName;
TokenLifespan = TimeSpan.FromMinutes(10);
}
}

19
modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/IdentityUserManagerSingleActiveTokenExtensions.cs

@ -40,4 +40,23 @@ public static class IdentityUserManagerSingleActiveTokenExtensions
var name = manager.Options.Tokens.ChangeEmailTokenProvider + ":" + UserManager<IdentityUser>.GetChangeEmailTokenPurpose(newEmail);
return manager.RemoveAuthenticationTokenAsync(user, AbpSingleActiveTokenProvider.InternalLoginProvider, name);
}
/// <summary>
/// Removes the stored link-user token hash for <paramref name="user"/>,
/// immediately invalidating any previously issued link-user token.
/// </summary>
public static Task<IdentityResult> RemoveLinkUserTokenAsync(this IdentityUserManager manager, IdentityUser user)
{
return RemoveLinkUserTokenAsync(manager, user, LinkUserTokenProviderConsts.LinkUserTokenPurpose);
}
/// <summary>
/// Removes the stored link-user token hash for <paramref name="user"/> and the given <paramref name="purpose"/>,
/// immediately invalidating any previously issued link-user token for that purpose.
/// </summary>
public static Task<IdentityResult> RemoveLinkUserTokenAsync(this IdentityUserManager manager, IdentityUser user, string purpose)
{
var name = LinkUserTokenProviderConsts.LinkUserTokenProviderName + ":" + purpose;
return manager.RemoveAuthenticationTokenAsync(user, AbpSingleActiveTokenProvider.InternalLoginProvider, name);
}
}

19
modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/LinkUserTokenProvider.cs

@ -1,18 +1,25 @@
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.Identity;
using Volo.Abp.Threading;
namespace Volo.Abp.Identity.AspNetCore;
public class LinkUserTokenProvider : DataProtectorTokenProvider<IdentityUser>
/// <summary>
/// Link-user token provider that enforces, per purpose, only the most recently issued
/// token to be valid, with a configurable expiration period.
/// </summary>
public class LinkUserTokenProvider : AbpSingleActiveTokenProvider
{
public LinkUserTokenProvider(
IDataProtectionProvider dataProtectionProvider,
IOptions<DataProtectionTokenProviderOptions> options,
ILogger<DataProtectorTokenProvider<IdentityUser>> logger)
: base(dataProtectionProvider, options, logger)
IOptions<AbpLinkUserTokenProviderOptions> options,
ILogger<DataProtectorTokenProvider<IdentityUser>> logger,
IIdentityUserRepository userRepository,
ICancellationTokenProvider cancellationTokenProvider)
: base(dataProtectionProvider, options, logger, userRepository, cancellationTokenProvider)
{
}
}

4
modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/LinkUserTokenProviderConsts.cs

@ -7,4 +7,8 @@ public static class LinkUserTokenProviderConsts
public static string LinkUserTokenPurpose { get; set; } = "AbpLinkUser";
public static string LinkUserLoginTokenPurpose { get; set; } = "AbpLinkUserLogin";
public static string LinkUserConsentLoginProvider { get; set; } = "[AbpLinkUserConsent]";
public static string LinkUserConsentTokenName { get; set; } = "Consent";
}

54
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityLinkUserManager.cs

@ -3,8 +3,10 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Volo.Abp.Domain.Services;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow;
namespace Volo.Abp.Identity;
@ -160,4 +162,56 @@ public class IdentityLinkUserManager : DomainService
token);
}
}
[UnitOfWork]
public virtual async Task SetLinkConsentAsync(IdentityLinkUserInfo sourceLinkUser, string consent, CancellationToken cancellationToken = default)
{
using (CurrentTenant.Change(sourceLinkUser.TenantId))
{
var user = await UserManager.FindByIdAsync(sourceLinkUser.UserId.ToString());
if (user == null)
{
return;
}
(await UserManager.SetAuthenticationTokenAsync(
user,
LinkUserTokenProviderConsts.LinkUserConsentLoginProvider,
LinkUserTokenProviderConsts.LinkUserConsentTokenName,
consent)).CheckErrors();
}
}
[UnitOfWork]
public virtual async Task<string?> GetLinkConsentAsync(IdentityLinkUserInfo sourceLinkUser, CancellationToken cancellationToken = default)
{
using (CurrentTenant.Change(sourceLinkUser.TenantId))
{
var user = await UserManager.FindByIdAsync(sourceLinkUser.UserId.ToString());
if (user == null)
{
return null;
}
return await UserManager.GetAuthenticationTokenAsync(
user,
LinkUserTokenProviderConsts.LinkUserConsentLoginProvider,
LinkUserTokenProviderConsts.LinkUserConsentTokenName);
}
}
[UnitOfWork]
public virtual async Task RemoveLinkConsentAsync(IdentityLinkUserInfo sourceLinkUser, CancellationToken cancellationToken = default)
{
using (CurrentTenant.Change(sourceLinkUser.TenantId))
{
var user = await UserManager.FindByIdAsync(sourceLinkUser.UserId.ToString());
if (user == null)
{
return;
}
(await UserManager.RemoveAuthenticationTokenAsync(
user,
LinkUserTokenProviderConsts.LinkUserConsentLoginProvider,
LinkUserTokenProviderConsts.LinkUserConsentTokenName)).CheckErrors();
}
}
}

108
modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/LinkUserTokenProvider_Tests.cs

@ -1,28 +1,40 @@
using System.Threading.Tasks;
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 LinkUserTokenProvider_Tests : AbpIdentityAspNetCoreTestBase
public class LinkUserTokenProvider_Tests : AbpSingleActiveTokenProviderTestBase
{
protected IIdentityUserRepository UserRepository { get; }
protected IIdentityLinkUserRepository IdentityLinkUserRepository { get; }
protected IdentityLinkUserManager IdentityLinkUserManager { get; }
protected IdentityTestData TestData { get; }
public LinkUserTokenProvider_Tests()
{
UserRepository = GetRequiredService<IIdentityUserRepository>();
IdentityLinkUserRepository = GetRequiredService<IIdentityLinkUserRepository>();
IdentityLinkUserManager = GetRequiredService<IdentityLinkUserManager>();
TestData = GetRequiredService<IdentityTestData>();
}
protected override Task<string> GenerateTokenAsync(IdentityUser user)
=> IdentityLinkUserManager.GenerateLinkTokenAsync(
new IdentityLinkUserInfo(user.Id, user.TenantId),
LinkUserTokenProviderConsts.LinkUserTokenPurpose);
protected override Task<bool> VerifyTokenAsync(IdentityUser user, string token)
=> IdentityLinkUserManager.VerifyLinkTokenAsync(
new IdentityLinkUserInfo(user.Id, user.TenantId),
token,
LinkUserTokenProviderConsts.LinkUserTokenPurpose);
protected override string GetProviderName()
=> LinkUserTokenProviderConsts.LinkUserTokenProviderName;
protected override string GetPurpose()
=> LinkUserTokenProviderConsts.LinkUserTokenPurpose;
[Fact]
public void LinkUserTokenProvider_Should_Be_Register()
public void LinkUserTokenProvider_Should_Be_Registered()
{
var identityOptions = GetRequiredService<IOptions<IdentityOptions>>().Value;
@ -35,11 +47,79 @@ public class LinkUserTokenProvider_Tests : AbpIdentityAspNetCoreTestBase
[InlineData("TestTokenPurpose1")]
[InlineData("TestTokenPurpose2")]
[InlineData("TestTokenPurpose3")]
public virtual async Task GenerateAndVerifyLinkTokenAsync(string tokenPurpose)
public virtual async Task GenerateAndVerifyLinkTokenAsync_With_Custom_Purpose(string tokenPurpose)
{
using (var uow = UnitOfWorkManager.Begin())
{
var john = await UserRepository.GetAsync(TestData.UserJohnId);
var token = await IdentityLinkUserManager.GenerateLinkTokenAsync(new IdentityLinkUserInfo(john.Id, john.TenantId), tokenPurpose);
(await IdentityLinkUserManager.VerifyLinkTokenAsync(new IdentityLinkUserInfo(john.Id, john.TenantId), token, tokenPurpose)).ShouldBeTrue();
(await IdentityLinkUserManager.VerifyLinkTokenAsync(new IdentityLinkUserInfo(john.Id, john.TenantId), "123123", tokenPurpose)).ShouldBeFalse();
await uow.CompleteAsync();
}
}
[Fact]
public async Task RemoveLinkUserTokenAsync_Should_Invalidate_Stored_Hash()
{
using (var uow = UnitOfWorkManager.Begin())
{
var john = await UserRepository.GetAsync(TestData.UserJohnId);
var token = await GenerateTokenAsync(john);
john = await UserRepository.GetAsync(TestData.UserJohnId);
(await UserManager.RemoveLinkUserTokenAsync(john)).Succeeded.ShouldBeTrue();
john = await UserRepository.GetAsync(TestData.UserJohnId);
(await VerifyTokenAsync(john, token)).ShouldBeFalse();
await uow.CompleteAsync();
}
}
[Fact]
public async Task RemoveLinkUserTokenAsync_With_Custom_Purpose_Should_Invalidate_Only_That_Purpose()
{
var john = await UserRepository.GetAsync(TestData.UserJohnId);
var token = await IdentityLinkUserManager.GenerateLinkTokenAsync(new IdentityLinkUserInfo(john.Id, john.TenantId), tokenPurpose);
(await IdentityLinkUserManager.VerifyLinkTokenAsync(new IdentityLinkUserInfo(john.Id, john.TenantId), token, tokenPurpose)).ShouldBeTrue();
(await IdentityLinkUserManager.VerifyLinkTokenAsync(new IdentityLinkUserInfo(john.Id, john.TenantId), "123123", tokenPurpose)).ShouldBeFalse();
const string customPurpose = "CustomLinkUserPurpose";
using (var uow = UnitOfWorkManager.Begin())
{
var john = await UserRepository.GetAsync(TestData.UserJohnId);
var defaultPurposeToken = await GenerateTokenAsync(john);
var customPurposeToken = await IdentityLinkUserManager.GenerateLinkTokenAsync(
new IdentityLinkUserInfo(john.Id, john.TenantId),
customPurpose);
john = await UserRepository.GetAsync(TestData.UserJohnId);
(await UserManager.RemoveLinkUserTokenAsync(john, customPurpose)).Succeeded.ShouldBeTrue();
john = await UserRepository.GetAsync(TestData.UserJohnId);
(await IdentityLinkUserManager.VerifyLinkTokenAsync(new IdentityLinkUserInfo(john.Id, john.TenantId), customPurposeToken, customPurpose)).ShouldBeFalse();
(await VerifyTokenAsync(john, defaultPurposeToken)).ShouldBeTrue();
await uow.CompleteAsync();
}
}
[Fact]
public async Task GenerateLinkTokenAsync_With_Custom_Purpose_Should_Invalidate_Previous_Token_For_Same_Purpose()
{
const string customPurpose = "CustomLinkUserPurpose";
using (var uow = UnitOfWorkManager.Begin())
{
var john = await UserRepository.GetAsync(TestData.UserJohnId);
var firstToken = await IdentityLinkUserManager.GenerateLinkTokenAsync(
new IdentityLinkUserInfo(john.Id, john.TenantId),
customPurpose);
var secondToken = await IdentityLinkUserManager.GenerateLinkTokenAsync(
new IdentityLinkUserInfo(john.Id, john.TenantId),
customPurpose);
(await IdentityLinkUserManager.VerifyLinkTokenAsync(new IdentityLinkUserInfo(john.Id, john.TenantId), firstToken, customPurpose)).ShouldBeFalse();
(await IdentityLinkUserManager.VerifyLinkTokenAsync(new IdentityLinkUserInfo(john.Id, john.TenantId), secondToken, customPurpose)).ShouldBeTrue();
await uow.CompleteAsync();
}
}
}

95
modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityLinkUserManager_Tests.cs

@ -188,4 +188,99 @@ public class IdentityLinkUserManager_Tests : AbpIdentityDomainTestBase
(await IdentityLinkUserManager.IsLinkedAsync(new IdentityLinkUserInfo(john.Id, john.TenantId),
new IdentityLinkUserInfo(neo.Id, neo.TenantId))).ShouldBeFalse();
}
[Fact]
public virtual async Task SetLinkConsentAsync()
{
await UsingUowAsync(async () =>
{
var john = await UserRepository.GetAsync(TestData.UserJohnId);
var source = new IdentityLinkUserInfo(john.Id, john.TenantId);
await IdentityLinkUserManager.SetLinkConsentAsync(source, "consent-payload");
(await IdentityLinkUserManager.GetLinkConsentAsync(source)).ShouldBe("consent-payload");
});
}
[Fact]
public virtual async Task SetLinkConsentAsync_Should_Overwrite_Previous_Value()
{
await UsingUowAsync(async () =>
{
var john = await UserRepository.GetAsync(TestData.UserJohnId);
var source = new IdentityLinkUserInfo(john.Id, john.TenantId);
await IdentityLinkUserManager.SetLinkConsentAsync(source, "first");
await IdentityLinkUserManager.SetLinkConsentAsync(source, "second");
(await IdentityLinkUserManager.GetLinkConsentAsync(source)).ShouldBe("second");
});
}
[Fact]
public virtual async Task GetLinkConsentAsync_Should_Return_Null_When_No_Consent_Written()
{
var neo = await UserRepository.GetAsync(TestData.UserNeoId);
var stored = await IdentityLinkUserManager.GetLinkConsentAsync(new IdentityLinkUserInfo(neo.Id, neo.TenantId));
stored.ShouldBeNull();
}
[Fact]
public virtual async Task GetLinkConsentAsync_Should_Return_Null_When_User_Not_Found()
{
var stored = await IdentityLinkUserManager.GetLinkConsentAsync(new IdentityLinkUserInfo(Guid.NewGuid(), null));
stored.ShouldBeNull();
}
[Fact]
public virtual async Task RemoveLinkConsentAsync()
{
await UsingUowAsync(async () =>
{
var john = await UserRepository.GetAsync(TestData.UserJohnId);
var source = new IdentityLinkUserInfo(john.Id, john.TenantId);
await IdentityLinkUserManager.SetLinkConsentAsync(source, "payload");
await IdentityLinkUserManager.RemoveLinkConsentAsync(source);
(await IdentityLinkUserManager.GetLinkConsentAsync(source)).ShouldBeNull();
});
}
[Fact]
public virtual async Task RemoveLinkConsentAsync_Should_Be_Noop_When_User_Not_Found()
{
await IdentityLinkUserManager.RemoveLinkConsentAsync(new IdentityLinkUserInfo(Guid.NewGuid(), null));
}
[Fact]
public virtual async Task SetLinkConsentAsync_Should_Persist_Across_UnitOfWork()
{
var john = await UserRepository.GetAsync(TestData.UserJohnId);
var source = new IdentityLinkUserInfo(john.Id, john.TenantId);
await UsingUowAsync(async () =>
{
await IdentityLinkUserManager.SetLinkConsentAsync(source, "consent-payload");
});
await UsingUowAsync(async () =>
{
(await IdentityLinkUserManager.GetLinkConsentAsync(source)).ShouldBe("consent-payload");
});
await UsingUowAsync(async () =>
{
await IdentityLinkUserManager.RemoveLinkConsentAsync(source);
});
await UsingUowAsync(async () =>
{
(await IdentityLinkUserManager.GetLinkConsentAsync(source)).ShouldBeNull();
});
}
}

Loading…
Cancel
Save