From d2f32496c39eecf84aa4f817d68cb6a1b33198dc Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Mon, 10 Nov 2025 16:55:29 -0800 Subject: [PATCH 1/5] Ensure ClaimsPrincipal.Current is set if user changes The existing setup would only set Current to the existing user. Some scenarios seem to invoke at other times, so this uses the IRequestUserFeature to ensure that any updates to the user will be mirrored to the ClaimsUser.Current --- .../AuthRemoteIdentityCore/Program.cs | 21 +++++++++- .../CurrentPrincipalMiddleware.cs | 24 +++--------- .../Adapters/Features/IRequestUserFeature.cs | 5 +++ .../Internal/RequestUserExtensions.cs | 38 ++++++++++++++++++- .../RemoteAuthIdentityTests.cs | 37 +++++++++++++----- 5 files changed, 92 insertions(+), 33 deletions(-) diff --git a/samples/AuthRemoteIdentity/AuthRemoteIdentityCore/Program.cs b/samples/AuthRemoteIdentity/AuthRemoteIdentityCore/Program.cs index 6293a5aa10..e3ea39a0be 100644 --- a/samples/AuthRemoteIdentity/AuthRemoteIdentityCore/Program.cs +++ b/samples/AuthRemoteIdentity/AuthRemoteIdentityCore/Program.cs @@ -99,15 +99,32 @@ app.UseRouting(); -app.UseAuthenticationEvents(); app.UseAuthentication(); -app.UseAuthorizationEvents(); +app.UseAuthenticationEvents(); app.UseAuthorization(); +app.UseAuthorizationEvents(); app.UseSystemWebAdapters(); app.MapDefaultControllerRoute(); +app.Map("/user", () => +{ + var user = ClaimsPrincipal.Current; + + if (user is null) + { + return Results.Problem("Empty ClaimsPrincipal"); + } + + return Results.Json(new + { + IsAuthenticated = user.Identity?.IsAuthenticated ?? false, + Name = user.Identity?.Name, + Claims = user.Claims.Select(c => new { c.Type, c.Value }) + }); +}).WithMetadata(new SetThreadCurrentPrincipalAttribute()); ; + app.MapRemoteAppFallback() .ShortCircuit(); diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/CurrentPrincipalMiddleware.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/CurrentPrincipalMiddleware.cs index 95334ac518..5e5b415f7b 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/CurrentPrincipalMiddleware.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/CurrentPrincipalMiddleware.cs @@ -8,11 +8,8 @@ namespace Microsoft.AspNetCore.SystemWebAdapters; -internal partial class CurrentPrincipalMiddleware +internal sealed class CurrentPrincipalMiddleware { - [LoggerMessage(0, LogLevel.Trace, "Thread.CurrentPrincipal has been set with the current user")] - private partial void LogCurrentPrincipalUsage(); - private readonly RequestDelegate _next; private readonly ILogger _logger; @@ -23,23 +20,12 @@ public CurrentPrincipalMiddleware(RequestDelegate next, ILogger context.GetEndpoint()?.Metadata.GetMetadata() is { IsDisabled: false } ? SetUserAsync(context) : _next(context); - - private async Task SetUserAsync(HttpContext context) { - LogCurrentPrincipalUsage(); - - var current = Thread.CurrentPrincipal; - - try + if (context.GetEndpoint()?.Metadata.GetMetadata() is { IsDisabled: false }) { - Thread.CurrentPrincipal = context.User; - - await _next(context); - } - finally - { - Thread.CurrentPrincipal = current; + context.GetRequestUser().EnableStaticAccessors(); } + + return _next(context); } } diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/Features/IRequestUserFeature.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/Features/IRequestUserFeature.cs index 20191c197d..9101c03109 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/Features/IRequestUserFeature.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/Features/IRequestUserFeature.cs @@ -28,6 +28,11 @@ public interface IRequestUserFeature /// Gets the logged on user that corresponds to /// WindowsIdentity? LogonUserIdentity { get; } + + /// + /// Enables access to and for the duration of the request. + /// + void EnableStaticAccessors(); } #endif diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Internal/RequestUserExtensions.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Internal/RequestUserExtensions.cs index 8e6c92a257..c14e39076e 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/Internal/RequestUserExtensions.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Internal/RequestUserExtensions.cs @@ -4,6 +4,7 @@ using System; using System.Security.Claims; using System.Security.Principal; +using System.Threading; using Microsoft.AspNetCore.Http.Features.Authentication; using Microsoft.AspNetCore.SystemWebAdapters.Features; using Microsoft.Extensions.DependencyInjection; @@ -23,15 +24,17 @@ public static IRequestUserFeature GetRequestUser(this HttpContextCore context) var newFeature = new RequestUserFeature(context) { User = context.User }; + context.Response.RegisterForDispose(newFeature); context.Features.Set(newFeature); context.Features.Set(newFeature); return newFeature; } - private sealed partial class RequestUserFeature : IRequestUserFeature, IHttpAuthenticationFeature + private sealed partial class RequestUserFeature : IRequestUserFeature, IHttpAuthenticationFeature, IDisposable { private readonly ILogger _logger; + private bool _setCurrentAccessors; public RequestUserFeature(HttpContextCore context) { @@ -45,6 +48,9 @@ public RequestUserFeature(HttpContextCore context) [LoggerMessage(0, LogLevel.Debug, "A custom principal {PrincipalType} is being used and should be replaced with a ClaimsPrincipal derived type.")] private partial void LogNonClaimsPrincipal(Type principalType); + [LoggerMessage(1, LogLevel.Trace, "Thread.CurrentPrincipal has been set with the current user")] + private partial void LogCurrentPrincipalUsage(); + public IPrincipal? User { get; set; } WindowsIdentity? IRequestUserFeature.LogonUserIdentity => User?.Identity as WindowsIdentity; @@ -52,7 +58,11 @@ public RequestUserFeature(HttpContextCore context) ClaimsPrincipal? IHttpAuthenticationFeature.User { get => GetOrCreateClaims(User); - set => User = value; + set + { + User = value; + EnsureCurrentPrincipalSetIfRequired(); + } } private ClaimsPrincipal? GetOrCreateClaims(IPrincipal? principal) @@ -71,5 +81,29 @@ public RequestUserFeature(HttpContextCore context) return new ClaimsPrincipal(principal); } + + public void EnableStaticAccessors() + { + LogCurrentPrincipalUsage(); + _setCurrentAccessors = true; + EnsureCurrentPrincipalSetIfRequired(); + } + + private void EnsureCurrentPrincipalSetIfRequired() + { + if (_setCurrentAccessors) + { + var claimsPrincipal = GetOrCreateClaims(User); + Thread.CurrentPrincipal = claimsPrincipal; + } + } + + public void Dispose() + { + if (_setCurrentAccessors) + { + Thread.CurrentPrincipal = null; + } + } } } diff --git a/test/Microsoft.AspNetCore.SystemWebAdapters.E2E.Tests/RemoteAuthIdentityTests.cs b/test/Microsoft.AspNetCore.SystemWebAdapters.E2E.Tests/RemoteAuthIdentityTests.cs index bbebeefe3b..3706c0c247 100644 --- a/test/Microsoft.AspNetCore.SystemWebAdapters.E2E.Tests/RemoteAuthIdentityTests.cs +++ b/test/Microsoft.AspNetCore.SystemWebAdapters.E2E.Tests/RemoteAuthIdentityTests.cs @@ -3,7 +3,9 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Playwright.Xunit; using Projects; +using System.Text.Json; using Xunit; +using System.Text.Json.Serialization; namespace Microsoft.AspNetCore.SystemWebAdapters.E2E.Tests; @@ -18,19 +20,19 @@ public async Task MVCCoreAppCanLogoutBothApps(string name) var coreAppEndpoint = await GetEndpoint(name); var frameworkAppEndpoint = await GetAspNetFrameworkEndpoint(); - await Page.GotoAsync(frameworkAppEndpoint); + await Page.GotoAsync(frameworkAppEndpoint.AbsoluteUri); await Expect(Page.Locator("text=My ASP.NET Application")).ToBeVisibleAsync(); await RegisterUser(email); // Make sure core app also logged in - await Page.GotoAsync(coreAppEndpoint); + await Page.GotoAsync(coreAppEndpoint.AbsoluteUri); await Expect(Page.Locator($"text=Hello {email}!")).ToBeVisibleAsync(); // Logout on core app and make sure both logged out await Page.Locator(@"text=Log out").ClickAsync(); await Expect(Page.Locator(@"text=Log in")).ToBeVisibleAsync(); - await Page.GotoAsync(frameworkAppEndpoint); + await Page.GotoAsync(frameworkAppEndpoint.AbsoluteUri); await Expect(Page.Locator(@"text=Log in")).ToBeVisibleAsync(); } @@ -44,20 +46,27 @@ public async Task MVCAppCanLogoutBothApps(string name) var frameworkAppEndpoint = await GetAspNetFrameworkEndpoint(); // Login with core app - await Page.GotoAsync(coreAppEndpoint); + await Page.GotoAsync(coreAppEndpoint.AbsoluteUri); await Expect(Page.Locator("text=ASP.NET Core")).ToBeVisibleAsync(); // Create the user await RegisterUser(email); + // Check the /user endpoint for ClaimsIdentity.Current setting + var user = await Page.GotoAsync(new Uri(coreAppEndpoint, "/user").AbsoluteUri); + Assert.NotNull(user); + var userResult = await user.JsonAsync(); + Assert.Equal(email, userResult.Name); + Assert.True(userResult.IsAuthenticated); + // Make sure framework app also logged in - await Page.GotoAsync(frameworkAppEndpoint); + await Page.GotoAsync(frameworkAppEndpoint.AbsoluteUri); await Expect(Page.Locator($"text=Hello {email}!")).ToBeVisibleAsync(); // Logout on framework app and make sure both logged out await Page.Locator(@"text=Log out").ClickAsync(); await Expect(Page.Locator(@"text=Log in")).ToBeVisibleAsync(); - await Page.GotoAsync(coreAppEndpoint); + await Page.GotoAsync(coreAppEndpoint.AbsoluteUri); await Expect(Page.Locator(@"text=Log in")).ToBeVisibleAsync(); } @@ -94,12 +103,20 @@ static string CreatePassword() } } - private async ValueTask GetEndpoint(string name) + private async ValueTask GetEndpoint(string name) { var app = await aspire.GetApplicationAsync(); - var uri = app.GetEndpoint(name, "https").AbsoluteUri; - return uri; + return app.GetEndpoint(name, "https"); } - private ValueTask GetAspNetFrameworkEndpoint() => GetEndpoint("framework"); + private ValueTask GetAspNetFrameworkEndpoint() => GetEndpoint("framework"); + + private sealed class UserResult + { + [JsonPropertyName("isAuthenticated")] + public bool IsAuthenticated { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + } } From bcd06f211e33eaf7e7fb982c4cf6865df4eeed3a Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Tue, 11 Nov 2025 10:57:48 -0800 Subject: [PATCH 2/5] always set feature --- .../CurrentPrincipalMiddleware.cs | 4 +- .../Features/RequestUserFeature.cs | 89 ++++++++++++++ .../RegisterAdapterFeaturesMiddleware.cs | 17 +++ .../HttpContext.cs | 2 +- .../HttpRequest.cs | 2 +- .../Internal/RequestUserExtensions.cs | 109 ------------------ .../HttpContextTests.cs | 52 +++++++++ 7 files changed, 163 insertions(+), 112 deletions(-) create mode 100644 src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Features/RequestUserFeature.cs delete mode 100644 src/Microsoft.AspNetCore.SystemWebAdapters/Internal/RequestUserExtensions.cs diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/CurrentPrincipalMiddleware.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/CurrentPrincipalMiddleware.cs index 5e5b415f7b..7b2373d8a0 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/CurrentPrincipalMiddleware.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/CurrentPrincipalMiddleware.cs @@ -4,6 +4,8 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.SystemWebAdapters.Features; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.SystemWebAdapters; @@ -23,7 +25,7 @@ public Task InvokeAsync(HttpContext context) { if (context.GetEndpoint()?.Metadata.GetMetadata() is { IsDisabled: false }) { - context.GetRequestUser().EnableStaticAccessors(); + context.Features.GetRequiredFeature().EnableStaticAccessors(); } return _next(context); diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Features/RequestUserFeature.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Features/RequestUserFeature.cs new file mode 100644 index 0000000000..4ebd46ca7f --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Features/RequestUserFeature.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Security.Claims; +using System.Security.Principal; +using System.Threading; +using Microsoft.AspNetCore.Http.Features.Authentication; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.AspNetCore.SystemWebAdapters.Features; + +internal sealed partial class RequestUserFeature : IRequestUserFeature, IHttpAuthenticationFeature, IDisposable +{ + private readonly ILogger _logger; + private bool _setCurrentAccessors; + + public RequestUserFeature(HttpContextCore context) + { + var logger = (ILogger?)context.RequestServices?.GetService()?.CreateLogger(); + + _logger = logger ?? NullLogger.Instance; + + User = context.User; + } + + [LoggerMessage(0, LogLevel.Debug, "A custom principal {PrincipalType} is being used and should be replaced with a ClaimsPrincipal derived type.")] + private partial void LogNonClaimsPrincipal(Type principalType); + + [LoggerMessage(1, LogLevel.Trace, "Thread.CurrentPrincipal has been set with the current user")] + private partial void LogCurrentPrincipalUsage(); + + public IPrincipal? User { get; set; } + + WindowsIdentity? IRequestUserFeature.LogonUserIdentity => User?.Identity as WindowsIdentity; + + ClaimsPrincipal? IHttpAuthenticationFeature.User + { + get => GetOrCreateClaims(User); + set + { + User = value; + EnsureCurrentPrincipalSetIfRequired(); + } + } + + private ClaimsPrincipal? GetOrCreateClaims(IPrincipal? principal) + { + if (principal is null) + { + return null; + } + + if (principal is ClaimsPrincipal claimsPrincipal) + { + return claimsPrincipal; + } + + LogNonClaimsPrincipal(principal.GetType()); + + return new ClaimsPrincipal(principal); + } + + public void EnableStaticAccessors() + { + LogCurrentPrincipalUsage(); + _setCurrentAccessors = true; + EnsureCurrentPrincipalSetIfRequired(); + } + + private void EnsureCurrentPrincipalSetIfRequired() + { + if (_setCurrentAccessors) + { + var claimsPrincipal = GetOrCreateClaims(User); + Thread.CurrentPrincipal = claimsPrincipal; + } + } + + public void Dispose() + { + if (_setCurrentAccessors) + { + Thread.CurrentPrincipal = null; + } + } +} diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/RegisterAdapterFeaturesMiddleware.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/RegisterAdapterFeaturesMiddleware.cs index 39ee909646..e1a8d7ccd3 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/RegisterAdapterFeaturesMiddleware.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/RegisterAdapterFeaturesMiddleware.cs @@ -6,6 +6,7 @@ using System.Web; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Features.Authentication; using Microsoft.AspNetCore.SystemWebAdapters; using Microsoft.AspNetCore.SystemWebAdapters.Features; @@ -23,6 +24,7 @@ public async Task InvokeAsync(HttpContextCore context) { using (RegisterRequestFeatures(context)) using (RegisterResponseFeatures(context)) + using (RegisterUserFeatures(context)) { try { @@ -39,6 +41,21 @@ public async Task InvokeAsync(HttpContextCore context) } } + private static DelegateDisposable RegisterUserFeatures(HttpContextCore context) + { + var userFeature = new RequestUserFeature(context); + + context.Response.RegisterForDispose(userFeature); + context.Features.Set(userFeature); + context.Features.Set(userFeature); + + return new DelegateDisposable(() => + { + context.Features.Set(null); + context.Features.Set(null); + }); + } + private static DelegateDisposable RegisterRequestFeatures(HttpContextCore context) { var existing = context.Features.GetRequiredFeature(); diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContext.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContext.cs index 61c508ea60..1b89bdafeb 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContext.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContext.cs @@ -101,7 +101,7 @@ public IHttpHandler? Handler public IPrincipal User { get => Context.Features.Get()?.User ?? Context.User; - set => Context.GetRequestUser().User = value; + set => Context.Features.GetRequiredFeature().User = value; } public HttpSessionState? Session => Context.Features.Get()?.Session; diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequest.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequest.cs index 4fc1bcac36..2ebfde1b8e 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequest.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequest.cs @@ -57,7 +57,7 @@ internal HttpRequest(HttpRequestCore request) public string CurrentExecutionFilePath => Request.HttpContext.Features.GetRequiredFeature().CurrentExecutionFilePath; - public WindowsIdentity? LogonUserIdentity => Request.HttpContext.GetRequestUser().LogonUserIdentity; + public WindowsIdentity? LogonUserIdentity => Request.HttpContext.Features.GetRequiredFeature().LogonUserIdentity; public NameValueCollection Headers => _headers ??= Request.Headers.ToNameValueCollection(); diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Internal/RequestUserExtensions.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Internal/RequestUserExtensions.cs deleted file mode 100644 index c14e39076e..0000000000 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/Internal/RequestUserExtensions.cs +++ /dev/null @@ -1,109 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Security.Claims; -using System.Security.Principal; -using System.Threading; -using Microsoft.AspNetCore.Http.Features.Authentication; -using Microsoft.AspNetCore.SystemWebAdapters.Features; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Microsoft.AspNetCore.SystemWebAdapters; - -internal static partial class RequestUserExtensions -{ - public static IRequestUserFeature GetRequestUser(this HttpContextCore context) - { - if (context.Features.Get() is { } existing) - { - return existing; - } - - var newFeature = new RequestUserFeature(context) { User = context.User }; - - context.Response.RegisterForDispose(newFeature); - context.Features.Set(newFeature); - context.Features.Set(newFeature); - - return newFeature; - } - - private sealed partial class RequestUserFeature : IRequestUserFeature, IHttpAuthenticationFeature, IDisposable - { - private readonly ILogger _logger; - private bool _setCurrentAccessors; - - public RequestUserFeature(HttpContextCore context) - { - var logger = (ILogger?)context.RequestServices?.GetService()?.CreateLogger(); - - _logger = logger ?? NullLogger.Instance; - - User = context.User; - } - - [LoggerMessage(0, LogLevel.Debug, "A custom principal {PrincipalType} is being used and should be replaced with a ClaimsPrincipal derived type.")] - private partial void LogNonClaimsPrincipal(Type principalType); - - [LoggerMessage(1, LogLevel.Trace, "Thread.CurrentPrincipal has been set with the current user")] - private partial void LogCurrentPrincipalUsage(); - - public IPrincipal? User { get; set; } - - WindowsIdentity? IRequestUserFeature.LogonUserIdentity => User?.Identity as WindowsIdentity; - - ClaimsPrincipal? IHttpAuthenticationFeature.User - { - get => GetOrCreateClaims(User); - set - { - User = value; - EnsureCurrentPrincipalSetIfRequired(); - } - } - - private ClaimsPrincipal? GetOrCreateClaims(IPrincipal? principal) - { - if (principal is null) - { - return null; - } - - if (principal is ClaimsPrincipal claimsPrincipal) - { - return claimsPrincipal; - } - - LogNonClaimsPrincipal(principal.GetType()); - - return new ClaimsPrincipal(principal); - } - - public void EnableStaticAccessors() - { - LogCurrentPrincipalUsage(); - _setCurrentAccessors = true; - EnsureCurrentPrincipalSetIfRequired(); - } - - private void EnsureCurrentPrincipalSetIfRequired() - { - if (_setCurrentAccessors) - { - var claimsPrincipal = GetOrCreateClaims(User); - Thread.CurrentPrincipal = claimsPrincipal; - } - } - - public void Dispose() - { - if (_setCurrentAccessors) - { - Thread.CurrentPrincipal = null; - } - } - } -} diff --git a/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/HttpContextTests.cs b/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/HttpContextTests.cs index 9dfc6ab15b..dfb6f464b3 100644 --- a/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/HttpContextTests.cs +++ b/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/HttpContextTests.cs @@ -10,6 +10,7 @@ using System.Web.SessionState; using AutoFixture; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features.Authentication; using Microsoft.AspNetCore.SystemWebAdapters.Features; using Microsoft.AspNetCore.SystemWebAdapters.SessionState; using Microsoft.Extensions.DependencyInjection; @@ -64,6 +65,8 @@ public void ServerIsCached() public void UserIsProxied() { var coreContext = new DefaultHttpContext(); + TestFeatures.Enable(coreContext); + var context = new HttpContext(coreContext); Assert.Same(coreContext.User, context.User); @@ -78,6 +81,8 @@ public void UserIsProxied() public void UserIsProxiedWhenSetOnCoreAfterAdapter() { var coreContext = new DefaultHttpContext(); + TestFeatures.Enable(coreContext); + var context = new HttpContext(coreContext); Assert.Same(coreContext.User, context.User); @@ -98,6 +103,8 @@ public void UserIsProxiedWhenSetOnCoreAfterAdapter() public void UserIsNotClaimsPrincipal() { var coreContext = new DefaultHttpContext(); + TestFeatures.Enable(coreContext); + var context = new HttpContext(coreContext); Assert.Same(coreContext.User, context.User); @@ -113,6 +120,8 @@ public void UserIsNotClaimsPrincipal() public void UserIsDerivedClaimsPrincipal() { var coreContext = new DefaultHttpContext(); + TestFeatures.Enable(coreContext); + var context = new HttpContext(coreContext); Assert.Same(coreContext.User, context.User); @@ -131,6 +140,8 @@ private sealed class MyPrincipal : ClaimsPrincipal public void NonClaimsPrincipalIsCopied() { var coreContext = new DefaultHttpContext(); + TestFeatures.Enable(coreContext); + var context = new HttpContext(coreContext); var newUser = new Mock(); @@ -556,5 +567,46 @@ public void SetSessionStateBehavior(SessionStateBehavior behavior) // Assert feature.VerifySet(f => f.Behavior = behavior); } + + private sealed class TestFeatures : IRequestUserFeature, IHttpAuthenticationFeature + { + public IPrincipal? User { get; set; } + + public WindowsIdentity? LogonUserIdentity => null; + + ClaimsPrincipal? IHttpAuthenticationFeature.User + { + get + { + if (User is null) + { + return null; + } + + if (User is ClaimsPrincipal claimsPrincipal) + { + return claimsPrincipal; + } + + return new ClaimsPrincipal(User); + } + set + { + User = value; + } + } + + public void EnableStaticAccessors() + { + throw new NotImplementedException(); + } + + public static void Enable(HttpContext context) + { + var features = new TestFeatures(); + context.Context.Features.Set(features); + context.Context.Features.Set(features); + } + } } } From 1d9b8847172fe3d2d1bf5f7afa107b13af66ac18 Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Tue, 11 Nov 2025 11:41:26 -0800 Subject: [PATCH 3/5] add setting to turn on ClaimsPrincipal.Current everywhere --- .../Features/RequestUserFeature.cs | 89 -------------- .../{ => Middleware}/CachePolicyMiddleware.cs | 2 +- .../CurrentPrincipalMiddleware.cs | 2 +- .../PreBufferRequestStreamMiddleware.cs | 2 +- .../Middleware/RequestFeaturesMiddleware.cs | 37 ++++++ .../RequestUserFeaturesMiddleware.cs | 107 +++++++++++++++++ .../Middleware/ResponseFeaturesMiddleware.cs | 42 +++++++ .../SessionEventsMiddleware.cs | 2 +- .../{ => Middleware}/SessionMiddleware.cs | 0 .../SessionStateMiddleware.cs | 2 +- .../SetHttpContextTimestampMiddleware.cs | 2 +- .../SingleThreadedRequestMiddleware.cs | 2 +- .../RegisterAdapterFeaturesMiddleware.cs | 111 ------------------ .../SystemWebAdaptersExtensions.cs | 20 +++- .../SystemWebAdaptersOptions.cs | 5 + .../HttpRuntimeIntegrationTests.cs | 68 +++++++---- .../SelfHostedTestBase.cs | 30 ----- 17 files changed, 262 insertions(+), 261 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Features/RequestUserFeature.cs rename src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/{ => Middleware}/CachePolicyMiddleware.cs (94%) rename src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/{ => Middleware}/CurrentPrincipalMiddleware.cs (94%) rename src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/{ => Middleware}/PreBufferRequestStreamMiddleware.cs (94%) create mode 100644 src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/RequestFeaturesMiddleware.cs create mode 100644 src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/RequestUserFeaturesMiddleware.cs create mode 100644 src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/ResponseFeaturesMiddleware.cs rename src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/{ => Middleware}/SessionEventsMiddleware.cs (96%) rename src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/{ => Middleware}/SessionMiddleware.cs (100%) rename src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/{ => Middleware}/SessionStateMiddleware.cs (96%) rename src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/{ => Middleware}/SetHttpContextTimestampMiddleware.cs (94%) rename src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/{ => Middleware}/SingleThreadedRequestMiddleware.cs (93%) delete mode 100644 src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/RegisterAdapterFeaturesMiddleware.cs delete mode 100644 test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/SelfHostedTestBase.cs diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Features/RequestUserFeature.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Features/RequestUserFeature.cs deleted file mode 100644 index 4ebd46ca7f..0000000000 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Features/RequestUserFeature.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Security.Claims; -using System.Security.Principal; -using System.Threading; -using Microsoft.AspNetCore.Http.Features.Authentication; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Microsoft.AspNetCore.SystemWebAdapters.Features; - -internal sealed partial class RequestUserFeature : IRequestUserFeature, IHttpAuthenticationFeature, IDisposable -{ - private readonly ILogger _logger; - private bool _setCurrentAccessors; - - public RequestUserFeature(HttpContextCore context) - { - var logger = (ILogger?)context.RequestServices?.GetService()?.CreateLogger(); - - _logger = logger ?? NullLogger.Instance; - - User = context.User; - } - - [LoggerMessage(0, LogLevel.Debug, "A custom principal {PrincipalType} is being used and should be replaced with a ClaimsPrincipal derived type.")] - private partial void LogNonClaimsPrincipal(Type principalType); - - [LoggerMessage(1, LogLevel.Trace, "Thread.CurrentPrincipal has been set with the current user")] - private partial void LogCurrentPrincipalUsage(); - - public IPrincipal? User { get; set; } - - WindowsIdentity? IRequestUserFeature.LogonUserIdentity => User?.Identity as WindowsIdentity; - - ClaimsPrincipal? IHttpAuthenticationFeature.User - { - get => GetOrCreateClaims(User); - set - { - User = value; - EnsureCurrentPrincipalSetIfRequired(); - } - } - - private ClaimsPrincipal? GetOrCreateClaims(IPrincipal? principal) - { - if (principal is null) - { - return null; - } - - if (principal is ClaimsPrincipal claimsPrincipal) - { - return claimsPrincipal; - } - - LogNonClaimsPrincipal(principal.GetType()); - - return new ClaimsPrincipal(principal); - } - - public void EnableStaticAccessors() - { - LogCurrentPrincipalUsage(); - _setCurrentAccessors = true; - EnsureCurrentPrincipalSetIfRequired(); - } - - private void EnsureCurrentPrincipalSetIfRequired() - { - if (_setCurrentAccessors) - { - var claimsPrincipal = GetOrCreateClaims(User); - Thread.CurrentPrincipal = claimsPrincipal; - } - } - - public void Dispose() - { - if (_setCurrentAccessors) - { - Thread.CurrentPrincipal = null; - } - } -} diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/CachePolicyMiddleware.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/CachePolicyMiddleware.cs similarity index 94% rename from src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/CachePolicyMiddleware.cs rename to src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/CachePolicyMiddleware.cs index b6e03b0962..ee11b4af5a 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/CachePolicyMiddleware.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/CachePolicyMiddleware.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.SystemWebAdapters; +namespace Microsoft.AspNetCore.SystemWebAdapters.Middleware; internal sealed class CachePolicyMiddleware(RequestDelegate next) { diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/CurrentPrincipalMiddleware.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/CurrentPrincipalMiddleware.cs similarity index 94% rename from src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/CurrentPrincipalMiddleware.cs rename to src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/CurrentPrincipalMiddleware.cs index 7b2373d8a0..a0e3f89cd7 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/CurrentPrincipalMiddleware.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/CurrentPrincipalMiddleware.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.SystemWebAdapters.Features; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.SystemWebAdapters; +namespace Microsoft.AspNetCore.SystemWebAdapters.Middleware; internal sealed class CurrentPrincipalMiddleware { diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/PreBufferRequestStreamMiddleware.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/PreBufferRequestStreamMiddleware.cs similarity index 94% rename from src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/PreBufferRequestStreamMiddleware.cs rename to src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/PreBufferRequestStreamMiddleware.cs index 1088564037..e99ead9069 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/PreBufferRequestStreamMiddleware.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/PreBufferRequestStreamMiddleware.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.SystemWebAdapters.Features; -namespace Microsoft.AspNetCore.SystemWebAdapters; +namespace Microsoft.AspNetCore.SystemWebAdapters.Middleware; internal partial class PreBufferRequestStreamMiddleware { diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/RequestFeaturesMiddleware.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/RequestFeaturesMiddleware.cs new file mode 100644 index 0000000000..a9eb923068 --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/RequestFeaturesMiddleware.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.SystemWebAdapters.Features; + +namespace Microsoft.AspNetCore.SystemWebAdapters.Middleware; + +internal sealed class RequestFeaturesMiddleware(RequestDelegate next) +{ + public async Task InvokeAsync(HttpContextCore context) + { + var existing = context.Features.GetRequiredFeature(); + var existingPipe = context.Features.Get(); + + using var inputStreamFeature = new HttpRequestInputStreamFeature(existing); + + context.Features.Set(inputStreamFeature); + context.Features.Set(inputStreamFeature); + context.Features.Set(inputStreamFeature); + context.Features.Set(inputStreamFeature); + + try + { + await next(context); + } + finally + { + context.Features.Set(existing); + context.Features.Set(existingPipe); + context.Features.Set(null); + context.Features.Set(null); + } + } +} diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/RequestUserFeaturesMiddleware.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/RequestUserFeaturesMiddleware.cs new file mode 100644 index 0000000000..fd04d1dd67 --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/RequestUserFeaturesMiddleware.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Security.Claims; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features.Authentication; +using Microsoft.AspNetCore.SystemWebAdapters.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.SystemWebAdapters.Middleware; + +internal sealed partial class RequestUserFeaturesMiddleware(RequestDelegate next, IOptions options, ILoggerFactory loggerFactory) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + private readonly bool _setCurrentAccessors = options.Value.EnableStaticUserAccessors; + + public async Task InvokeAsync(HttpContextCore context, IOptions options) + { + using var userFeature = new RequestUserFeature(_logger, _setCurrentAccessors) { User = context.User }; + + context.Features.Set(userFeature); + context.Features.Set(userFeature); + + try + { + await next(context); + } + finally + { + context.Features.Set(null); + context.Features.Set(null); + } + } + + private sealed partial class RequestUserFeature(ILogger logger, bool setCurrentAccessors) : IRequestUserFeature, IHttpAuthenticationFeature, IDisposable + { + private readonly ILogger logger = logger; + + [LoggerMessage(0, LogLevel.Debug, "A custom principal {PrincipalType} is being used and should be replaced with a ClaimsPrincipal derived type.")] + private partial void LogNonClaimsPrincipal(Type principalType); + + [LoggerMessage(1, LogLevel.Trace, "Thread.CurrentPrincipal has been set with the current user")] + private partial void LogCurrentPrincipalUsage(); + + public IPrincipal? User { get; set; } + + WindowsIdentity? IRequestUserFeature.LogonUserIdentity => User?.Identity as WindowsIdentity; + + ClaimsPrincipal? IHttpAuthenticationFeature.User + { + get => GetOrCreateClaims(User); + set + { + User = value; + EnsureCurrentPrincipalSetIfRequired(); + } + } + + private ClaimsPrincipal? GetOrCreateClaims(IPrincipal? principal) + { + if (principal is null) + { + return null; + } + + if (principal is ClaimsPrincipal claimsPrincipal) + { + return claimsPrincipal; + } + + LogNonClaimsPrincipal(principal.GetType()); + + return new ClaimsPrincipal(principal); + } + + public void EnableStaticAccessors() + { + LogCurrentPrincipalUsage(); + setCurrentAccessors = true; + EnsureCurrentPrincipalSetIfRequired(); + } + + private void EnsureCurrentPrincipalSetIfRequired() + { + if (setCurrentAccessors) + { + var claimsPrincipal = GetOrCreateClaims(User); + Thread.CurrentPrincipal = claimsPrincipal; + } + } + + public void Dispose() + { + if (setCurrentAccessors) + { + Thread.CurrentPrincipal = null; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/ResponseFeaturesMiddleware.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/ResponseFeaturesMiddleware.cs new file mode 100644 index 0000000000..0dadee8f9d --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/ResponseFeaturesMiddleware.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.SystemWebAdapters.Features; + +namespace Microsoft.AspNetCore.SystemWebAdapters.Middleware; + +internal sealed class ResponseFeaturesMiddleware(RequestDelegate next) +{ + public async Task InvokeAsync(HttpContextCore context) + { + var responseBodyFeature = context.Features.GetRequiredFeature(); + + using var adapterFeature = new HttpResponseAdapterFeature(responseBodyFeature); + + context.Features.Set(adapterFeature); + context.Features.Set(adapterFeature); + context.Features.Set(adapterFeature); + context.Features.Set(adapterFeature); + + try + { + await next(context); + } + finally + { + // The buffering feature may be removed if the response has ended i.e in usage with YARP + if (context.Features.Get() is { } buffer) + { + await buffer.FlushAsync(); + } + + context.Features.Set(responseBodyFeature); + context.Features.Set(null); + context.Features.Set(null); + context.Features.Set(null); + } + } +} diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SessionEventsMiddleware.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/SessionEventsMiddleware.cs similarity index 96% rename from src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SessionEventsMiddleware.cs rename to src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/SessionEventsMiddleware.cs index c8fadbdb9b..e8b2cd6887 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SessionEventsMiddleware.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/SessionEventsMiddleware.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.SystemWebAdapters.Features; -namespace Microsoft.AspNetCore.SystemWebAdapters; +namespace Microsoft.AspNetCore.SystemWebAdapters.Middleware; internal sealed class SessionEventsMiddleware { diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SessionMiddleware.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/SessionMiddleware.cs similarity index 100% rename from src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SessionMiddleware.cs rename to src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/SessionMiddleware.cs diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SessionStateMiddleware.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/SessionStateMiddleware.cs similarity index 96% rename from src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SessionStateMiddleware.cs rename to src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/SessionStateMiddleware.cs index 1ac73c5def..12cbbe7059 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SessionStateMiddleware.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/SessionStateMiddleware.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.SystemWebAdapters.Features; using Microsoft.AspNetCore.SystemWebAdapters.SessionState; -namespace Microsoft.AspNetCore.SystemWebAdapters; +namespace Microsoft.AspNetCore.SystemWebAdapters.Middleware; internal sealed class SessionStateMiddleware { diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SetHttpContextTimestampMiddleware.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/SetHttpContextTimestampMiddleware.cs similarity index 94% rename from src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SetHttpContextTimestampMiddleware.cs rename to src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/SetHttpContextTimestampMiddleware.cs index 1463f3387d..c304e2376a 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SetHttpContextTimestampMiddleware.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/SetHttpContextTimestampMiddleware.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.SystemWebAdapters.Features; -namespace Microsoft.AspNetCore.SystemWebAdapters; +namespace Microsoft.AspNetCore.SystemWebAdapters.Middleware; internal class SetHttpContextTimestampMiddleware { diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SingleThreadedRequestMiddleware.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/SingleThreadedRequestMiddleware.cs similarity index 93% rename from src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SingleThreadedRequestMiddleware.cs rename to src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/SingleThreadedRequestMiddleware.cs index 688bcee543..d5cf726c6e 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SingleThreadedRequestMiddleware.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/SingleThreadedRequestMiddleware.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.SystemWebAdapters; +namespace Microsoft.AspNetCore.SystemWebAdapters.Middleware; internal class SingleThreadedRequestMiddleware { diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/RegisterAdapterFeaturesMiddleware.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/RegisterAdapterFeaturesMiddleware.cs deleted file mode 100644 index e1a8d7ccd3..0000000000 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/RegisterAdapterFeaturesMiddleware.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Threading.Tasks; -using System.Web; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Http.Features.Authentication; -using Microsoft.AspNetCore.SystemWebAdapters; -using Microsoft.AspNetCore.SystemWebAdapters.Features; - -namespace Microsoft.Extensions.DependencyInjection; - -[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = Constants.DisposeIsRegistered)] -internal sealed class RegisterAdapterFeaturesMiddleware -{ - private readonly RequestDelegate _next; - - public RegisterAdapterFeaturesMiddleware(RequestDelegate next) - => _next = next; - - public async Task InvokeAsync(HttpContextCore context) - { - using (RegisterRequestFeatures(context)) - using (RegisterResponseFeatures(context)) - using (RegisterUserFeatures(context)) - { - try - { - await _next(context); - } - finally - { - // The buffering feature may be removed if the response has ended i.e in usage with YARP - if (context.Features.Get() is { } buffer) - { - await buffer.FlushAsync(); - } - } - } - } - - private static DelegateDisposable RegisterUserFeatures(HttpContextCore context) - { - var userFeature = new RequestUserFeature(context); - - context.Response.RegisterForDispose(userFeature); - context.Features.Set(userFeature); - context.Features.Set(userFeature); - - return new DelegateDisposable(() => - { - context.Features.Set(null); - context.Features.Set(null); - }); - } - - private static DelegateDisposable RegisterRequestFeatures(HttpContextCore context) - { - var existing = context.Features.GetRequiredFeature(); - var existingPipe = context.Features.Get(); - - var inputStreamFeature = new HttpRequestInputStreamFeature(existing); - - context.Response.RegisterForDispose(inputStreamFeature); - context.Features.Set(inputStreamFeature); - context.Features.Set(inputStreamFeature); - context.Features.Set(inputStreamFeature); - context.Features.Set(inputStreamFeature); - - return new DelegateDisposable(() => - { - context.Features.Set(existing); - context.Features.Set(existingPipe); - context.Features.Set(null); - context.Features.Set(null); - }); - } - - private static DelegateDisposable RegisterResponseFeatures(HttpContextCore context) - { - var responseBodyFeature = context.Features.GetRequiredFeature(); - - var adapterFeature = new HttpResponseAdapterFeature(responseBodyFeature); - - context.Features.Set(adapterFeature); - context.Features.Set(adapterFeature); - context.Features.Set(adapterFeature); - context.Features.Set(adapterFeature); - - context.Response.RegisterForDisposeAsync(adapterFeature); - - return new DelegateDisposable(() => - { - context.Features.Set(responseBodyFeature); - context.Features.Set(null); - context.Features.Set(null); - context.Features.Set(null); - }); - } - - private sealed class DelegateDisposable : IDisposable - { - private readonly Action _action; - - public DelegateDisposable(Action action) => _action = action; - - public void Dispose() => _action(); - } -} diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SystemWebAdaptersExtensions.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SystemWebAdaptersExtensions.cs index 21a269e4ed..2e532e6877 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SystemWebAdaptersExtensions.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SystemWebAdaptersExtensions.cs @@ -9,9 +9,12 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.SystemWebAdapters; using Microsoft.AspNetCore.SystemWebAdapters.Features; +using Microsoft.AspNetCore.SystemWebAdapters.Middleware; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using System.Security; +using System.Security.Claims; namespace Microsoft.Extensions.DependencyInjection; @@ -47,6 +50,19 @@ public static ISystemWebAdapterBuilder AddLoggingTraceContext(this ISystemWebAda return builder; } + /// + /// Enables all requests to have static accessors for and . + /// + /// System.Web adapter builder + public static ISystemWebAdapterBuilder AddStaticUserAccessors(this ISystemWebAdapterBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + + builder.Services.Configure(options => options.EnableStaticUserAccessors = true); + + return builder; + } + internal static bool HasBeenAdded(this IApplicationBuilder app, [CallerMemberName] string key = null!) { if (app.Properties.ContainsKey(key)) @@ -150,7 +166,9 @@ public Action Configure(Action next) builder.UseMiddleware(); } - builder.UseMiddleware(); + builder.UseMiddleware(); + builder.UseMiddleware(); + builder.UseMiddleware(); builder.UseMiddleware(); if (builder.AreHttpApplicationEventsRequired()) diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/SystemWebAdaptersOptions.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/SystemWebAdaptersOptions.cs index 56511ae805..be94709dc7 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/SystemWebAdaptersOptions.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/SystemWebAdaptersOptions.cs @@ -48,6 +48,11 @@ public class SystemWebAdaptersOptions /// public string AppDomainAppPath { get; set; } = AppContext.BaseDirectory; + /// + /// Gets or sets a value indicating whether to enable access to and for the duration of the request. + /// + public bool EnableStaticUserAccessors { get; set; } + /// /// Gets or sets the value used by . /// diff --git a/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/HttpRuntimeIntegrationTests.cs b/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/HttpRuntimeIntegrationTests.cs index ad7d1ef6b7..8b1468e4da 100644 --- a/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/HttpRuntimeIntegrationTests.cs +++ b/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/HttpRuntimeIntegrationTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.IIS; using Microsoft.AspNetCore.TestHost; @@ -15,7 +16,8 @@ namespace Microsoft.AspNetCore.SystemWebAdapters; -public class HttpRuntimeIntegrationTests : SelfHostedTestBase +[Collection(nameof(SelfHostedTests))] +public class HttpRuntimeIntegrationTests { private const string IIS_VERSION = "IIS_Version"; private const string IIS_SITE_ID = "IIS_SITE_ID"; @@ -31,26 +33,29 @@ public class HttpRuntimeIntegrationTests : SelfHostedTestBase public async Task ConfigureRuntimeViaConfig() { // Arrange - using var host = await GetTestHost() - .ConfigureAppConfiguration(config => - { - config.AddInMemoryCollection(new Dictionary - { - [IIS_VERSION] = "10.0", - [IIS_SITE_ID] = "1", - [IIS_APP_POOL_ID] = IIS_APP_POOL_ID, - [IIS_APP_POOL_CONFIG_FILE] = IIS_APP_POOL_CONFIG_FILE, - [IIS_APP_CONFIG_PATH] = IIS_APP_CONFIG_PATH, - [IIS_PHYSICAL_PATH] = IIS_PHYSICAL_PATH, - [IIS_APPLICATION_VIRTUAL_PATH] = IIS_APPLICATION_VIRTUAL_PATH, - [IIS_APPLICATION_ID] = IIS_APPLICATION_ID, - [IIS_SITE_NAME] = IIS_SITE_NAME, - }); - }) - .StartAsync(); + var builder = WebApplication.CreateBuilder(); + + builder.WebHost.UseTestServer(); + builder.Services.AddSystemWebAdapters(); + builder.Configuration.AddInMemoryCollection(new Dictionary + { + [IIS_VERSION] = "10.0", + [IIS_SITE_ID] = "1", + [IIS_APP_POOL_ID] = IIS_APP_POOL_ID, + [IIS_APP_POOL_CONFIG_FILE] = IIS_APP_POOL_CONFIG_FILE, + [IIS_APP_CONFIG_PATH] = IIS_APP_CONFIG_PATH, + [IIS_PHYSICAL_PATH] = IIS_PHYSICAL_PATH, + [IIS_APPLICATION_VIRTUAL_PATH] = IIS_APPLICATION_VIRTUAL_PATH, + [IIS_APPLICATION_ID] = IIS_APPLICATION_ID, + [IIS_SITE_NAME] = IIS_SITE_NAME, + }); + + using var app = builder.Build(); + + await app.StartAsync(); // Act - var options = host.Services.GetRequiredService>().Value; + var options = app.Services.GetRequiredService>().Value; // Assert Assert.Equal(IIS_SITE_NAME, options.SiteName); @@ -76,13 +81,30 @@ public async Task ConfigureRuntimeViaFeature() feature.Setup(f => f.AppPoolConfigFile).Returns(IIS_APP_POOL_CONFIG_FILE); feature.Setup(f => f.AppPoolId).Returns(IIS_APP_POOL_ID); - using var host = await GetTestHost() - .StartAsync(); + var builder = WebApplication.CreateBuilder(); + + builder.WebHost.UseTestServer(); + builder.Services.AddSystemWebAdapters(); + builder.Configuration.AddInMemoryCollection(new Dictionary + { + [IIS_VERSION] = "10.0", + [IIS_SITE_ID] = "1", + [IIS_APP_POOL_ID] = IIS_APP_POOL_ID, + [IIS_APP_POOL_CONFIG_FILE] = IIS_APP_POOL_CONFIG_FILE, + [IIS_APP_CONFIG_PATH] = IIS_APP_CONFIG_PATH, + [IIS_PHYSICAL_PATH] = IIS_PHYSICAL_PATH, + [IIS_APPLICATION_VIRTUAL_PATH] = IIS_APPLICATION_VIRTUAL_PATH, + [IIS_APPLICATION_ID] = IIS_APPLICATION_ID, + [IIS_SITE_NAME] = IIS_SITE_NAME, + }); + + using var app = builder.Build(); - host.GetTestServer().Features.Set(feature.Object); + // Must be set before calling anything that will build the options (i.e. including starting the app) + app.GetTestServer().Features.Set(feature.Object); // Act - var options = host.Services.GetRequiredService>().Value; + var options = app.Services.GetRequiredService>().Value; // Assert Assert.Equal(IIS_SITE_NAME, options.SiteName); diff --git a/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/SelfHostedTestBase.cs b/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/SelfHostedTestBase.cs deleted file mode 100644 index 1df2ccaa14..0000000000 --- a/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/SelfHostedTestBase.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Xunit; - -namespace Microsoft.AspNetCore.SystemWebAdapters; - -[Collection(nameof(SelfHostedTests))] -public class SelfHostedTestBase -{ - protected static IHostBuilder GetTestHost() - => new HostBuilder() - .ConfigureWebHost(webBuilder => - { - webBuilder - .UseTestServer() - .ConfigureServices(services => - { - services.AddSystemWebAdapters(); - }) - .Configure(app => - { - // No need to configure pipeline for tests - }); - }); -} From 15ba1e513a2ae71667876c8c9cddb2cd4250dcc0 Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Tue, 11 Nov 2025 12:18:58 -0800 Subject: [PATCH 4/5] clean up --- .../Middleware/CurrentPrincipalMiddleware.cs | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/CurrentPrincipalMiddleware.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/CurrentPrincipalMiddleware.cs index a0e3f89cd7..57a6bc8df7 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/CurrentPrincipalMiddleware.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/CurrentPrincipalMiddleware.cs @@ -1,33 +1,22 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.SystemWebAdapters.Features; -using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.SystemWebAdapters.Middleware; -internal sealed class CurrentPrincipalMiddleware +internal sealed class CurrentPrincipalMiddleware(RequestDelegate next) { - private readonly RequestDelegate _next; - private readonly ILogger _logger; - - public CurrentPrincipalMiddleware(RequestDelegate next, ILogger logger) - { - _next = next; - _logger = logger; - } - - public Task InvokeAsync(HttpContext context) + public Task InvokeAsync(HttpContextCore context) { if (context.GetEndpoint()?.Metadata.GetMetadata() is { IsDisabled: false }) { context.Features.GetRequiredFeature().EnableStaticAccessors(); } - return _next(context); + return next(context); } } From 342f9387967136e5b2d1c0e0a9b70faa387d4eb9 Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Tue, 11 Nov 2025 12:30:36 -0800 Subject: [PATCH 5/5] remove unnecessary usings --- .../Middleware/RequestUserFeaturesMiddleware.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/RequestUserFeaturesMiddleware.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/RequestUserFeaturesMiddleware.cs index fd04d1dd67..6fe258fd3b 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/RequestUserFeaturesMiddleware.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/RequestUserFeaturesMiddleware.cs @@ -9,9 +9,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features.Authentication; using Microsoft.AspNetCore.SystemWebAdapters.Features; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.SystemWebAdapters.Middleware;