diff --git a/Microsoft.AspNetCore.SystemWebAdapters.sln b/Microsoft.AspNetCore.SystemWebAdapters.sln index 45b045fd5d..f4f0509573 100644 --- a/Microsoft.AspNetCore.SystemWebAdapters.sln +++ b/Microsoft.AspNetCore.SystemWebAdapters.sln @@ -100,6 +100,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.System EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SystemWebAdapters.Apis.Tests", "test\Microsoft.AspNetCore.SystemWebAdapters.Apis.Tests\Microsoft.AspNetCore.SystemWebAdapters.Apis.Tests.csproj", "{E4D9A131-DC4E-403F-A10F-65F5E5E42475}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsAuth", "samples\WindowsAuth\WindowsAuth.csproj", "{B5E840F8-2021-4175-BFBF-F9447506242E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -222,6 +224,10 @@ Global {E4D9A131-DC4E-403F-A10F-65F5E5E42475}.Debug|Any CPU.Build.0 = Debug|Any CPU {E4D9A131-DC4E-403F-A10F-65F5E5E42475}.Release|Any CPU.ActiveCfg = Release|Any CPU {E4D9A131-DC4E-403F-A10F-65F5E5E42475}.Release|Any CPU.Build.0 = Release|Any CPU + {B5E840F8-2021-4175-BFBF-F9447506242E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5E840F8-2021-4175-BFBF-F9447506242E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5E840F8-2021-4175-BFBF-F9447506242E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5E840F8-2021-4175-BFBF-F9447506242E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -264,6 +270,7 @@ Global {FA39AC22-0725-4532-A682-B054ADA5BDA2} = {03E4CEAB-D845-402A-9311-F7390B24BA2A} {17055F45-E79A-41EF-825E-0B2211433729} = {A1BDA50C-D70B-416C-97F1-74B0649797C5} {E4D9A131-DC4E-403F-A10F-65F5E5E42475} = {A1BDA50C-D70B-416C-97F1-74B0649797C5} + {B5E840F8-2021-4175-BFBF-F9447506242E} = {95915611-30BF-4AFF-AE41-5CDC6F57DCF7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DABA3C65-9D74-4EB6-9B1C-730328710EAD} diff --git a/samples/WindowsAuth/Program.cs b/samples/WindowsAuth/Program.cs new file mode 100644 index 0000000000..f2b946c50c --- /dev/null +++ b/samples/WindowsAuth/Program.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Authentication.Negotiate; +using Microsoft.AspNetCore.SystemWebAdapters; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme) + .AddNegotiate(); + +var app = builder.Build(); + +app.UseAuthentication(); + +app.MapGet("/", (HttpContext context) => +{ + return new + { + User = context.User.Identity?.Name, + Principal = context.AsSystemWeb().User.Identity?.Name, + LogonUser = OperatingSystem.IsWindows() ? context.AsSystemWeb().Request.LogonUserIdentity?.Name : null, + }; +}); + +app.Run(); diff --git a/samples/WindowsAuth/Properties/launchSettings.json b/samples/WindowsAuth/Properties/launchSettings.json new file mode 100644 index 0000000000..a80f7522bf --- /dev/null +++ b/samples/WindowsAuth/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": true, + "anonymousAuthentication": false, + "iisExpress": { + "applicationUrl": "http://localhost:50790", + "sslPort": 44383 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5041", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7245;http://localhost:5041", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/WindowsAuth/WindowsAuth.csproj b/samples/WindowsAuth/WindowsAuth.csproj new file mode 100644 index 0000000000..8b288ff812 --- /dev/null +++ b/samples/WindowsAuth/WindowsAuth.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/samples/WindowsAuth/appsettings.Development.json b/samples/WindowsAuth/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/samples/WindowsAuth/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/WindowsAuth/appsettings.json b/samples/WindowsAuth/appsettings.json new file mode 100644 index 0000000000..10f68b8c8b --- /dev/null +++ b/samples/WindowsAuth/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/Features/IPrincipalUserFeature.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/Features/IRequestUserFeature.cs similarity index 53% rename from src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/Features/IPrincipalUserFeature.cs rename to src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/Features/IRequestUserFeature.cs index a626a06dc8..20191c197d 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/Features/IPrincipalUserFeature.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/Features/IRequestUserFeature.cs @@ -5,6 +5,8 @@ using Microsoft.AspNetCore.Http.Features.Authentication; using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; using System.Security.Claims; using System.Security.Principal; using System.Web; @@ -12,13 +14,20 @@ namespace Microsoft.AspNetCore.SystemWebAdapters.Features; /// -/// Represents the user as an as opposed to the in-built which -/// expects a . +/// Represents the users that ASP.NET Framework used. /// [Experimental(Constants.ExperimentalFeatures.DiagnosticId)] -public interface IPrincipalUserFeature +public interface IRequestUserFeature { + /// + /// Gets or sets the user that corresponds to + /// IPrincipal? User { get; set; } + + /// + /// Gets the logged on user that corresponds to + /// + WindowsIdentity? LogonUserIdentity { get; } } #endif diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs index e0de79955d..4c73768a4d 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs @@ -439,6 +439,7 @@ internal HttpRequest() { } public bool IsLocal { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public bool IsSecureConnection { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public string this[string key] { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public System.Security.Principal.WindowsIdentity LogonUserIdentity { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public System.Collections.Specialized.NameValueCollection Params { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public string Path { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public string PathInfo { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } @@ -483,6 +484,7 @@ public abstract partial class HttpRequestBase public virtual bool IsLocal { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public virtual bool IsSecureConnection { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public virtual string this[string key] { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public virtual System.Security.Principal.WindowsIdentity LogonUserIdentity { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public virtual System.Collections.Specialized.NameValueCollection Params { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public virtual string Path { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public virtual string PathInfo { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } @@ -527,6 +529,7 @@ public partial class HttpRequestWrapper : System.Web.HttpRequestBase public override bool IsLocal { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public override bool IsSecureConnection { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public override string this[string key] { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public override System.Security.Principal.WindowsIdentity LogonUserIdentity { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public override System.Collections.Specialized.NameValueCollection Params { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public override string Path { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public override string PathInfo { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContext.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContext.cs index 60450ab52b..8a88de30e5 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContext.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContext.cs @@ -99,21 +99,8 @@ public IHttpHandler? Handler public IPrincipal User { - get => Context.Features.Get()?.User ?? Context.User; - set - { - if (Context.Features.Get() is { } feature) - { - feature.User = value; - } - else - { - var newFeature = new PrincipalUserFeature(Context) { User = value }; - - Context.Features.Set(newFeature); - Context.Features.Set(newFeature); - } - } + get => Context.Features.Get()?.User ?? Context.User; + set => Context.GetRequestUser().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 1c8d1b95d1..c00443a7e0 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequest.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequest.cs @@ -57,6 +57,8 @@ internal HttpRequest(HttpRequestCore request) public string CurrentExecutionFilePath => Request.HttpContext.Features.GetRequired().CurrentExecutionFilePath; + public WindowsIdentity? LogonUserIdentity => Request.HttpContext.GetRequestUser().LogonUserIdentity; + public NameValueCollection Headers => _headers ??= Request.Headers.ToNameValueCollection(); public Uri Url => new(Request.GetEncodedUrl()); diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequestBase.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequestBase.cs index a1b124bc5d..b990031ac1 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequestBase.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequestBase.cs @@ -26,6 +26,8 @@ public abstract class HttpRequestBase public virtual string? FilePath => throw new NotImplementedException(); + public virtual WindowsIdentity? LogonUserIdentity => throw new NotImplementedException(); + public virtual ReadEntityBodyMode ReadEntityBodyMode => throw new NotImplementedException(); public virtual void SaveAs(string filename, bool includeHeaders) => throw new NotImplementedException(); diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequestWrapper.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequestWrapper.cs index 17cced4ff6..0e852afe0b 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequestWrapper.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequestWrapper.cs @@ -3,6 +3,7 @@ using System.Collections.Specialized; using System.IO; +using System.Security.Principal; using System.Text; namespace System.Web @@ -42,6 +43,8 @@ public override string? ContentType public override NameValueCollection Form => _request.Form; + public override WindowsIdentity? LogonUserIdentity => _request.LogonUserIdentity; + public override string HttpMethod => _request.HttpMethod; public override Stream InputStream => _request.InputStream; diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Internal/PrincipalUserFeature.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Internal/PrincipalUserFeature.cs deleted file mode 100644 index fd09dc585a..0000000000 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/Internal/PrincipalUserFeature.cs +++ /dev/null @@ -1,53 +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 sealed partial class PrincipalUserFeature : IPrincipalUserFeature, IHttpAuthenticationFeature -{ - private readonly ILogger _logger; - - public PrincipalUserFeature(HttpContextCore context) - { - var logger = (ILogger?)context.RequestServices?.GetService()?.CreateLogger(); - - _logger = logger ?? NullLogger.Instance; - } - - [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; } - - 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/Internal/RequestUserExtensions.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Internal/RequestUserExtensions.cs new file mode 100644 index 0000000000..8e6c92a257 --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Internal/RequestUserExtensions.cs @@ -0,0 +1,75 @@ +// 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/Microsoft.AspNetCore.SystemWebAdapters.csproj b/src/Microsoft.AspNetCore.SystemWebAdapters/Microsoft.AspNetCore.SystemWebAdapters.csproj index 2508a38c34..b7d2598197 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/Microsoft.AspNetCore.SystemWebAdapters.csproj +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Microsoft.AspNetCore.SystemWebAdapters.csproj @@ -31,6 +31,8 @@ + + diff --git a/test/Microsoft.AspNetCore.SystemWebAdapters.Apis.Tests/VerifyMembersHaveSameSignature.cs b/test/Microsoft.AspNetCore.SystemWebAdapters.Apis.Tests/VerifyMembersHaveSameSignature.cs index e9fd92cedc..2f7318ec13 100644 --- a/test/Microsoft.AspNetCore.SystemWebAdapters.Apis.Tests/VerifyMembersHaveSameSignature.cs +++ b/test/Microsoft.AspNetCore.SystemWebAdapters.Apis.Tests/VerifyMembersHaveSameSignature.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using Basic.Reference.Assemblies; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -23,8 +24,12 @@ public VerifyMembersHaveSameSignature(ITestOutputHelper output) [Fact] public void VerifyMembersOnTypesAreConsistent() { - var netstandard = GetDocumentationIds("adapters/netstandard/Microsoft.AspNetCore.SystemWebAdapters.dll", NetStandard20.References.All); - var framework = GetDocumentationIds("adapters/netfx/Microsoft.AspNetCore.SystemWebAdapters.dll", Net472.References.All); + var netstandard = GetDocumentationIds( + "adapters/netstandard/Microsoft.AspNetCore.SystemWebAdapters.dll", + NetStandard20.References.All.Concat(GetAdditionalNetStandardReferences())); + var framework = GetDocumentationIds( + "adapters/netfx/Microsoft.AspNetCore.SystemWebAdapters.dll", + Net472.References.All); var ok = File.ReadAllLines("BaselineOk.txt"); netstandard.ExceptWith(framework); @@ -38,6 +43,12 @@ public void VerifyMembersOnTypesAreConsistent() Assert.Empty(netstandard); } + private static IEnumerable GetAdditionalNetStandardReferences() + { + // Even though this isn't the exact reference assembly for .NET Standard, it is sufficient to be able to reconstruct docids + yield return ReferenceAssemblies.Net60.Single(p => p.FilePath == "System.Security.Principal.Windows.dll"); + } + public static HashSet GetDocumentationIds(string path, IEnumerable referenceAssemblies) { var _adapter = MetadataReference.CreateFromFile(path);