diff --git a/src/Servers/IIS/IISIntegration/src/AuthenticationHandler.cs b/src/Servers/IIS/IISIntegration/src/AuthenticationHandler.cs index 1a660b490f..075705000a 100644 --- a/src/Servers/IIS/IISIntegration/src/AuthenticationHandler.cs +++ b/src/Servers/IIS/IISIntegration/src/AuthenticationHandler.cs @@ -1,32 +1,24 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Security.Claims; -using System.Globalization; using System.Security.Principal; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Server.IISIntegration { internal class AuthenticationHandler : IAuthenticationHandler { - private const string MSAspNetCoreWinAuthToken = "MS-ASPNETCORE-WINAUTHTOKEN"; - private static readonly Func ClearUserDelegate = ClearUser; private WindowsPrincipal _user; private HttpContext _context; - - internal AuthenticationScheme Scheme { get; private set; } + private AuthenticationScheme _scheme; public Task AuthenticateAsync() { - var user = GetUser(); - if (user != null) + if (_user != null) { - return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(user, Scheme.Name))); + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(_user, _scheme.Name))); } else { @@ -34,44 +26,6 @@ namespace Microsoft.AspNetCore.Server.IISIntegration } } - private WindowsPrincipal GetUser() - { - if (_user == null) - { - var tokenHeader = _context.Request.Headers[MSAspNetCoreWinAuthToken]; - - int hexHandle; - if (!StringValues.IsNullOrEmpty(tokenHeader) - && int.TryParse(tokenHeader, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out hexHandle)) - { - // Always create the identity if the handle exists, we need to dispose it so it does not leak. - var handle = new IntPtr(hexHandle); - var winIdentity = new WindowsIdentity(handle, IISDefaults.AuthenticationScheme); - - // WindowsIdentity just duplicated the handle so we need to close the original. - NativeMethods.CloseHandle(handle); - - _context.Response.RegisterForDispose(winIdentity); - // We don't want loggers accessing a disposed identity. - // https://github.com/aspnet/Logging/issues/543#issuecomment-321907828 - _context.Response.OnCompleted(ClearUserDelegate, _context); - _user = new WindowsPrincipal(winIdentity); - } - } - - return _user; - } - - private static Task ClearUser(object arg) - { - var context = (HttpContext)arg; - if (context.User is WindowsPrincipal) - { - context.User = null; - } - return Task.CompletedTask; - } - public Task ChallengeAsync(AuthenticationProperties properties) { // We would normally set the www-authenticate header here, but IIS does that for us. @@ -87,8 +41,9 @@ namespace Microsoft.AspNetCore.Server.IISIntegration public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) { - Scheme = scheme; + _scheme = scheme; _context = context; + _user = context.Features.Get(); // See IISMiddleware return Task.CompletedTask; } } diff --git a/src/Servers/IIS/IISIntegration/src/IISMiddleware.cs b/src/Servers/IIS/IISIntegration/src/IISMiddleware.cs index 77a4211b74..f0596d40cc 100644 --- a/src/Servers/IIS/IISIntegration/src/IISMiddleware.cs +++ b/src/Servers/IIS/IISIntegration/src/IISMiddleware.cs @@ -3,10 +3,11 @@ using System; using System.Diagnostics; +using System.Globalization; +using System.Security.Principal; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Hosting; @@ -21,8 +22,10 @@ namespace Microsoft.AspNetCore.Server.IISIntegration private const string MSAspNetCoreClientCert = "MS-ASPNETCORE-CLIENTCERT"; private const string MSAspNetCoreToken = "MS-ASPNETCORE-TOKEN"; private const string MSAspNetCoreEvent = "MS-ASPNETCORE-EVENT"; + private const string MSAspNetCoreWinAuthToken = "MS-ASPNETCORE-WINAUTHTOKEN"; private const string ANCMShutdownEventHeaderValue = "shutdown"; private static readonly PathString ANCMRequestPath = new PathString("/iisintegration"); + private static readonly Func ClearUserDelegate = ClearUser; private readonly RequestDelegate _next; private readonly IISOptions _options; @@ -131,10 +134,16 @@ namespace Microsoft.AspNetCore.Server.IISIntegration if (_options.ForwardWindowsAuthentication) { // We must always process and clean up the windows identity, even if we don't assign the User. - var result = await httpContext.AuthenticateAsync(IISDefaults.AuthenticationScheme); - if (result.Succeeded && _options.AutomaticAuthentication) + var user = GetUser(httpContext); + if (user != null) { - httpContext.User = result.Principal; + // Flow it through to the authentication handler. + httpContext.Features.Set(user); + + if (_options.AutomaticAuthentication) + { + httpContext.User = user; + } } } @@ -147,5 +156,39 @@ namespace Microsoft.AspNetCore.Server.IISIntegration await _next(httpContext); } + + private WindowsPrincipal GetUser(HttpContext context) + { + var tokenHeader = context.Request.Headers[MSAspNetCoreWinAuthToken]; + + if (!StringValues.IsNullOrEmpty(tokenHeader) + && int.TryParse(tokenHeader, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hexHandle)) + { + // Always create the identity if the handle exists, we need to dispose it so it does not leak. + var handle = new IntPtr(hexHandle); + var winIdentity = new WindowsIdentity(handle, IISDefaults.AuthenticationScheme); + + // WindowsIdentity just duplicated the handle so we need to close the original. + NativeMethods.CloseHandle(handle); + + context.Response.OnCompleted(ClearUserDelegate, context); + context.Response.RegisterForDispose(winIdentity); + return new WindowsPrincipal(winIdentity); + } + + return null; + } + + private static Task ClearUser(object arg) + { + var context = (HttpContext)arg; + // We don't want loggers accessing a disposed identity. + // https://github.com/aspnet/Logging/issues/543#issuecomment-321907828 + if (context.User is WindowsPrincipal) + { + context.User = null; + } + return Task.CompletedTask; + } } } diff --git a/src/Servers/testassets/ServerComparison.TestSites/OneTransformPerRequest.cs b/src/Servers/testassets/ServerComparison.TestSites/OneTransformPerRequest.cs new file mode 100644 index 0000000000..47d392d9d6 --- /dev/null +++ b/src/Servers/testassets/ServerComparison.TestSites/OneTransformPerRequest.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; + +namespace ServerComparison.TestSites +{ + public class OneTransformPerRequest : IClaimsTransformation + { + public OneTransformPerRequest(IHttpContextAccessor contextAccessor) + { + ContextAccessor = contextAccessor; + } + + public IHttpContextAccessor ContextAccessor { get; } + + public Task TransformAsync(ClaimsPrincipal principal) + { + var context = ContextAccessor.HttpContext; + if (context.Items["Transformed"] != null) + { + throw new InvalidOperationException("Transformation ran multiple times."); + } + context.Items["Transformed"] = true; + return Task.FromResult(principal); + } + } +} diff --git a/src/Servers/testassets/ServerComparison.TestSites/StartupNtlmAuthentication.cs b/src/Servers/testassets/ServerComparison.TestSites/StartupNtlmAuthentication.cs index 73810a6b80..5528d19578 100644 --- a/src/Servers/testassets/ServerComparison.TestSites/StartupNtlmAuthentication.cs +++ b/src/Servers/testassets/ServerComparison.TestSites/StartupNtlmAuthentication.cs @@ -25,6 +25,8 @@ namespace ServerComparison.TestSites public void ConfigureServices(IServiceCollection services) { + services.AddHttpContextAccessor(); + services.AddSingleton(); if (IsKestrel) { services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)