From fa9e4f13cfc2ef61483a18772323e64d45e6f6c9 Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 21 Apr 2026 21:09:00 +0800 Subject: [PATCH] Support shared mode lookup by id for two-factor authentication user - Add IdentityUserManager.FindSharedUserByIdAsync to resolve a user by id across tenants in shared user sharing strategy - Override AbpSignInManager.GetTwoFactorAuthenticationUserAsync to use it so the 2FA mid-flow can still find a tenant-scoped user when CurrentTenant is host - Cover the new method with unit tests --- .../Identity/AspNetCore/AbpSignInManager.cs | 20 ++++++- .../Volo/Abp/Identity/IdentityUserManager.cs | 27 +++++++++ .../Abp/Identity/IdentityUserManager_Tests.cs | 60 +++++++++++++++++++ 3 files changed, 106 insertions(+), 1 deletion(-) diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs index 4b3147c14e..e6f56c8dec 100644 --- a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Security.Claims; +using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; @@ -133,6 +134,23 @@ public class AbpSignInManager : SignInManager return await IdentityUserManager.FindSharedUserByLoginAsync(loginProvider, providerKey); } + public override async Task GetTwoFactorAuthenticationUserAsync() + { + var result = await Context.AuthenticateAsync(IdentityConstants.TwoFactorUserIdScheme); + if (result?.Principal == null) + { + return null; + } + + var userId = result.Principal.FindFirstValue(ClaimTypes.Name); + if (string.IsNullOrWhiteSpace(userId)) + { + return null; + } + + return await IdentityUserManager.FindSharedUserByIdAsync(userId); + } + /// /// This is to call the protection method PreSignInCheck /// diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs index c2f72366fb..46f7a635f0 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs @@ -718,4 +718,31 @@ public class IdentityUserManager : UserManager, IDomainService } } + public virtual async Task FindSharedUserByIdAsync(string userId) + { + if (MultiTenancyOptions.Value.UserSharingStrategy == TenantUserSharingStrategy.Isolated) + { + return await base.FindByIdAsync(userId); + } + + using (CurrentTenant.Change(null)) + { + using (DataFilter.Disable()) + { + var user = await base.FindByIdAsync(userId); + if (user == null) + { + return null; + } + + using (DataFilter.Enable()) + { + using (CurrentTenant.Change(user.TenantId)) + { + return await base.FindByIdAsync(userId); + } + } + } + } + } } diff --git a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs index 48da394a05..a62a9cdff8 100644 --- a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs @@ -618,6 +618,66 @@ public class SharedTenantUserSharingStrategy_IdentityUserManager_Tests : AbpIden } } + [Fact] + public async Task FindSharedUserByIdAsync_Should_Find_Tenant_User_From_Host_Context() + { + var tenantId = Guid.NewGuid(); + IdentityUser tenantUser; + + using (var uow = _unitOfWorkManager.Begin()) + { + tenantUser = await CreateUserAsync(tenantId, "shared-id-tenant-only", "shared-id-tenant-only@abp.io"); + await uow.CompleteAsync(); + } + + // Simulates the 2FA mid-flow on a Shared deployment: CurrentTenant is null + // but the user row only exists under a tenant. FindByIdAsync alone would be + // filtered out by the IMultiTenant filter, so FindSharedUserByIdAsync must + // disable the filter and still return the tenant user. + using (_currentTenant.Change(null)) + { + var user = await _identityUserManager.FindSharedUserByIdAsync(tenantUser.Id.ToString()); + + user.ShouldNotBeNull(); + user.Id.ShouldBe(tenantUser.Id); + user.TenantId.ShouldBe(tenantId); + user.UserName.ShouldBe("shared-id-tenant-only"); + } + } + + [Fact] + public async Task FindSharedUserByIdAsync_Should_Find_Host_User_From_Tenant_Context() + { + var tenantId = Guid.NewGuid(); + IdentityUser hostUser; + + using (var uow = _unitOfWorkManager.Begin()) + { + hostUser = await CreateUserAsync(null, "shared-id-host-only", "shared-id-host-only@abp.io"); + await uow.CompleteAsync(); + } + + using (_currentTenant.Change(tenantId)) + { + var user = await _identityUserManager.FindSharedUserByIdAsync(hostUser.Id.ToString()); + + user.ShouldNotBeNull(); + user.Id.ShouldBe(hostUser.Id); + user.TenantId.ShouldBeNull(); + user.UserName.ShouldBe("shared-id-host-only"); + } + } + + [Fact] + public async Task FindSharedUserByIdAsync_Should_Return_Null_For_Unknown_Id() + { + using (_currentTenant.Change(null)) + { + var user = await _identityUserManager.FindSharedUserByIdAsync(Guid.NewGuid().ToString()); + user.ShouldBeNull(); + } + } + private async Task CreateUserAsync( Guid? tenantId, string userName,