Skip to content

Commit f6c3f9e

Browse files
authored
Avoid calling AuthenticateAsync for IIS out-of-proc #7750 (#11390)
1 parent 5ce20eb commit f6c3f9e

File tree

4 files changed

+86
-54
lines changed

4 files changed

+86
-54
lines changed

src/Servers/IIS/IISIntegration/src/AuthenticationHandler.cs

Lines changed: 5 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,31 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
using System;
5-
using System.Security.Claims;
6-
using System.Globalization;
74
using System.Security.Principal;
85
using System.Threading.Tasks;
96
using Microsoft.AspNetCore.Authentication;
107
using Microsoft.AspNetCore.Http;
11-
using Microsoft.Extensions.Primitives;
128

139
namespace Microsoft.AspNetCore.Server.IISIntegration
1410
{
1511
internal class AuthenticationHandler : IAuthenticationHandler
1612
{
17-
private const string MSAspNetCoreWinAuthToken = "MS-ASPNETCORE-WINAUTHTOKEN";
18-
private static readonly Func<object, Task> ClearUserDelegate = ClearUser;
1913
private WindowsPrincipal _user;
2014
private HttpContext _context;
21-
22-
internal AuthenticationScheme Scheme { get; private set; }
15+
private AuthenticationScheme _scheme;
2316

2417
public Task<AuthenticateResult> AuthenticateAsync()
2518
{
26-
var user = GetUser();
27-
if (user != null)
19+
if (_user != null)
2820
{
29-
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(user, Scheme.Name)));
21+
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(_user, _scheme.Name)));
3022
}
3123
else
3224
{
3325
return Task.FromResult(AuthenticateResult.NoResult());
3426
}
3527
}
3628

37-
private WindowsPrincipal GetUser()
38-
{
39-
if (_user == null)
40-
{
41-
var tokenHeader = _context.Request.Headers[MSAspNetCoreWinAuthToken];
42-
43-
int hexHandle;
44-
if (!StringValues.IsNullOrEmpty(tokenHeader)
45-
&& int.TryParse(tokenHeader, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out hexHandle))
46-
{
47-
// Always create the identity if the handle exists, we need to dispose it so it does not leak.
48-
var handle = new IntPtr(hexHandle);
49-
var winIdentity = new WindowsIdentity(handle, IISDefaults.AuthenticationScheme);
50-
51-
// WindowsIdentity just duplicated the handle so we need to close the original.
52-
NativeMethods.CloseHandle(handle);
53-
54-
_context.Response.RegisterForDispose(winIdentity);
55-
// We don't want loggers accessing a disposed identity.
56-
// https://github.com/aspnet/Logging/issues/543#issuecomment-321907828
57-
_context.Response.OnCompleted(ClearUserDelegate, _context);
58-
_user = new WindowsPrincipal(winIdentity);
59-
}
60-
}
61-
62-
return _user;
63-
}
64-
65-
private static Task ClearUser(object arg)
66-
{
67-
var context = (HttpContext)arg;
68-
if (context.User is WindowsPrincipal)
69-
{
70-
context.User = null;
71-
}
72-
return Task.CompletedTask;
73-
}
74-
7529
public Task ChallengeAsync(AuthenticationProperties properties)
7630
{
7731
// We would normally set the www-authenticate header here, but IIS does that for us.
@@ -87,8 +41,9 @@ public Task ForbidAsync(AuthenticationProperties properties)
8741

8842
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
8943
{
90-
Scheme = scheme;
44+
_scheme = scheme;
9145
_context = context;
46+
_user = context.Features.Get<WindowsPrincipal>(); // See IISMiddleware
9247
return Task.CompletedTask;
9348
}
9449
}

src/Servers/IIS/IISIntegration/src/IISMiddleware.cs

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33

