From 7b24d3d49f085dd0725344e6f6aa4d94cee79c39 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 20 Aug 2026 16:51:56 +0800 Subject: [PATCH 1/2] Reject invalid identity session cookies instead of signing them out --- ...eDynamicClaimsPrincipalContributorCache.cs | 25 +++- .../Client/AbpAspNetCoreMvcClientModule.cs | 6 + ...msCookieAuthenticationOptionsExtensions.cs | 104 +++++++++++++++++ .../CookieAuthenticationOptionsExtensions.cs | 43 +++++-- ...amicClaimsPrincipalContributorCacheBase.cs | 9 +- ...ieAuthenticationOptionsExtensions_Tests.cs | 109 ++++++++++++++++++ .../AspNetCore/AbpIdentityAspNetCoreModule.cs | 6 + ...onCookieAuthenticationOptionsExtensions.cs | 66 +++++++++++ ...IdentitySessionValidationResultAccessor.cs | 46 ++++++++ .../Abp/Identity/IIdentitySessionChecker.cs | 8 ++ ...IdentitySessionValidationResultAccessor.cs | 8 ++ .../Identity/NullIdentitySessionChecker.cs | 12 ++ ...IdentitySessionValidationResultAccessor.cs | 17 +++ .../AbpIdentityAspNetCoreTestModule.cs | 6 + .../AspNetCore/FakeIdentitySessionChecker.cs | 35 ++++++ .../IdentitySessionCookieValidation_Tests.cs | 108 +++++++++++++++++ .../IdentitySessionSlidingRenewal_Tests.cs | 83 +++++++++++++ .../ShortLivedCookieIdentityTestModule.cs | 26 +++++ .../ShortLivedCookieIdentityTestStartup.cs | 18 +++ .../AspNetCore/SignInTestController.cs | 19 +++ .../Identity/AspNetCore/TestTimeProvider.cs | 18 +++ 21 files changed, 755 insertions(+), 17 deletions(-) create mode 100644 framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/AbpRemoteDynamicClaimsCookieAuthenticationOptionsExtensions.cs create mode 100644 framework/test/Volo.Abp.AspNetCore.Mvc.Client.Tests/Volo/Abp/AspNetCore/Mvc/Client/AbpRemoteDynamicClaimsCookieAuthenticationOptionsExtensions_Tests.cs create mode 100644 modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentitySessionCookieAuthenticationOptionsExtensions.cs create mode 100644 modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/HttpContextIdentitySessionValidationResultAccessor.cs create mode 100644 modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentitySessionChecker.cs create mode 100644 modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentitySessionValidationResultAccessor.cs create mode 100644 modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/NullIdentitySessionChecker.cs create mode 100644 modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/NullIdentitySessionValidationResultAccessor.cs create mode 100644 modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/FakeIdentitySessionChecker.cs create mode 100644 modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/IdentitySessionCookieValidation_Tests.cs create mode 100644 modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/IdentitySessionSlidingRenewal_Tests.cs create mode 100644 modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/ShortLivedCookieIdentityTestModule.cs create mode 100644 modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/ShortLivedCookieIdentityTestStartup.cs create mode 100644 modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/TestTimeProvider.cs diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteDynamicClaimsPrincipalContributorCache.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteDynamicClaimsPrincipalContributorCache.cs index ba42b55d18..8f81b5aab4 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteDynamicClaimsPrincipalContributorCache.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteDynamicClaimsPrincipalContributorCache.cs @@ -1,5 +1,6 @@ using System; using System.Net.Http; +using System.Net.Http.Headers; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -46,20 +47,38 @@ public class RemoteDynamicClaimsPrincipalContributorCache : RemoteDynamicClaimsP return await Cache.GetAsync(AbpDynamicClaimCacheItem.CalculateCacheKey(userId, tenantId)); } - protected async override Task RefreshAsync(Guid userId, Guid? tenantId = null) + public virtual Task GetAsync(Guid userId, Guid? tenantId, string accessToken) + { + return GetAsync(userId, tenantId, () => RefreshAsync(userId, tenantId, accessToken)); + } + + protected override Task RefreshAsync(Guid userId, Guid? tenantId = null) + { + return RefreshAsync(userId, tenantId, null); + } + + protected virtual async Task RefreshAsync(Guid userId, Guid? tenantId, string? accessToken) { try { var client = HttpClientFactory.CreateClient(HttpClientName); var requestMessage = new HttpRequestMessage(HttpMethod.Post, AbpClaimsPrincipalFactoryOptions.Value.RemoteRefreshUrl); - await HttpClientAuthenticator.Authenticate(new RemoteServiceHttpClientAuthenticateContext(client, requestMessage, new RemoteServiceConfiguration("/"), string.Empty)); + if (accessToken != null) + { + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + } + else + { + await HttpClientAuthenticator.Authenticate(new RemoteServiceHttpClientAuthenticateContext(client, requestMessage, new RemoteServiceConfiguration("/"), string.Empty)); + } + var response = await client.SendAsync(requestMessage); response.EnsureSuccessStatusCode(); } catch (Exception e) { Logger.LogWarning(e, $"Failed to refresh remote claims for user: {userId}"); - await ApplicationConfigurationDtoCache.RemoveAsync(await CacheHelper.CreateCacheKeyAsync(CurrentUser.Id)); + await ApplicationConfigurationDtoCache.RemoveAsync(await CacheHelper.CreateCacheKeyAsync(CurrentUser.Id ?? userId)); throw; } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/AbpAspNetCoreMvcClientModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/AbpAspNetCoreMvcClientModule.cs index a22e68b7b9..3b4f82ca57 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/AbpAspNetCoreMvcClientModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/AbpAspNetCoreMvcClientModule.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.EventBus; using Volo.Abp.Modularity; @@ -22,4 +23,9 @@ public class AbpAspNetCoreMvcClientModule : AbpModule }); } } + + public override void PostConfigureServices(ServiceConfigurationContext context) + { + context.Services.PostConfigureAll(cookieOptions => cookieOptions.ValidateRemoteDynamicClaims()); + } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/AbpRemoteDynamicClaimsCookieAuthenticationOptionsExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/AbpRemoteDynamicClaimsCookieAuthenticationOptionsExtensions.cs new file mode 100644 index 0000000000..4887429898 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/AbpRemoteDynamicClaimsCookieAuthenticationOptionsExtensions.cs @@ -0,0 +1,104 @@ +using System; +using System.Linq; +using System.Security.Claims; +using System.Security.Principal; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Security.Claims; + +namespace Volo.Abp.AspNetCore.Mvc.Client; + +public static class AbpRemoteDynamicClaimsCookieAuthenticationOptionsExtensions +{ + public static CookieAuthenticationOptions ValidateRemoteDynamicClaims(this CookieAuthenticationOptions options) + { + var previousOnCheckSlidingExpiration = options.Events.OnCheckSlidingExpiration; + options.Events.OnCheckSlidingExpiration = async cookieSlidingExpirationContext => + { + await previousOnCheckSlidingExpiration(cookieSlidingExpirationContext); + + if (cookieSlidingExpirationContext.ShouldRenew && + cookieSlidingExpirationContext.Principal != null && + !await AreRemoteDynamicClaimsValidAsync(cookieSlidingExpirationContext.HttpContext, cookieSlidingExpirationContext.Scheme.Name, cookieSlidingExpirationContext.Principal, cookieSlidingExpirationContext.Properties)) + { + cookieSlidingExpirationContext.ShouldRenew = false; + } + }; + + var previousOnValidatePrincipal = options.Events.OnValidatePrincipal; + options.Events.OnValidatePrincipal = async cookieValidatePrincipalContext => + { + await previousOnValidatePrincipal(cookieValidatePrincipalContext); + + if (cookieValidatePrincipalContext.Principal != null && + !await AreRemoteDynamicClaimsValidAsync(cookieValidatePrincipalContext.HttpContext, cookieValidatePrincipalContext.Scheme.Name, cookieValidatePrincipalContext.Principal, cookieValidatePrincipalContext.Properties)) + { + cookieValidatePrincipalContext.ShouldRenew = false; + cookieValidatePrincipalContext.RejectPrincipal(); + } + }; + + return options; + } + + private static async Task AreRemoteDynamicClaimsValidAsync(HttpContext httpContext, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) + { + if (scheme == IdentityConstants.ExternalScheme || + scheme == IdentityConstants.TwoFactorUserIdScheme || + scheme == IdentityConstants.TwoFactorRememberMeScheme) + { + return true; + } + + var identity = principal.Identities.FirstOrDefault(); + var userId = identity?.FindUserId(); + if (userId == null) + { + return true; + } + + var abpClaimsPrincipalFactoryOptions = httpContext.RequestServices.GetRequiredService>().Value; + if (!abpClaimsPrincipalFactoryOptions.IsDynamicClaimsEnabled || !abpClaimsPrincipalFactoryOptions.IsRemoteRefreshEnabled) + { + return true; + } + + var accessToken = properties.GetTokenValue("access_token"); + if (accessToken.IsNullOrWhiteSpace()) + { + return true; + } + + var dynamicClaimsCache = httpContext.RequestServices.GetService(); + if (dynamicClaimsCache == null) + { + return true; + } + + try + { + // The multi-tenancy middleware hasn't resolved the tenant yet, but the dynamic claims are cached per tenant. + var tenantId = identity!.FindTenantId(); + using (httpContext.RequestServices.GetRequiredService().Change(tenantId)) + { + await dynamicClaimsCache.GetAsync(userId.Value, tenantId, accessToken); + } + + return true; + } + catch (Exception e) + { + httpContext.RequestServices + .GetRequiredService>() + .LogWarning(e, $"Failed to refresh remote dynamic claims for user: {userId.Value}, the authentication cookie is rejected."); + return false; + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs b/framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs index 85cc987b61..69ba96025c 100644 --- a/framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs @@ -20,6 +20,18 @@ public static class CookieAuthenticationOptionsExtensions { advance ??= TimeSpan.FromMinutes(3); validationInterval ??= TimeSpan.FromMinutes(1); + + var previousOnCheckSlidingExpiration = options.Events.OnCheckSlidingExpiration; + options.Events.OnCheckSlidingExpiration = async slidingExpirationContext => + { + await previousOnCheckSlidingExpiration(slidingExpirationContext); + + if (slidingExpirationContext.ShouldRenew && IsAccessTokenExpired(slidingExpirationContext.Properties, advance.Value)) + { + slidingExpirationContext.ShouldRenew = false; + } + }; + var previousHandler = options.Events.OnValidatePrincipal; options.Events.OnValidatePrincipal = async principalContext => { @@ -31,12 +43,10 @@ public static class CookieAuthenticationOptionsExtensions var logger = principalContext.HttpContext.RequestServices.GetRequiredService>(); - var tokenExpiresAt = principalContext.Properties.GetString(".Token.expires_at"); - if (!tokenExpiresAt.IsNullOrWhiteSpace() && DateTimeOffset.TryParseExact(tokenExpiresAt, "o", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var expiresAt) && - expiresAt <= DateTimeOffset.UtcNow.Add(advance.Value)) + if (IsAccessTokenExpired(principalContext.Properties, advance.Value)) { - logger.LogInformation("The access_token expires within {AdvanceSeconds}s; signing out.", advance.Value.TotalSeconds); - await SignOutAndInvokePreviousHandlerAsync(principalContext, previousHandler); + logger.LogInformation("The access_token expires within {AdvanceSeconds}s; rejecting the principal.", advance.Value.TotalSeconds); + await RejectPrincipalAndInvokePreviousHandlerAsync(principalContext, previousHandler); return; } @@ -74,14 +84,14 @@ public static class CookieAuthenticationOptionsExtensions if (response.IsError) { logger.LogError("Token introspection error: {Error}", response.Error); - await SignOutAndInvokePreviousHandlerAsync(principalContext, previousHandler); + await RejectPrincipalAndInvokePreviousHandlerAsync(principalContext, previousHandler); return; } if (!response.IsActive) { logger.LogError("The access_token is not active."); - await SignOutAndInvokePreviousHandlerAsync(principalContext, previousHandler); + await RejectPrincipalAndInvokePreviousHandlerAsync(principalContext, previousHandler); return; } @@ -91,7 +101,7 @@ public static class CookieAuthenticationOptionsExtensions else { logger.LogError("The access_token is not found in the cookie properties. Ensure SaveTokens of OpenIdConnectOptions is true."); - await SignOutAsync(principalContext); + await RejectPrincipalAsync(principalContext); } } @@ -113,10 +123,19 @@ public static class CookieAuthenticationOptionsExtensions return openIdConnectOptions; } - private static async Task SignOutAsync(CookieValidatePrincipalContext principalContext) + private static bool IsAccessTokenExpired(AuthenticationProperties properties, TimeSpan advance) + { + var tokenExpiresAt = properties.GetString(".Token.expires_at"); + return !tokenExpiresAt.IsNullOrWhiteSpace() && + DateTimeOffset.TryParseExact(tokenExpiresAt, "o", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var expiresAt) && + expiresAt <= DateTimeOffset.UtcNow.Add(advance); + } + + private static Task RejectPrincipalAsync(CookieValidatePrincipalContext principalContext) { + principalContext.ShouldRenew = false; principalContext.RejectPrincipal(); - await principalContext.HttpContext.SignOutAsync(principalContext.Scheme.Name); + return Task.CompletedTask; } private static Task InvokePreviousHandlerAsync(CookieValidatePrincipalContext principalContext, Func? previousHandler) @@ -124,9 +143,9 @@ public static class CookieAuthenticationOptionsExtensions return previousHandler != null ? previousHandler(principalContext) : Task.CompletedTask; } - private static async Task SignOutAndInvokePreviousHandlerAsync(CookieValidatePrincipalContext principalContext, Func? previousHandler) + private static async Task RejectPrincipalAndInvokePreviousHandlerAsync(CookieValidatePrincipalContext principalContext, Func? previousHandler) { - await SignOutAsync(principalContext); + await RejectPrincipalAsync(principalContext); await InvokePreviousHandlerAsync(principalContext, previousHandler); } } diff --git a/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/RemoteDynamicClaimsPrincipalContributorCacheBase.cs b/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/RemoteDynamicClaimsPrincipalContributorCacheBase.cs index 71fdbf0c17..e8cedc77e9 100644 --- a/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/RemoteDynamicClaimsPrincipalContributorCacheBase.cs +++ b/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/RemoteDynamicClaimsPrincipalContributorCacheBase.cs @@ -19,7 +19,12 @@ public abstract class RemoteDynamicClaimsPrincipalContributorCacheBase.Instance; } - public async Task GetAsync(Guid userId, Guid? tenantId = null) + public Task GetAsync(Guid userId, Guid? tenantId = null) + { + return GetAsync(userId, tenantId, () => RefreshAsync(userId, tenantId)); + } + + protected virtual async Task GetAsync(Guid userId, Guid? tenantId, Func refresh) { Logger.LogDebug($"Get dynamic claims cache for user: {userId}"); var dynamicClaims = await GetCacheAsync(userId, tenantId); @@ -31,7 +36,7 @@ public abstract class RemoteDynamicClaimsPrincipalContributorCacheBase(); + httpClientFactory.CreateClient(Arg.Any()).Returns(_ => new HttpClient(_remoteRefreshResponseHandler) + { + BaseAddress = new Uri("https://localhost/") + }); + + services.Replace(ServiceDescriptor.Singleton(httpClientFactory)); + services.Replace(ServiceDescriptor.Transient(_ => Substitute.For())); + services.Configure(options => + { + options.IsDynamicClaimsEnabled = true; + }); + } + + [Fact] + public async Task Should_Reject_The_Principal_When_The_Remote_Refresh_Is_Rejected() + { + var context = await ValidatePrincipalAsync(Guid.NewGuid()); + + context.Principal.ShouldBeNull(); + context.ShouldRenew.ShouldBeFalse(); + _remoteRefreshResponseHandler.ReceivedAccessTokens.ShouldBe(new[] { "test-access-token" }); + } + + [Fact] + public async Task Should_Keep_The_Principal_When_The_Dynamic_Claims_Are_Cached() + { + var userId = Guid.NewGuid(); + await GetRequiredService>().SetAsync( + AbpDynamicClaimCacheItem.CalculateCacheKey(userId, null), + new AbpDynamicClaimCacheItem()); + + var context = await ValidatePrincipalAsync(userId); + + context.Principal.ShouldNotBeNull(); + _remoteRefreshResponseHandler.ReceivedAccessTokens.ShouldBeEmpty(); + } + + [Fact] + public async Task Should_Keep_The_Principal_When_There_Is_No_Access_Token() + { + var context = await ValidatePrincipalAsync(Guid.NewGuid(), accessToken: null); + + context.Principal.ShouldNotBeNull(); + _remoteRefreshResponseHandler.ReceivedAccessTokens.ShouldBeEmpty(); + } + + private async Task ValidatePrincipalAsync(Guid userId, string accessToken = "test-access-token") + { + var options = new CookieAuthenticationOptions().ValidateRemoteDynamicClaims(); + + var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme, AbpClaimTypes.UserName, AbpClaimTypes.Role); + identity.AddClaim(new Claim(AbpClaimTypes.UserId, userId.ToString())); + identity.AddClaim(new Claim(AbpClaimTypes.UserName, "john")); + + var properties = new AuthenticationProperties(); + if (accessToken != null) + { + properties.StoreTokens(new[] { new AuthenticationToken { Name = "access_token", Value = accessToken } }); + } + + var context = new CookieValidatePrincipalContext( + new DefaultHttpContext { RequestServices = ServiceProvider }, + new AuthenticationScheme(CookieAuthenticationDefaults.AuthenticationScheme, null, typeof(CookieAuthenticationHandler)), + options, + new AuthenticationTicket(new ClaimsPrincipal(identity), properties, CookieAuthenticationDefaults.AuthenticationScheme)); + + await options.Events.ValidatePrincipal(context); + return context; + } + + private class RemoteRefreshResponseHandler : HttpMessageHandler + { + public List ReceivedAccessTokens { get; } = new(); + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + ReceivedAccessTokens.Add(request.Headers.Authorization?.Parameter); + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.Unauthorized)); + } + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs index 195e82a9bc..91453af613 100644 --- a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -33,6 +34,8 @@ public class AbpIdentityAspNetCoreModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { + context.Services.AddHttpContextAccessor(); + Configure(options => { options.Tokens.PasswordResetTokenProvider = AbpPasswordResetTokenProvider.ProviderName; @@ -61,6 +64,9 @@ public class AbpIdentityAspNetCoreModule : AbpModule public override void PostConfigureServices(ServiceConfigurationContext context) { + context.Services.PostConfigure(IdentityConstants.ApplicationScheme, + cookieOptions => cookieOptions.ValidateIdentitySession()); + // Replace the default UserValidator with AbpIdentityUserValidator context.Services.RemoveAll(x => x.ServiceType == typeof(IUserValidator) && x.ImplementationType == typeof(UserValidator)); context.Services.AddAbpOptions() diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentitySessionCookieAuthenticationOptionsExtensions.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentitySessionCookieAuthenticationOptionsExtensions.cs new file mode 100644 index 0000000000..68228defd9 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentitySessionCookieAuthenticationOptionsExtensions.cs @@ -0,0 +1,66 @@ +using System; +using System.Security.Claims; +using System.Security.Principal; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Security.Claims; + +namespace Volo.Abp.Identity.AspNetCore; + +public static class AbpIdentitySessionCookieAuthenticationOptionsExtensions +{ + public static CookieAuthenticationOptions ValidateIdentitySession(this CookieAuthenticationOptions options) + { + var previousOnCheckSlidingExpiration = options.Events.OnCheckSlidingExpiration; + options.Events.OnCheckSlidingExpiration = async cookieSlidingExpirationContext => + { + await previousOnCheckSlidingExpiration(cookieSlidingExpirationContext); + + if (cookieSlidingExpirationContext.ShouldRenew && + !await IsIdentitySessionValidAsync(cookieSlidingExpirationContext.HttpContext, cookieSlidingExpirationContext.Principal)) + { + cookieSlidingExpirationContext.ShouldRenew = false; + } + }; + + var previousOnValidatePrincipal = options.Events.OnValidatePrincipal; + options.Events.OnValidatePrincipal = async cookieValidatePrincipalContext => + { + await previousOnValidatePrincipal(cookieValidatePrincipalContext); + + if (cookieValidatePrincipalContext.Principal != null && + !await IsIdentitySessionValidAsync(cookieValidatePrincipalContext.HttpContext, cookieValidatePrincipalContext.Principal)) + { + cookieValidatePrincipalContext.ShouldRenew = false; + cookieValidatePrincipalContext.RejectPrincipal(); + } + }; + + return options; + } + + private static async Task IsIdentitySessionValidAsync(HttpContext httpContext, ClaimsPrincipal principal) + { + var sessionId = principal.FindSessionId(); + if (sessionId.IsNullOrWhiteSpace()) + { + return true; + } + + if (!httpContext.RequestServices.GetRequiredService>().Value.IsDynamicClaimsEnabled) + { + return true; + } + + var currentTenant = httpContext.RequestServices.GetRequiredService(); + var identitySessionChecker = httpContext.RequestServices.GetRequiredService(); + using (currentTenant.Change(principal.FindTenantId())) + { + return await identitySessionChecker.IsValidateAsync(sessionId); + } + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/HttpContextIdentitySessionValidationResultAccessor.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/HttpContextIdentitySessionValidationResultAccessor.cs new file mode 100644 index 0000000000..8c0e12fedf --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/HttpContextIdentitySessionValidationResultAccessor.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Identity.AspNetCore; + +[Dependency(ReplaceServices = true)] +public class HttpContextIdentitySessionValidationResultAccessor : IIdentitySessionValidationResultAccessor, ITransientDependency +{ + public const string HttpContextItemName = "__AbpIdentitySessionValidationResults"; + + private readonly IHttpContextAccessor _httpContextAccessor; + + public HttpContextIdentitySessionValidationResultAccessor(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public bool? GetOrNull(string sessionId) + { + return GetResults()?.TryGetValue(sessionId, out var isValid) == true ? isValid : null; + } + + public void Set(string sessionId, bool isValid) + { + var httpContext = _httpContextAccessor.HttpContext; + if (httpContext == null) + { + return; + } + + var results = GetResults(); + if (results == null) + { + results = new Dictionary(); + httpContext.Items[HttpContextItemName] = results; + } + + results[sessionId] = isValid; + } + + private Dictionary GetResults() + { + return _httpContextAccessor.HttpContext?.Items[HttpContextItemName] as Dictionary; + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentitySessionChecker.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentitySessionChecker.cs new file mode 100644 index 0000000000..bb10480a34 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentitySessionChecker.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.Identity; + +public interface IIdentitySessionChecker +{ + Task IsValidateAsync(string sessionId); +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentitySessionValidationResultAccessor.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentitySessionValidationResultAccessor.cs new file mode 100644 index 0000000000..89cf60a558 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentitySessionValidationResultAccessor.cs @@ -0,0 +1,8 @@ +namespace Volo.Abp.Identity; + +public interface IIdentitySessionValidationResultAccessor +{ + bool? GetOrNull(string sessionId); + + void Set(string sessionId, bool isValid); +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/NullIdentitySessionChecker.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/NullIdentitySessionChecker.cs new file mode 100644 index 0000000000..4d8c7bf050 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/NullIdentitySessionChecker.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Identity; + +public class NullIdentitySessionChecker : IIdentitySessionChecker, ISingletonDependency +{ + public Task IsValidateAsync(string sessionId) + { + return Task.FromResult(true); + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/NullIdentitySessionValidationResultAccessor.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/NullIdentitySessionValidationResultAccessor.cs new file mode 100644 index 0000000000..11056eea26 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/NullIdentitySessionValidationResultAccessor.cs @@ -0,0 +1,17 @@ +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Identity; + +public class NullIdentitySessionValidationResultAccessor : IIdentitySessionValidationResultAccessor, ISingletonDependency +{ + public static NullIdentitySessionValidationResultAccessor Instance { get; } = new(); + + public bool? GetOrNull(string sessionId) + { + return null; + } + + public void Set(string sessionId, bool isValid) + { + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreTestModule.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreTestModule.cs index 45b3756b42..a7dba2c7d0 100644 --- a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreTestModule.cs +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreTestModule.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.TestBase; using Volo.Abp.Modularity; +using Volo.Abp.Security.Claims; namespace Volo.Abp.Identity.AspNetCore; @@ -29,6 +30,11 @@ public class AbpIdentityAspNetCoreTestModule : AbpModule { options.ExternalLoginProviders.Add(FakeExternalLoginProvider.Name); }); + + Configure(options => + { + options.IsDynamicClaimsEnabled = true; + }); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/FakeIdentitySessionChecker.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/FakeIdentitySessionChecker.cs new file mode 100644 index 0000000000..0b4d91c586 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/FakeIdentitySessionChecker.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Security.Principal; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Security.Claims; + +namespace Volo.Abp.Identity.AspNetCore; + +[Dependency(ReplaceServices = true)] +public class FakeIdentitySessionChecker : IIdentitySessionChecker, ISingletonDependency +{ + public HashSet RevokedSessionIds { get; } = new(); + + public Task IsValidateAsync(string sessionId) + { + return Task.FromResult(!RevokedSessionIds.Contains(sessionId)); + } +} + +public class TestSessionIdClaimsPrincipalContributor : IAbpClaimsPrincipalContributor, ITransientDependency +{ + public Task ContributeAsync(AbpClaimsPrincipalContributorContext context) + { + var identity = context.ClaimsPrincipal.Identities.FirstOrDefault(); + if (identity != null && identity.FindSessionId() == null) + { + identity.AddClaim(new Claim(AbpClaimTypes.SessionId, Guid.NewGuid().ToString())); + } + + return Task.CompletedTask; + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/IdentitySessionCookieValidation_Tests.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/IdentitySessionCookieValidation_Tests.cs new file mode 100644 index 0000000000..7df806fb02 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/IdentitySessionCookieValidation_Tests.cs @@ -0,0 +1,108 @@ +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Identity; +using Shouldly; +using Xunit; + +namespace Volo.Abp.Identity.AspNetCore; + +public class IdentitySessionCookieValidation_Tests : AbpIdentityAspNetCoreTestBase +{ + [Fact] + public async Task Should_Authenticate_The_Cookie_When_The_Session_Is_Valid() + { + var cookie = await LoginAsync(); + + using (var response = await GetCurrentUserAsync(cookie)) + { + (await response.Content.ReadAsStringAsync()).ShouldStartWith("admin|"); + } + } + + [Fact] + public async Task Should_Reject_The_Cookie_Without_Writing_It_When_The_Session_Is_Revoked() + { + var cookie = await LoginAsync(); + var sessionId = await GetSessionIdAsync(cookie); + + GetRequiredService().RevokedSessionIds.Add(sessionId); + + using (var response = await GetCurrentUserAsync(cookie)) + { + (await response.Content.ReadAsStringAsync()).ShouldBe("anonymous"); + response.Headers.Contains("Set-Cookie").ShouldBeFalse(); + } + } + + [Fact] + public async Task Account_Switch_Cookie_Should_Survive_An_In_Flight_Request_Carrying_The_Old_Revoked_Cookie() + { + var oldCookie = await LoginAsync(); + var oldSessionId = await GetSessionIdAsync(oldCookie); + + var newCookie = await SwitchAccountAsync(oldCookie, "john.nash"); + newCookie.ShouldNotBeNull(); + newCookie.ShouldNotBe(oldCookie); + + // The switch revoked the old session. + GetRequiredService().RevokedSessionIds.Add(oldSessionId); + + // An in-flight request still carrying the old cookie must be rejected without emitting a + // delete-cookie that would wipe the freshly issued account-switch cookie. + using (var response = await GetCurrentUserAsync(oldCookie)) + { + (await response.Content.ReadAsStringAsync()).ShouldBe("anonymous"); + response.Headers.Contains("Set-Cookie").ShouldBeFalse(); + } + + using (var response = await GetCurrentUserAsync(newCookie)) + { + (await response.Content.ReadAsStringAsync()).ShouldStartWith("john.nash|"); + } + } + + private async Task SwitchAccountAsync(string cookie, string userName) + { + using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, "api/signin-test/switch-account?userName=" + userName)) + { + requestMessage.Headers.Add("Cookie", cookie); + using (var response = await Client.SendAsync(requestMessage)) + { + (await response.Content.ReadAsStringAsync()).ShouldBe("Succeeded"); + var cookieName = CookieAuthenticationDefaults.CookiePrefix + IdentityConstants.ApplicationScheme; + return response.Headers.GetValues("Set-Cookie") + .Select(x => x.Split(';')[0]) + .Last(x => x.StartsWith(cookieName) && x.Length > cookieName.Length + 1); + } + } + } + + private async Task LoginAsync() + { + using (var response = await Client.GetAsync("api/signin-test/password?userName=admin&password=1q2w3E*")) + { + (await response.Content.ReadAsStringAsync()).ShouldBe("Succeeded"); + var cookie = response.Headers.GetValues("Set-Cookie").First(x => x.StartsWith(CookieAuthenticationDefaults.CookiePrefix + IdentityConstants.ApplicationScheme)); + return cookie.Split(';')[0]; + } + } + + private async Task GetSessionIdAsync(string cookie) + { + using (var response = await GetCurrentUserAsync(cookie)) + { + return (await response.Content.ReadAsStringAsync()).Split('|')[1]; + } + } + + private async Task GetCurrentUserAsync(string cookie) + { + using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, "api/signin-test/current-user")) + { + requestMessage.Headers.Add("Cookie", cookie); + return await Client.SendAsync(requestMessage); + } + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/IdentitySessionSlidingRenewal_Tests.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/IdentitySessionSlidingRenewal_Tests.cs new file mode 100644 index 0000000000..d97a1d21b2 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/IdentitySessionSlidingRenewal_Tests.cs @@ -0,0 +1,83 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Identity; +using Shouldly; +using Volo.Abp.AspNetCore.TestBase; +using Xunit; + +namespace Volo.Abp.Identity.AspNetCore; + +public class IdentitySessionSlidingRenewal_Tests : AbpAspNetCoreIntegratedTestBase +{ + private static readonly string AuthenticationCookieName = CookieAuthenticationDefaults.CookiePrefix + IdentityConstants.ApplicationScheme; + + [Fact] + public async Task Should_Renew_The_Cookie_When_The_Session_Is_Valid_Past_The_Half_Life() + { + var cookie = await LoginAsync(); + + // Past the half-life of the 10s window, so a renewal is scheduled for a still-valid session. + GetRequiredService().Advance(TimeSpan.FromSeconds(6)); + + using (var response = await SendAsync("api/signin-test/current-user", cookie)) + { + (await response.Content.ReadAsStringAsync()).ShouldStartWith("admin|"); + GetAuthCookieHeader(response).ShouldNotBeNull(); + } + } + + [Fact] + public async Task Should_Not_Renew_The_Cookie_When_The_Session_Is_Already_Revoked_At_Auth_Time() + { + var cookie = await LoginAsync(); + var sessionId = await GetSessionIdAsync(cookie); + + GetRequiredService().RevokedSessionIds.Add(sessionId); + + // Same instant the valid session would renew at, so the missing renewal is the fix, not expiry. + GetRequiredService().Advance(TimeSpan.FromSeconds(6)); + + using (var response = await SendAsync("api/signin-test/current-user", cookie)) + { + (await response.Content.ReadAsStringAsync()).ShouldBe("anonymous"); + GetAuthCookieHeader(response).ShouldBeNull(); + } + } + + private async Task LoginAsync() + { + using (var response = await Client.GetAsync("api/signin-test/password?userName=admin&password=1q2w3E*")) + { + (await response.Content.ReadAsStringAsync()).ShouldBe("Succeeded"); + var cookie = response.Headers.GetValues("Set-Cookie").First(x => x.StartsWith(AuthenticationCookieName)); + return cookie.Split(';')[0]; + } + } + + private async Task GetSessionIdAsync(string cookie) + { + using (var response = await SendAsync("api/signin-test/current-user", cookie)) + { + return (await response.Content.ReadAsStringAsync()).Split('|')[1]; + } + } + + private async Task SendAsync(string url, string cookie) + { + using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, url)) + { + requestMessage.Headers.Add("Cookie", cookie); + return await Client.SendAsync(requestMessage); + } + } + + private static string GetAuthCookieHeader(HttpResponseMessage response) + { + return response.Headers.TryGetValues("Set-Cookie", out var values) + ? values.FirstOrDefault(x => x.StartsWith(AuthenticationCookieName)) + : null; + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/ShortLivedCookieIdentityTestModule.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/ShortLivedCookieIdentityTestModule.cs new file mode 100644 index 0000000000..0bb38d23b9 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/ShortLivedCookieIdentityTestModule.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Modularity; + +namespace Volo.Abp.Identity.AspNetCore; + +[DependsOn(typeof(AbpIdentityAspNetCoreTestModule))] +public class ShortLivedCookieIdentityTestModule : AbpModule +{ + public override void PostConfigureServices(ServiceConfigurationContext context) + { + var timeProvider = new TestTimeProvider(); + context.Services.AddSingleton(timeProvider); + + // Short lifetime with sliding expiration on a controllable clock, so a request past the + // half-life schedules a cookie renewal without real time delays. + context.Services.PostConfigure(IdentityConstants.ApplicationScheme, options => + { + options.ExpireTimeSpan = TimeSpan.FromSeconds(10); + options.SlidingExpiration = true; + options.TimeProvider = timeProvider; + }); + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/ShortLivedCookieIdentityTestStartup.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/ShortLivedCookieIdentityTestStartup.cs new file mode 100644 index 0000000000..5b7fe0a965 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/ShortLivedCookieIdentityTestStartup.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Volo.Abp.Identity.AspNetCore; + +public class ShortLivedCookieIdentityTestStartup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddApplication(); + } + + public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) + { + app.InitializeApplication(); + } +} 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 b2381a6a53..8eb7ab76e7 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 @@ -3,7 +3,9 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.Users; namespace Volo.Abp.Identity.AspNetCore; @@ -30,6 +32,23 @@ public class SignInTestController : AbpController return Content(result.ToString()); } + [Route("current-user")] + public ActionResult GetCurrentUser() + { + return Content(CurrentUser.IsAuthenticated ? CurrentUser.UserName + "|" + CurrentUser.FindSessionId() : "anonymous"); + } + + [Route("switch-account")] + public async Task SwitchAccount(string userName) + { + // Account switch (LinkLogin / impersonation): sign out the current identity and sign in + // as another user within the same request. + await _signInManager.SignOutAsync(); + var user = await _signInManager.UserManager.FindByNameAsync(userName); + await _signInManager.SignInAsync(user, isPersistent: false); + return Content("Succeeded"); + } + [Route("write-two-factor-cookie")] public async Task WriteTwoFactorCookie(string userId) { diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/TestTimeProvider.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/TestTimeProvider.cs new file mode 100644 index 0000000000..3f5e5bcf97 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/TestTimeProvider.cs @@ -0,0 +1,18 @@ +using System; + +namespace Volo.Abp.Identity.AspNetCore; + +public class TestTimeProvider : TimeProvider +{ + private DateTimeOffset _utcNow = DateTimeOffset.UtcNow; + + public override DateTimeOffset GetUtcNow() + { + return _utcNow; + } + + public void Advance(TimeSpan timeSpan) + { + _utcNow = _utcNow.Add(timeSpan); + } +} From cdb8a1c805ea1f9a973ea26d28b93e83190cedd2 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 20 Aug 2026 17:19:02 +0800 Subject: [PATCH 2/2] Rename `IsValidateAsync` to `IsValidAsync` --- .../AbpIdentitySessionCookieAuthenticationOptionsExtensions.cs | 2 +- .../Volo/Abp/Identity/IIdentitySessionChecker.cs | 2 +- .../Volo/Abp/Identity/NullIdentitySessionChecker.cs | 2 +- .../Volo/Abp/Identity/AspNetCore/FakeIdentitySessionChecker.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentitySessionCookieAuthenticationOptionsExtensions.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentitySessionCookieAuthenticationOptionsExtensions.cs index 68228defd9..da5960081e 100644 --- a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentitySessionCookieAuthenticationOptionsExtensions.cs +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentitySessionCookieAuthenticationOptionsExtensions.cs @@ -60,7 +60,7 @@ public static class AbpIdentitySessionCookieAuthenticationOptionsExtensions var identitySessionChecker = httpContext.RequestServices.GetRequiredService(); using (currentTenant.Change(principal.FindTenantId())) { - return await identitySessionChecker.IsValidateAsync(sessionId); + return await identitySessionChecker.IsValidAsync(sessionId); } } } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentitySessionChecker.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentitySessionChecker.cs index bb10480a34..b0c1a47f6a 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentitySessionChecker.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentitySessionChecker.cs @@ -4,5 +4,5 @@ namespace Volo.Abp.Identity; public interface IIdentitySessionChecker { - Task IsValidateAsync(string sessionId); + Task IsValidAsync(string sessionId); } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/NullIdentitySessionChecker.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/NullIdentitySessionChecker.cs index 4d8c7bf050..bf5a803e6e 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/NullIdentitySessionChecker.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/NullIdentitySessionChecker.cs @@ -5,7 +5,7 @@ namespace Volo.Abp.Identity; public class NullIdentitySessionChecker : IIdentitySessionChecker, ISingletonDependency { - public Task IsValidateAsync(string sessionId) + public Task IsValidAsync(string sessionId) { return Task.FromResult(true); } diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/FakeIdentitySessionChecker.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/FakeIdentitySessionChecker.cs index 0b4d91c586..f560e38e68 100644 --- a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/FakeIdentitySessionChecker.cs +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/FakeIdentitySessionChecker.cs @@ -14,7 +14,7 @@ public class FakeIdentitySessionChecker : IIdentitySessionChecker, ISingletonDep { public HashSet RevokedSessionIds { get; } = new(); - public Task IsValidateAsync(string sessionId) + public Task IsValidAsync(string sessionId) { return Task.FromResult(!RevokedSessionIds.Contains(sessionId)); }