Avoid calling AuthenticateAsync for IIS out-of-proc #7750 (#11390)

This commit is contained in:
Chris Ross 2019-06-20 21:53:09 -07:00 committed by GitHub
parent 5ce20ebdde
commit f6c3f9e02a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 86 additions and 54 deletions

View File

@ -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<object, Task> ClearUserDelegate = ClearUser;
private WindowsPrincipal _user;
private HttpContext _context;
internal AuthenticationScheme Scheme { get; private set; }
private AuthenticationScheme _scheme;
public Task<AuthenticateResult> 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<WindowsPrincipal>(); // See IISMiddleware
return Task.CompletedTask;
}
}

View File

@ -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<object, Task> 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;
}
}
}

View File

@ -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<ClaimsPrincipal> 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);
}
}
}

View File

@ -25,6 +25,8 @@ namespace ServerComparison.TestSites
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddSingleton<IClaimsTransformation, OneTransformPerRequest>();
if (IsKestrel)
{
services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)