From 5c06c4825529b70f50ef93e649b41eb9ed0f212f Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 27 Jul 2026 15:34:46 +0800 Subject: [PATCH 1/2] Preserve AuthenticateResult while normalizing the antiforgery principal --- .../Mvc/AntiForgery/AbpAntiforgery.cs | 29 ++- .../Mvc/AntiForgery/AbpAntiforgery_Tests.cs | 206 +++++++++++++++++- 2 files changed, 230 insertions(+), 5 deletions(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAntiforgery.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAntiforgery.cs index 22c4900379..289920d704 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAntiforgery.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAntiforgery.cs @@ -1,7 +1,10 @@ using System; +using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features.Authentication; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -67,6 +70,8 @@ public class AbpAntiforgery : IAntiforgery } var normalizer = httpContext.RequestServices.GetRequiredService(); + var authenticateResultFeature = httpContext.Features.Get(); + var originalResult = authenticateResultFeature?.AuthenticateResult; var originalPrincipal = httpContext.User; httpContext.User = normalizer.Normalize(originalPrincipal); try @@ -75,7 +80,7 @@ public class AbpAntiforgery : IAntiforgery } finally { - httpContext.User = originalPrincipal; + RestoreAuthenticationState(httpContext, authenticateResultFeature, originalResult, originalPrincipal); } } @@ -87,6 +92,8 @@ public class AbpAntiforgery : IAntiforgery } var normalizer = httpContext.RequestServices.GetRequiredService(); + var authenticateResultFeature = httpContext.Features.Get(); + var originalResult = authenticateResultFeature?.AuthenticateResult; var originalPrincipal = httpContext.User; httpContext.User = normalizer.Normalize(originalPrincipal); try @@ -95,7 +102,25 @@ public class AbpAntiforgery : IAntiforgery } finally { - httpContext.User = originalPrincipal; + RestoreAuthenticationState(httpContext, authenticateResultFeature, originalResult, originalPrincipal); + } + } + + protected virtual void RestoreAuthenticationState( + HttpContext httpContext, + IAuthenticateResultFeature? authenticateResultFeature, + AuthenticateResult? originalResult, + ClaimsPrincipal originalPrincipal) + { + // Assigning HttpContext.User drops the AuthenticateResult on the built-in feature, so restoring the + // principal alone would leave downstream consumers (SignalR connection expiration, idle checks, ...) + // without the authentication properties. IAuthenticateResultFeature does not require an implementation + // to keep the two in sync, so restore both: the principal first, then the result. + httpContext.User = originalPrincipal; + + if (originalResult != null && authenticateResultFeature != null) + { + authenticateResultFeature.AuthenticateResult = originalResult; } } } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAntiforgery_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAntiforgery_Tests.cs index 9ac20ed4d6..e204667e33 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAntiforgery_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAntiforgery_Tests.cs @@ -2,7 +2,9 @@ using System; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features.Authentication; using Microsoft.Extensions.DependencyInjection; using Shouldly; using Volo.Abp.Security.Claims; @@ -14,6 +16,7 @@ public class AbpAntiforgery_Tests { private const string BearerIssuer = "https://localhost:44361/"; private const string UserId = "3a0e6f1c-1111-2222-3333-444455556666"; + private static readonly DateTimeOffset ExpiresUtc = new(2030, 1, 2, 3, 4, 5, TimeSpan.Zero); [Fact] public Task GetAndStoreTokens_should_normalize_the_user_and_restore_it() => @@ -45,6 +48,109 @@ public class AbpAntiforgery_Tests (antiforgery, httpContext) => { antiforgery.SetCookieTokenAndHeader(httpContext); return Task.CompletedTask; }, inner => inner.UserSeenBySetCookieTokenAndHeader); + [Fact] + public Task GetAndStoreTokens_should_preserve_the_authenticate_result() => + Should_preserve_the_authenticate_result( + (antiforgery, httpContext) => { antiforgery.GetAndStoreTokens(httpContext); return Task.CompletedTask; }); + + [Fact] + public Task GetTokens_should_preserve_the_authenticate_result() => + Should_preserve_the_authenticate_result( + (antiforgery, httpContext) => { antiforgery.GetTokens(httpContext); return Task.CompletedTask; }); + + [Fact] + public Task IsRequestValidAsync_should_preserve_the_authenticate_result() => + Should_preserve_the_authenticate_result( + (antiforgery, httpContext) => antiforgery.IsRequestValidAsync(httpContext)); + + [Fact] + public Task ValidateRequestAsync_should_preserve_the_authenticate_result() => + Should_preserve_the_authenticate_result( + (antiforgery, httpContext) => antiforgery.ValidateRequestAsync(httpContext)); + + [Fact] + public Task SetCookieTokenAndHeader_should_preserve_the_authenticate_result() => + Should_preserve_the_authenticate_result( + (antiforgery, httpContext) => { antiforgery.SetCookieTokenAndHeader(httpContext); return Task.CompletedTask; }); + + [Fact] + public async Task Should_preserve_the_authenticate_result_when_the_inner_antiforgery_throws() + { + var inner = new RecordingAntiforgery { ThrowOnValidateRequest = true }; + var antiforgery = new AbpAntiforgery(inner, CreateOptions(normalize: true)); + var original = CreatePrincipal(BearerIssuer); + var httpContext = CreateHttpContext(original, withNormalizer: true, CreateSuccessResult(original)); + + await Should.ThrowAsync( + () => antiforgery.ValidateRequestAsync(httpContext)); + + AssertResultPreserved(httpContext, original); + } + + [Fact] + public void Should_restore_both_when_the_feature_does_not_sync_the_user() + { + // IAuthenticateResultFeature does not require an implementation to keep the principal and the result + // in sync; ASP.NET Core itself reuses such a feature. Restoring only the result would leave the + // normalized principal on HttpContext.User. + var inner = new RecordingAntiforgery(); + var antiforgery = new AbpAntiforgery(inner, CreateOptions(normalize: true)); + var original = CreatePrincipal(BearerIssuer); + var originalResult = CreateSuccessResult(original); + var httpContext = CreateHttpContext(original, withNormalizer: true); + httpContext.Features.Set( + new DecoupledAuthenticateResultFeature { AuthenticateResult = originalResult }); + + antiforgery.GetAndStoreTokens(httpContext); + + httpContext.User.ShouldBeSameAs(original); + httpContext.Features.Get()!.AuthenticateResult.ShouldBeSameAs(originalResult); + } + + [Fact] + public void Should_restore_the_original_authenticate_result_instance() + { + var inner = new RecordingAntiforgery(); + var antiforgery = new AbpAntiforgery(inner, CreateOptions(normalize: true)); + var original = CreatePrincipal(BearerIssuer); + var originalResult = CreateSuccessResult(original); + var httpContext = CreateHttpContext(original, withNormalizer: true, originalResult); + + antiforgery.GetAndStoreTokens(httpContext); + + httpContext.Features.Get()!.AuthenticateResult.ShouldBeSameAs(originalResult); + } + + [Fact] + public void Should_restore_the_user_when_there_is_no_authenticate_result_feature() + { + var inner = new RecordingAntiforgery(); + var antiforgery = new AbpAntiforgery(inner, CreateOptions(normalize: true)); + var original = CreatePrincipal(BearerIssuer); + var httpContext = CreateHttpContext(original, withNormalizer: true); + + antiforgery.GetAndStoreTokens(httpContext); + + httpContext.Features.Get().ShouldBeNull(); + httpContext.User.ShouldBeSameAs(original); + } + + [Fact] + public void Should_restore_the_user_when_the_authenticate_result_is_null() + { + var inner = new RecordingAntiforgery(); + var antiforgery = new AbpAntiforgery(inner, CreateOptions(normalize: true)); + var original = CreatePrincipal(BearerIssuer); + var httpContext = CreateHttpContext(original, withNormalizer: true, CreateSuccessResult(original)); + httpContext.Features.Get()!.AuthenticateResult = null; + httpContext.User = original; + + antiforgery.GetAndStoreTokens(httpContext); + + httpContext.User.ShouldBeSameAs(original); + httpContext.Features.Get()!.AuthenticateResult.ShouldBeNull(); + } + [Fact] public void Should_delegate_the_result_to_the_inner_antiforgery() { @@ -103,13 +209,48 @@ public class AbpAntiforgery_Tests httpContext.User.ShouldBeSameAs(original); } + private static async Task Should_preserve_the_authenticate_result(Func invoke) + { + var inner = new RecordingAntiforgery(); + var antiforgery = new AbpAntiforgery(inner, CreateOptions(normalize: true)); + var original = CreatePrincipal(BearerIssuer); + var httpContext = CreateHttpContext(original, withNormalizer: true, CreateSuccessResult(original)); + + await invoke(antiforgery, httpContext); + + AssertResultPreserved(httpContext, original); + } + + private static void AssertResultPreserved(HttpContext httpContext, ClaimsPrincipal original) + { + var result = httpContext.Features.Get()!.AuthenticateResult; + result.ShouldNotBeNull(); + result.Properties!.IsPersistent.ShouldBeTrue(); + result.Properties.ExpiresUtc.ShouldBe(ExpiresUtc); + httpContext.User.ShouldBeSameAs(original); + } + + private static AuthenticateResult CreateSuccessResult(ClaimsPrincipal principal) + { + var properties = new AuthenticationProperties + { + IsPersistent = true, + ExpiresUtc = ExpiresUtc + }; + + return AuthenticateResult.Success(new AuthenticationTicket(principal, properties, "TestScheme")); + } + private static Microsoft.Extensions.Options.IOptions CreateOptions(bool normalize) { return Microsoft.Extensions.Options.Options.Create( new AbpAntiForgeryOptions { NormalizeUserIdClaimIssuer = normalize }); } - private static HttpContext CreateHttpContext(ClaimsPrincipal user, bool withNormalizer) + private static HttpContext CreateHttpContext( + ClaimsPrincipal user, + bool withNormalizer, + AuthenticateResult? authenticateResult = null) { var services = new ServiceCollection(); if (withNormalizer) @@ -117,11 +258,63 @@ public class AbpAntiforgery_Tests services.AddTransient(); } - return new DefaultHttpContext + var httpContext = new DefaultHttpContext { - User = user, RequestServices = services.BuildServiceProvider() }; + + if (authenticateResult != null) + { + var features = new TestAuthenticationFeatures(authenticateResult); + httpContext.Features.Set(features); + httpContext.Features.Set(features); + } + else + { + httpContext.User = user; + } + + return httpContext; + } + + // An IAuthenticateResultFeature that does not touch HttpContext.User, like the one ASP.NET Core + // reuses in AuthorizationMiddlewareTests. + private sealed class DecoupledAuthenticateResultFeature : IAuthenticateResultFeature + { + public AuthenticateResult? AuthenticateResult { get; set; } + } + + // Mirrors the framework's internal AuthenticationFeatures: assigning User drops the AuthenticateResult, + // which is the behaviour AbpAntiforgery has to compensate for when it restores the principal. + private sealed class TestAuthenticationFeatures : IAuthenticateResultFeature, IHttpAuthenticationFeature + { + private ClaimsPrincipal? _user; + private AuthenticateResult? _result; + + public TestAuthenticationFeatures(AuthenticateResult result) + { + AuthenticateResult = result; + } + + public AuthenticateResult? AuthenticateResult + { + get => _result; + set + { + _result = value; + _user = _result?.Principal; + } + } + + public ClaimsPrincipal? User + { + get => _user; + set + { + _user = value; + _result = null; + } + } } private static ClaimsPrincipal CreatePrincipal(string userIdClaimIssuer) @@ -145,6 +338,8 @@ public class AbpAntiforgery_Tests public ClaimsPrincipal? UserSeenByValidateRequest { get; private set; } public ClaimsPrincipal? UserSeenBySetCookieTokenAndHeader { get; private set; } + public bool ThrowOnValidateRequest { get; set; } + public AntiforgeryTokenSet GetAndStoreTokens(HttpContext httpContext) { UserSeenByGetAndStoreTokens = httpContext.User; @@ -166,6 +361,11 @@ public class AbpAntiforgery_Tests public Task ValidateRequestAsync(HttpContext httpContext) { UserSeenByValidateRequest = httpContext.User; + if (ThrowOnValidateRequest) + { + throw new AntiforgeryValidationException("test"); + } + return Task.CompletedTask; } From 0ccf12ac98653a4693254ea1adfdf5d1f107a3be Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 27 Jul 2026 15:34:46 +0800 Subject: [PATCH 2/2] Move UseAntiforgery after UseAuthorization in Blazor templates --- docs/en/release-info/migration-guides/abp-8-2-blazor-web-app.md | 2 +- .../MyProjectNameModule.cs | 2 +- .../MyProjectNameModule.cs | 2 +- .../Server.Mongo/MyProjectNameHostModule.cs | 2 +- .../Server/MyProjectNameHostModule.cs | 2 +- .../MyProjectNameBlazorModule.cs | 2 +- .../MyProjectNameBlazorModule.cs | 2 +- .../MyProjectNameBlazorModule.cs | 2 +- .../MyProjectNameBlazorModule.cs | 2 +- .../MyProjectNameBlazorHostModule.cs | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/en/release-info/migration-guides/abp-8-2-blazor-web-app.md b/docs/en/release-info/migration-guides/abp-8-2-blazor-web-app.md index e3b65e1221..2511ca659e 100644 --- a/docs/en/release-info/migration-guides/abp-8-2-blazor-web-app.md +++ b/docs/en/release-info/migration-guides/abp-8-2-blazor-web-app.md @@ -86,8 +86,8 @@ public override void OnApplicationInitialization(ApplicationInitializationContex var app = context.GetApplicationBuilder(); // ... - app.UseAntiforgery(); app.UseAuthorization(); + app.UseAntiforgery(); app.UseConfiguredEndpoints(builder => { diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyProjectNameModule.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyProjectNameModule.cs index df48e3abe5..8dde151ea7 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyProjectNameModule.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyProjectNameModule.cs @@ -361,8 +361,8 @@ public class MyProjectNameModule : AbpModule app.UseUnitOfWork(); app.UseDynamicClaims(); - app.UseAntiforgery(); app.UseAuthorization(); + app.UseAntiforgery(); app.UseSwagger(); app.UseAbpSwaggerUI(options => diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyProjectNameModule.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyProjectNameModule.cs index fdadb06ae6..8613a870ae 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyProjectNameModule.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyProjectNameModule.cs @@ -378,8 +378,8 @@ public class MyProjectNameModule : AbpModule app.UseUnitOfWork(); app.UseDynamicClaims(); - app.UseAntiforgery(); app.UseAuthorization(); + app.UseAntiforgery(); app.UseSwagger(); app.UseAbpSwaggerUI(options => diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyProjectNameHostModule.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyProjectNameHostModule.cs index 607e26d596..968a9da9c7 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyProjectNameHostModule.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyProjectNameHostModule.cs @@ -316,8 +316,8 @@ public class MyProjectNameHostModule : AbpModule app.UseUnitOfWork(); app.UseDynamicClaims(); - app.UseAntiforgery(); app.UseAuthorization(); + app.UseAntiforgery(); app.UseSwagger(); app.UseAbpSwaggerUI(options => diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyProjectNameHostModule.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyProjectNameHostModule.cs index 0753f5581d..476ea57b0c 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyProjectNameHostModule.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyProjectNameHostModule.cs @@ -335,8 +335,8 @@ public class MyProjectNameHostModule : AbpModule app.UseUnitOfWork(); app.UseDynamicClaims(); - app.UseAntiforgery(); app.UseAuthorization(); + app.UseAntiforgery(); app.UseSwagger(); app.UseAbpSwaggerUI(options => diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyProjectNameBlazorModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyProjectNameBlazorModule.cs index e98ea8659c..172881a800 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyProjectNameBlazorModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyProjectNameBlazorModule.cs @@ -353,8 +353,8 @@ public class MyProjectNameBlazorModule : AbpModule app.UseMultiTenancy(); } app.UseDynamicClaims(); - app.UseAntiforgery(); app.UseAuthorization(); + app.UseAntiforgery(); app.UseSwagger(); app.UseAbpSwaggerUI(options => { diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyProjectNameBlazorModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyProjectNameBlazorModule.cs index 68946c4493..c6f92fa464 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyProjectNameBlazorModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyProjectNameBlazorModule.cs @@ -266,8 +266,8 @@ public class MyProjectNameBlazorModule : AbpModule } app.UseUnitOfWork(); app.UseDynamicClaims(); - app.UseAntiforgery(); app.UseAuthorization(); + app.UseAntiforgery(); app.UseSwagger(); app.UseAbpSwaggerUI(options => diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyProjectNameBlazorModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyProjectNameBlazorModule.cs index 736b7ef7b3..f5a3b24495 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyProjectNameBlazorModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyProjectNameBlazorModule.cs @@ -360,8 +360,8 @@ public class MyProjectNameBlazorModule : AbpModule app.UseMultiTenancy(); } app.UseDynamicClaims(); - app.UseAntiforgery(); app.UseAuthorization(); + app.UseAntiforgery(); app.UseSwagger(); app.UseAbpSwaggerUI(options => { diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyProjectNameBlazorModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyProjectNameBlazorModule.cs index f89ba68756..5a2124a74a 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyProjectNameBlazorModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyProjectNameBlazorModule.cs @@ -274,8 +274,8 @@ public class MyProjectNameBlazorModule : AbpModule } app.UseUnitOfWork(); app.UseDynamicClaims(); - app.UseAntiforgery(); app.UseAuthorization(); + app.UseAntiforgery(); app.UseSwagger(); app.UseAbpSwaggerUI(options => diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/MyProjectNameBlazorHostModule.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/MyProjectNameBlazorHostModule.cs index 17ab4f1ee0..2ffcbde619 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/MyProjectNameBlazorHostModule.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/MyProjectNameBlazorHostModule.cs @@ -259,8 +259,8 @@ public class MyProjectNameBlazorHostModule : AbpModule } app.UseUnitOfWork(); - app.UseAntiforgery(); app.UseAuthorization(); + app.UseAntiforgery(); app.UseSwagger(); app.UseAbpSwaggerUI(options => {