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 deleted file mode 100644 index 95334ac518..0000000000 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/CurrentPrincipalMiddleware.cs +++ /dev/null @@ -1,45 +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.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.SystemWebAdapters; - -internal partial 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; - - public CurrentPrincipalMiddleware(RequestDelegate next, ILogger logger) - { - _next = next; - _logger = logger; - } - - public Task InvokeAsync(HttpContext context) - => context.GetEndpoint()?.Metadata.GetMetadata() is { IsDisabled: false } ? SetUserAsync(context) : _next(context); - - private async Task SetUserAsync(HttpContext context) - { - LogCurrentPrincipalUsage(); - - var current = Thread.CurrentPrincipal; - - try - { - Thread.CurrentPrincipal = context.User; - - await _next(context); - } - finally - { - Thread.CurrentPrincipal = current; - } - } -} 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/Middleware/CurrentPrincipalMiddleware.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/CurrentPrincipalMiddleware.cs new file mode 100644 index 0000000000..57a6bc8df7 --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/CurrentPrincipalMiddleware.cs @@ -0,0 +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.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.SystemWebAdapters.Features; + +namespace Microsoft.AspNetCore.SystemWebAdapters.Middleware; + +internal sealed class CurrentPrincipalMiddleware(RequestDelegate next) +{ + public Task InvokeAsync(HttpContextCore context) + { + if (context.GetEndpoint()?.Metadata.GetMetadata() is { IsDisabled: false }) + { + context.Features.GetRequiredFeature().EnableStaticAccessors(); + } + + return next(context); + } +} 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..6fe258fd3b --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Middleware/RequestUserFeaturesMiddleware.cs @@ -0,0 +1,105 @@ +// 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.Logging; +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 39ee909646..0000000000 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/RegisterAdapterFeaturesMiddleware.cs +++ /dev/null @@ -1,94 +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.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)) - { - 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 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/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/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 8e6c92a257..0000000000 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/Internal/RequestUserExtensions.cs +++ /dev/null @@ -1,75 +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 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.Features.Set(newFeature); - context.Features.Set(newFeature); - - return newFeature; - } - - private sealed partial class RequestUserFeature : IRequestUserFeature, IHttpAuthenticationFeature - { - private readonly ILogger _logger; - - 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); - - public IPrincipal? User { get; set; } - - WindowsIdentity? IRequestUserFeature.LogonUserIdentity => User?.Identity as WindowsIdentity; - - ClaimsPrincipal? IHttpAuthenticationFeature.User - { - get => GetOrCreateClaims(User); - set => User = value; - } - - 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); - } - } -} 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 - }); - }); -} 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; } + } } 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); + } + } } }