From db07204b7ad2c6c4ccf03d2b4b3a83508a6e871c Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 22 Apr 2026 10:51:08 +0800 Subject: [PATCH] Make UserManager.FindByIdAsync shared-aware by default - Override IdentityUserManager.FindByIdAsync to fall back to a cross-tenant lookup in shared user sharing strategy so any caller that hits FindByIdAsync from a non-matching tenant context (including base SignInManager internals for TwoFactorSignInAsync and TwoFactorRecoveryCodeSignInAsync) can still resolve a tenant user by id - Drop the now-redundant AbpSignInManager.GetTwoFactorAuthenticationUserAsync override; the base implementation works automatically through the new FindByIdAsync behavior - Cover the new FindByIdAsync behavior with unit tests --- .../Identity/AspNetCore/AbpSignInManager.cs | 20 +------- .../Volo/Abp/Identity/IdentityUserManager.cs | 17 +++++++ .../Abp/Identity/IdentityUserManager_Tests.cs | 49 +++++++++++++++++++ 3 files changed, 67 insertions(+), 19 deletions(-) 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 e6f56c8dec..4b3147c14e 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,5 +1,4 @@ -using System.Security.Claims; -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; @@ -134,23 +133,6 @@ 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 46f7a635f0..c0470eef80 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 @@ -745,4 +745,21 @@ public class IdentityUserManager : UserManager, IDomainService } } } + + public override async Task FindByIdAsync(string userId) + { + // In shared user sharing strategy, a user id is globally unique. Fall back to + // a cross-tenant lookup so callers that hit FindByIdAsync from a non-matching + // tenant context (notably the 2FA mid-flow invoked by ASP.NET Core Identity + // base SignInManager paths) can still resolve the user by id. + // Downstream operations on the returned entity should still be scoped to + // user.TenantId explicitly via CurrentTenant.Change. + var user = await base.FindByIdAsync(userId); + if (user != null || MultiTenancyOptions.Value.UserSharingStrategy == TenantUserSharingStrategy.Isolated) + { + return user; + } + + return await FindSharedUserByIdAsync(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 1982080cf1..803bc258a3 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 @@ -678,6 +678,55 @@ public class SharedTenantUserSharingStrategy_IdentityUserManager_Tests : AbpIden } } + [Fact] + public async Task FindByIdAsync_Should_Fall_Back_To_Cross_Tenant_Lookup_In_Shared_Mode() + { + // In shared user sharing strategy, UserManager.FindByIdAsync must resolve a + // tenant user even when CurrentTenant is host. This protects any caller that + // hits FindByIdAsync without an explicit shared-aware wrapper (e.g. ASP.NET + // Core Identity SignInManager internals for TwoFactorSignInAsync and + // TwoFactorRecoveryCodeSignInAsync). + var tenantId = Guid.NewGuid(); + IdentityUser tenantUser; + + using (var uow = _unitOfWorkManager.Begin()) + { + tenantUser = await CreateUserAsync(tenantId, "shared-findbyid-fallback", "shared-findbyid-fallback@abp.io"); + await uow.CompleteAsync(); + } + + using (_currentTenant.Change(null)) + { + var user = await _identityUserManager.FindByIdAsync(tenantUser.Id.ToString()); + + user.ShouldNotBeNull(); + user.Id.ShouldBe(tenantUser.Id); + user.TenantId.ShouldBe(tenantId); + } + } + + [Fact] + public async Task FindByIdAsync_Should_Return_Current_Tenant_Result_Without_Fallback_When_Found() + { + var tenantId = Guid.NewGuid(); + IdentityUser tenantUser; + + using (var uow = _unitOfWorkManager.Begin()) + { + tenantUser = await CreateUserAsync(tenantId, "shared-findbyid-hit", "shared-findbyid-hit@abp.io"); + await uow.CompleteAsync(); + } + + using (_currentTenant.Change(tenantId)) + { + var user = await _identityUserManager.FindByIdAsync(tenantUser.Id.ToString()); + + user.ShouldNotBeNull(); + user.Id.ShouldBe(tenantUser.Id); + user.TenantId.ShouldBe(tenantId); + } + } + [Fact] public async Task Login_Then_TwoFactor_MidFlow_Should_Resolve_Same_Tenant_User_In_Shared_Mode() {