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 eeaed97d31..34bf4e0c8d 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 @@ -115,12 +115,17 @@ public class AbpSignInManager : SignInManager return SignInResult.Failed; } - var error = await PreSignInCheck(user); - if (error != null) + using (CurrentTenant.Change(user.TenantId)) { - return error; + await IdentityOptionsAccessor.SetAsync(); + + var error = await PreSignInCheck(user); + if (error != null) + { + return error; + } + return await SignInOrTwoFactorAsync(user, isPersistent, loginProvider, bypassTwoFactor); } - return await SignInOrTwoFactorAsync(user, isPersistent, loginProvider, bypassTwoFactor); } public virtual async Task FindByEmailAsync(string email) @@ -165,6 +170,7 @@ public class AbpSignInManager : SignInManager using (CurrentTenant.Change(user.TenantId)) { + await IdentityOptionsAccessor.SetAsync(); return await base.TwoFactorSignInAsync(provider, code, isPersistent, rememberClient); } } @@ -179,6 +185,8 @@ public class AbpSignInManager : SignInManager using (CurrentTenant.Change(user.TenantId)) { + await IdentityOptionsAccessor.SetAsync(); + // Base TwoFactorRecoveryCodeSignInAsync does not invoke PreSignInCheck, which means // AbpSignInManager's IsActive / ShouldChangePassword checks would be bypassed. Run the // same pre-sign-in checks here so recovery-code sign-in has the same gating as the diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Isolated_TwoFactor_Tests.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Isolated_TwoFactor_Tests.cs new file mode 100644 index 0000000000..0ffcc2cf57 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Isolated_TwoFactor_Tests.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.Identity.AspNetCore; + +public class Isolated_TwoFactor_Tests : AbpIdentityAspNetCoreTestBase +{ + [Fact] + public async Task TwoFactorRecoveryCodeSignInAsync_Should_Return_NotAllowed_For_Inactive_User() + { + // The AbpSignInManager override adds PreSignInCheck to the recovery-code path (the base + // AspNetCore Identity implementation does not). This test asserts that behavior also works + // in the default (isolated) configuration so the new invariant is protected across modes. + var userManager = GetRequiredService(); + var userRepository = GetRequiredService(); + var unitOfWorkManager = GetRequiredService(); + + Guid userId; + using (var uow = unitOfWorkManager.Begin()) + { + var user = new IdentityUser(Guid.NewGuid(), "iso-recovery-inactive", "iso-recovery-inactive@abp.io"); + (await userManager.CreateAsync(user, "Iso!9Aa")).Succeeded.ShouldBeTrue(); + user.SetIsActive(false); + await userRepository.UpdateAsync(user); + userId = user.Id; + await uow.CompleteAsync(); + } + + var writeResponse = await Client.GetAsync($"/api/signin-test/write-two-factor-cookie?userId={userId}"); + writeResponse.EnsureSuccessStatusCode(); + if (writeResponse.Headers.TryGetValues("Set-Cookie", out var setCookies)) + { + Client.DefaultRequestHeaders.Remove("Cookie"); + foreach (var cookie in setCookies) + { + Client.DefaultRequestHeaders.Add("Cookie", cookie.Split(';').First()); + } + } + + var response = await Client.GetAsync("/api/signin-test/two-factor-recovery-signin?recoveryCode=invalid"); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadAsStringAsync(); + + result.ShouldBe("NotAllowed"); + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Shared_SignIn_Tests.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Shared_SignIn_Tests.cs new file mode 100644 index 0000000000..2f951345cc --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Shared_SignIn_Tests.cs @@ -0,0 +1,114 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Shouldly; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.Identity.AspNetCore; + +public class Shared_SignIn_Tests : SharedAbpIdentityAspNetCoreTestBase +{ + [Fact] + public async Task PasswordSignInAsync_Should_Sign_In_Tenant_User_From_Host_Context() + { + // In shared mode, calling PasswordSignInAsync with a username while CurrentTenant is host + // must resolve the tenant user via FindSharedUserByNameAsync, apply the user's tenant + // IdentityOptions (the fix added in AbpSignInManager), and complete the sign-in. + var userManager = GetRequiredService(); + var currentTenant = GetRequiredService(); + var unitOfWorkManager = GetRequiredService(); + + var tenantId = Guid.NewGuid(); + const string userName = "shared-password-signin"; + const string password = "Shared!9Aa"; + + using (var uow = unitOfWorkManager.Begin()) + { + using (currentTenant.Change(tenantId)) + { + var user = new IdentityUser(Guid.NewGuid(), userName, userName + "@abp.io", tenantId); + (await userManager.CreateAsync(user, password)).Succeeded.ShouldBeTrue(); + } + await uow.CompleteAsync(); + } + + var response = await Client.GetAsync($"/api/signin-test/password?userName={userName}&password={password}"); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadAsStringAsync(); + + result.ShouldBe("Succeeded"); + } + + [Fact] + public async Task PasswordSignInAsync_Should_Fail_For_Wrong_Password_In_Shared_Mode() + { + var userManager = GetRequiredService(); + var currentTenant = GetRequiredService(); + var unitOfWorkManager = GetRequiredService(); + + var tenantId = Guid.NewGuid(); + const string userName = "shared-password-wrong"; + + using (var uow = unitOfWorkManager.Begin()) + { + using (currentTenant.Change(tenantId)) + { + var user = new IdentityUser(Guid.NewGuid(), userName, userName + "@abp.io", tenantId); + (await userManager.CreateAsync(user, "Shared!9Aa")).Succeeded.ShouldBeTrue(); + } + await uow.CompleteAsync(); + } + + var response = await Client.GetAsync($"/api/signin-test/password?userName={userName}&password=wrong"); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadAsStringAsync(); + + result.ShouldBe("Failed"); + } + + [Fact] + public async Task ExternalLoginSignInAsync_Should_Sign_In_Tenant_User_From_Host_Context() + { + // Covers the AbpSignInManager.ExternalLoginSignInAsync override: finds the user via + // FindSharedUserByLoginAsync, switches CurrentTenant to user.TenantId, applies tenant + // IdentityOptions, then calls PreSignInCheck + SignInOrTwoFactorAsync. + const string loginProvider = "test-provider"; + var providerKey = "ext-" + Guid.NewGuid().ToString("N").Substring(0, 8); + + var userManager = GetRequiredService(); + var currentTenant = GetRequiredService(); + var unitOfWorkManager = GetRequiredService(); + + var tenantId = Guid.NewGuid(); + + using (var uow = unitOfWorkManager.Begin()) + { + using (currentTenant.Change(tenantId)) + { + var user = new IdentityUser(Guid.NewGuid(), "shared-external-signin", "shared-external-signin@abp.io", tenantId); + (await userManager.CreateAsync(user, "Shared!9Aa")).Succeeded.ShouldBeTrue(); + (await userManager.AddLoginAsync(user, new UserLoginInfo(loginProvider, providerKey, "Test Provider"))).Succeeded.ShouldBeTrue(); + } + await uow.CompleteAsync(); + } + + var response = await Client.GetAsync($"/api/signin-test/external-login-signin?loginProvider={loginProvider}&providerKey={providerKey}"); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadAsStringAsync(); + + result.ShouldBe("Succeeded"); + } + + [Fact] + public async Task ExternalLoginSignInAsync_Should_Fail_For_Unknown_Provider_Key_In_Shared_Mode() + { + var response = await Client.GetAsync($"/api/signin-test/external-login-signin?loginProvider=unknown&providerKey=none"); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadAsStringAsync(); + + result.ShouldBe("Failed"); + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SignInTestController.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SignInTestController.cs index 8922915e6d..b2381a6a53 100644 --- a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SignInTestController.cs +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SignInTestController.cs @@ -59,4 +59,11 @@ public class SignInTestController : AbpController var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode); return Content(result.ToString()); } + + [Route("external-login-signin")] + public async Task ExternalLoginSignIn(string loginProvider, string providerKey) + { + var result = await _signInManager.ExternalLoginSignInAsync(loginProvider, providerKey, false, false); + return Content(result.ToString()); + } }