44
using System;
55
using System.Diagnostics;
6+
using System.Globalization;
7+
using System.Security.Principal;
68
using System.Threading.Tasks;
79
using Microsoft.AspNetCore.Authentication;
810
using Microsoft.AspNetCore.Builder;
9-
using Microsoft.AspNetCore.Hosting;
1011
using Microsoft.AspNetCore.Http;
1112
using Microsoft.AspNetCore.Http.Features;
1213
using Microsoft.Extensions.Hosting;
@@ -21,8 +22,10 @@ public class IISMiddleware
2122
private const string MSAspNetCoreClientCert = "MS-ASPNETCORE-CLIENTCERT";
2223
private const string MSAspNetCoreToken = "MS-ASPNETCORE-TOKEN";
2324
private const string MSAspNetCoreEvent = "MS-ASPNETCORE-EVENT";
25+
private const string MSAspNetCoreWinAuthToken = "MS-ASPNETCORE-WINAUTHTOKEN";
2426
private const string ANCMShutdownEventHeaderValue = "shutdown";
2527
private static readonly PathString ANCMRequestPath = new PathString("/iisintegration");
28+
private static readonly Func<object, Task> ClearUserDelegate = ClearUser;
2629

2730
private readonly RequestDelegate _next;
2831
private readonly IISOptions _options;
@@ -131,10 +134,16 @@ public async Task Invoke(HttpContext httpContext)
131134
if (_options.ForwardWindowsAuthentication)
132135
{
133136
// We must always process and clean up the windows identity, even if we don't assign the User.
134-
var result = await httpContext.AuthenticateAsync(IISDefaults.AuthenticationScheme);
135-
if (result.Succeeded && _options.AutomaticAuthentication)
137+
var user = GetUser(httpContext);
138+
if (user != null)
136139
{
137-
httpContext.User = result.Principal;
140+
// Flow it through to the authentication handler.
141+
httpContext.Features.Set(user);
142+
143+
if (_options.AutomaticAuthentication)
144+
{
145+
httpContext.User = user;
146+
}
138147
}
139148
}
140149

@@ -147,5 +156,39 @@ public async Task Invoke(HttpContext httpContext)
147156

148157
await _next(httpContext);
149158
}
159+
160+
private WindowsPrincipal GetUser(HttpContext context)
161+
{
162+
var tokenHeader = context.Request.Headers[MSAspNetCoreWinAuthToken];
163+
164+
if (!StringValues.IsNullOrEmpty(tokenHeader)
165+
&& int.TryParse(tokenHeader, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hexHandle))
166+
{
167+
// Always create the identity if the handle exists, we need to dispose it so it does not leak.
168+
var handle = new IntPtr(hexHandle);
169+
var winIdentity = new WindowsIdentity(handle, IISDefaults.AuthenticationScheme);
170+
171+
// WindowsIdentity just duplicated the handle so we need to close the original.
172+
NativeMethods.CloseHandle(handle);
173+
174+
context.Response.OnCompleted(ClearUserDelegate, context);
175+
context.Response.RegisterForDispose(winIdentity);
176+
return new WindowsPrincipal(winIdentity);
177+
}
178+
179+
return null;
180+
}
181+
182+
private static Task ClearUser(object arg)
183+
{
184+
var context = (HttpContext)arg;
185+
// We don't want loggers accessing a disposed identity.
186+
// https://github.com/aspnet/Logging/issues/543#issuecomment-321907828
187+
if (context.User is WindowsPrincipal)
188+
{
189+
context.User = null;
190+
}
191+
return Task.CompletedTask;
192+
}
150193
}
151194
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Security.Claims;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Authentication;
8+
using Microsoft.AspNetCore.Http;
9+
10+
namespace ServerComparison.TestSites
11+
{
12+
public class OneTransformPerRequest : IClaimsTransformation
13+
{
14+
public OneTransformPerRequest(IHttpContextAccessor contextAccessor)
15+
{
16+
ContextAccessor = contextAccessor;
17+
}
18+
19+
public IHttpContextAccessor ContextAccessor { get; }
20+
21+
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
22+
{
23+
var context = ContextAccessor.HttpContext;
24+
if (context.Items["Transformed"] != null)
25+
{
26+
throw new InvalidOperationException("Transformation ran multiple times.");
27+
}
28+
context.Items["Transformed"] = true;
29+
return Task.FromResult(principal);
30+
}
31+
}
32+
}

src/Servers/testassets/ServerComparison.TestSites/StartupNtlmAuthentication.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ public StartupNtlmAuthentication(IConfiguration configuration)
2525

2626
public void ConfigureServices(IServiceCollection services)
2727
{
28+
services.AddHttpContextAccessor();
29+
services.AddSingleton<IClaimsTransformation, OneTransformPerRequest>();
2830
if (IsKestrel)
2931
{
3032
services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)

0 commit comments

Comments
 (0)