Skip to content

Commit dbf84ea

Browse files
Add a feature for accessing the AuthenticateResult (#33408)
1 parent 51a3073 commit dbf84ea

File tree

9 files changed

+447
-11
lines changed

9 files changed

+447
-11
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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 Microsoft.AspNetCore.Http.Features.Authentication;
5+
6+
namespace Microsoft.AspNetCore.Authentication
7+
{
8+
/// <summary>
9+
/// Used to capture the <see cref="AuthenticateResult"/> from the authorization middleware.
10+
/// </summary>
11+
public interface IAuthenticateResultFeature
12+
{
13+
/// <summary>
14+
/// The <see cref="AuthenticateResult"/> from the authorization middleware.
15+
/// Set to null if the <see cref="IHttpAuthenticationFeature.User"/> property is set after the authorization middleware.
16+
/// </summary>
17+
AuthenticateResult? AuthenticateResult { get; set; }
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Authentication.IAuthenticateResultFeature
3+
Microsoft.AspNetCore.Authentication.IAuthenticateResultFeature.AuthenticateResult.get -> Microsoft.AspNetCore.Authentication.AuthenticateResult?
4+
Microsoft.AspNetCore.Authentication.IAuthenticateResultFeature.AuthenticateResult.set -> void
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.Security.Claims;
5+
using Microsoft.AspNetCore.Http.Features.Authentication;
6+
7+
namespace Microsoft.AspNetCore.Authentication
8+
{
9+
/// <summary>
10+
/// Keeps the User and AuthenticationResult consistent with each other
11+
/// </summary>
12+
internal sealed class AuthenticationFeatures : IAuthenticateResultFeature, IHttpAuthenticationFeature
13+
{
14+
private ClaimsPrincipal? _user;
15+
private AuthenticateResult? _result;
16+
17+
public AuthenticationFeatures(AuthenticateResult result)
18+
{
19+
AuthenticateResult = result;
20+
}
21+
22+
public AuthenticateResult? AuthenticateResult
23+
{
24+
get => _result;
25+
set
26+
{
27+
_result = value;
28+
_user = _result?.Principal;
29+
}
30+
}
31+
32+
public ClaimsPrincipal? User
33+
{
34+
get => _user;
35+
set
36+
{
37+
_user = value;
38+
_result = null;
39+
}
40+
}
41+
}
42+
}

src/Security/Authentication/Core/src/AuthenticationMiddleware.cs

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Threading.Tasks;
66
using Microsoft.AspNetCore.Http;
7+
using Microsoft.AspNetCore.Http.Features.Authentication;
78
using Microsoft.Extensions.DependencyInjection;
89

910
namespace Microsoft.AspNetCore.Authentication
@@ -71,6 +72,12 @@ public async Task Invoke(HttpContext context)
7172
{
7273
context.User = result.Principal;
7374
}
75+
if (result?.Succeeded ?? false)
76+
{
77+
var authFeatures = new AuthenticationFeatures(result);
78+
context.Features.Set<IHttpAuthenticationFeature>(authFeatures);
79+
context.Features.Set<IAuthenticateResultFeature>(authFeatures);
80+
}
7481
}
7582

7683
await _next(context);

src/Security/Authentication/test/AuthenticationMiddlewareTests.cs

+124-1
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
using System;
44
using System.Security.Claims;
55
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.Authentication.JwtBearer;
67
using Microsoft.AspNetCore.Builder;
78
using Microsoft.AspNetCore.Hosting;
89
using Microsoft.AspNetCore.Http;
910
using Microsoft.AspNetCore.TestHost;
1011
using Microsoft.Extensions.DependencyInjection;
1112
using Microsoft.Extensions.Hosting;
13+
using Microsoft.Extensions.Logging;
14+
using Moq;
1215
using Xunit;
1316

1417
namespace Microsoft.AspNetCore.Authentication
@@ -54,6 +57,126 @@ public async Task OnlyInvokesCanHandleRequestHandlers()
5457
Assert.Equal(607, (int)response.StatusCode);
5558
}
5659

60+
[Fact]
61+
public async Task IAuthenticateResultFeature_SetOnSuccessfulAuthenticate()
62+
{
63+
var authenticationService = new Mock<IAuthenticationService>();
64+
authenticationService.Setup(s => s.AuthenticateAsync(It.IsAny<HttpContext>(), It.IsAny<string>()))
65+
.Returns(Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), "custom"))));
66+
var schemeProvider = new Mock<IAuthenticationSchemeProvider>();
67+
schemeProvider.Setup(p => p.GetDefaultAuthenticateSchemeAsync())
68+
.Returns(Task.FromResult(new AuthenticationScheme("custom", "custom", typeof(JwtBearerHandler))));
69+
var middleware = new AuthenticationMiddleware(c => Task.CompletedTask, schemeProvider.Object);
70+
var context = GetHttpContext(authenticationService: authenticationService.Object);
71+
72+
// Act
73+
await middleware.Invoke(context);
74+
75+
// Assert
76+
var authenticateResultFeature = context.Features.Get<IAuthenticateResultFeature>();
77+
Assert.NotNull(authenticateResultFeature);
78+
Assert.NotNull(authenticateResultFeature.AuthenticateResult);
79+
Assert.True(authenticateResultFeature.AuthenticateResult.Succeeded);
80+
Assert.Same(context.User, authenticateResultFeature.AuthenticateResult.Principal);
81+
}
82+
83+
[Fact]
84+
public async Task IAuthenticateResultFeature_NotSetOnUnsuccessfulAuthenticate()
85+
{
86+
var authenticationService = new Mock<IAuthenticationService>();
87+
authenticationService.Setup(s => s.AuthenticateAsync(It.IsAny<HttpContext>(), It.IsAny<string>()))
88+
.Returns(Task.FromResult(AuthenticateResult.Fail("not authenticated")));
89+
var schemeProvider = new Mock<IAuthenticationSchemeProvider>();
90+
schemeProvider.Setup(p => p.GetDefaultAuthenticateSchemeAsync())
91+
.Returns(Task.FromResult(new AuthenticationScheme("custom", "custom", typeof(JwtBearerHandler))));
92+
var middleware = new AuthenticationMiddleware(c => Task.CompletedTask, schemeProvider.Object);
93+
var context = GetHttpContext(authenticationService: authenticationService.Object);
94+
95+
// Act
96+
await middleware.Invoke(context);
97+
98+
// Assert
99+
var authenticateResultFeature = context.Features.Get<IAuthenticateResultFeature>();
100+
Assert.Null(authenticateResultFeature);
101+
}
102+
103+
[Fact]
104+
public async Task IAuthenticateResultFeature_NullResultWhenUserSetAfter()
105+
{
106+
var authenticationService = new Mock<IAuthenticationService>();
107+
authenticationService.Setup(s => s.AuthenticateAsync(It.IsAny<HttpContext>(), It.IsAny<string>()))
108+
.Returns(Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), "custom"))));
109+
var schemeProvider = new Mock<IAuthenticationSchemeProvider>();
110+
schemeProvider.Setup(p => p.GetDefaultAuthenticateSchemeAsync())
111+
.Returns(Task.FromResult(new AuthenticationScheme("custom", "custom", typeof(JwtBearerHandler))));
112+
var middleware = new AuthenticationMiddleware(c => Task.CompletedTask, schemeProvider.Object);
113+
var context = GetHttpContext(authenticationService: authenticationService.Object);
114+
115+
// Act
116+
await middleware.Invoke(context);
117+
118+
// Assert
119+
var authenticateResultFeature = context.Features.Get<IAuthenticateResultFeature>();
120+
Assert.NotNull(authenticateResultFeature);
121+
Assert.NotNull(authenticateResultFeature.AuthenticateResult);
122+
Assert.True(authenticateResultFeature.AuthenticateResult.Succeeded);
123+
Assert.Same(context.User, authenticateResultFeature.AuthenticateResult.Principal);
124+
125+
context.User = new ClaimsPrincipal();
126+
Assert.Null(authenticateResultFeature.AuthenticateResult);
127+
}
128+
129+
[Fact]
130+
public async Task IAuthenticateResultFeature_SettingResultSetsUser()
131+
{
132+
var authenticationService = new Mock<IAuthenticationService>();
133+
authenticationService.Setup(s => s.AuthenticateAsync(It.IsAny<HttpContext>(), It.IsAny<string>()))
134+
.Returns(Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), "custom"))));
135+
var schemeProvider = new Mock<IAuthenticationSchemeProvider>();
136+
schemeProvider.Setup(p => p.GetDefaultAuthenticateSchemeAsync())
137+
.Returns(Task.FromResult(new AuthenticationScheme("custom", "custom", typeof(JwtBearerHandler))));
138+
var middleware = new AuthenticationMiddleware(c => Task.CompletedTask, schemeProvider.Object);
139+
var context = GetHttpContext(authenticationService: authenticationService.Object);
140+
141+
// Act
142+
await middleware.Invoke(context);
143+
144+
// Assert
145+
var authenticateResultFeature = context.Features.Get<IAuthenticateResultFeature>();
146+
Assert.NotNull(authenticateResultFeature);
147+
Assert.NotNull(authenticateResultFeature.AuthenticateResult);
148+
Assert.True(authenticateResultFeature.AuthenticateResult.Succeeded);
149+
Assert.Same(context.User, authenticateResultFeature.AuthenticateResult.Principal);
150+
151+
var newTicket = new AuthenticationTicket(new ClaimsPrincipal(), "");
152+
authenticateResultFeature.AuthenticateResult = AuthenticateResult.Success(newTicket);
153+
Assert.Same(context.User, newTicket.Principal);
154+
}
155+
156+
private HttpContext GetHttpContext(
157+
Action<IServiceCollection> registerServices = null,
158+
IAuthenticationService authenticationService = null)
159+
{
160+
// ServiceProvider
161+
var serviceCollection = new ServiceCollection();
162+
163+
authenticationService = authenticationService ?? Mock.Of<IAuthenticationService>();
164+
165+
serviceCollection.AddSingleton(authenticationService);
166+
serviceCollection.AddOptions();
167+
serviceCollection.AddLogging();
168+
serviceCollection.AddAuthentication();
169+
registerServices?.Invoke(serviceCollection);
170+
171+
var serviceProvider = serviceCollection.BuildServiceProvider();
172+
173+
//// HttpContext
174+
var httpContext = new DefaultHttpContext();
175+
httpContext.RequestServices = serviceProvider;
176+
177+
return httpContext;
178+
}
179+
57180
private class ThreeOhFiveHandler : StatusCodeHandler {
58181
public ThreeOhFiveHandler() : base(305) { }
59182
}
@@ -77,7 +200,7 @@ public StatusCodeHandler(int code)
77200
{
78201
_code = code;
79202
}
80-
203+
81204
public Task<AuthenticateResult> AuthenticateAsync()
82205
{
83206
throw new NotImplementedException();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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.Security.Claims;
5+
using Microsoft.AspNetCore.Authentication;
6+
using Microsoft.AspNetCore.Http.Features.Authentication;
7+
8+
namespace Microsoft.AspNetCore.Authorization.Policy
9+
{
10+
/// <summary>
11+
/// Keeps the User and AuthenticationResult consistent with each other
12+
/// </summary>
13+
internal sealed class AuthenticationFeatures : IAuthenticateResultFeature, IHttpAuthenticationFeature
14+
{
15+
private ClaimsPrincipal? _user;
16+
private AuthenticateResult? _result;
17+
18+
public AuthenticationFeatures(AuthenticateResult result)
19+
{
20+
AuthenticateResult = result;
21+
}
22+
23+
public AuthenticateResult? AuthenticateResult
24+
{
25+
get => _result;
26+
set
27+
{
28+
_result = value;
29+
_user = _result?.Principal;
30+
}
31+
}
32+
33+
public ClaimsPrincipal? User
34+
{
35+
get => _user;
36+
set
37+
{
38+
_user = value;
39+
_result = null;
40+
}
41+
}
42+
}
43+
}

src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs

+21-5
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
using System;
55
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.Authentication;
67
using Microsoft.AspNetCore.Authorization.Policy;
78
using Microsoft.AspNetCore.Http;
9+
using Microsoft.AspNetCore.Http.Features.Authentication;
810
using Microsoft.Extensions.DependencyInjection;
911

1012
namespace Microsoft.AspNetCore.Authorization
@@ -29,7 +31,7 @@ public class AuthorizationMiddleware
2931
/// </summary>
3032
/// <param name="next">The next middleware in the application middleware pipeline.</param>
3133
/// <param name="policyProvider">The <see cref="IAuthorizationPolicyProvider"/>.</param>
32-
public AuthorizationMiddleware(RequestDelegate next, IAuthorizationPolicyProvider policyProvider)
34+
public AuthorizationMiddleware(RequestDelegate next, IAuthorizationPolicyProvider policyProvider)
3335
{
3436
_next = next ?? throw new ArgumentNullException(nameof(next));
3537
_policyProvider = policyProvider ?? throw new ArgumentNullException(nameof(policyProvider));
@@ -64,12 +66,26 @@ public async Task Invoke(HttpContext context)
6466
return;
6567
}
6668

67-
// Policy evaluator has transient lifetime so it fetched from request services instead of injecting in constructor
69+
// Policy evaluator has transient lifetime so it's fetched from request services instead of injecting in constructor
6870
var policyEvaluator = context.RequestServices.GetRequiredService<IPolicyEvaluator>();
6971

7072
var authenticateResult = await policyEvaluator.AuthenticateAsync(policy, context);
7173

72-
// Allow Anonymous skips all authorization
74+
if (authenticateResult?.Succeeded ?? false)
75+
{
76+
if (context.Features.Get<IAuthenticateResultFeature>() is IAuthenticateResultFeature authenticateResultFeature)
77+
{
78+
authenticateResultFeature.AuthenticateResult = authenticateResult;
79+
}
80+
else
81+
{
82+
var authFeatures = new AuthenticationFeatures(authenticateResult);
83+
context.Features.Set<IHttpAuthenticationFeature>(authFeatures);
84+
context.Features.Set<IAuthenticateResultFeature>(authFeatures);
85+
}
86+
}
87+
88+
// Allow Anonymous still wants to run authorization to populate the User but skips any failure/challenge handling
7389
if (endpoint?.Metadata.GetMetadata<IAllowAnonymous>() != null)
7490
{
7591
await _next(context);
@@ -85,8 +101,8 @@ public async Task Invoke(HttpContext context)
85101
{
86102
resource = context;
87103
}
88-
89-
var authorizeResult = await policyEvaluator.AuthorizeAsync(policy, authenticateResult, context, resource);
104+
105+
var authorizeResult = await policyEvaluator.AuthorizeAsync(policy, authenticateResult!, context, resource);
90106
var authorizationMiddlewareResultHandler = context.RequestServices.GetRequiredService<IAuthorizationMiddlewareResultHandler>();
91107
await authorizationMiddlewareResultHandler.HandleAsync(_next, context, policy, authorizeResult);
92108
}

src/Security/Authorization/Policy/src/PolicyEvaluator.cs

+11-1
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,29 @@ public virtual async Task<AuthenticateResult> AuthenticateAsync(AuthorizationPol
3838
if (policy.AuthenticationSchemes != null && policy.AuthenticationSchemes.Count > 0)
3939
{
4040
ClaimsPrincipal? newPrincipal = null;
41+
DateTimeOffset? minExpiresUtc = null;
4142
foreach (var scheme in policy.AuthenticationSchemes)
4243
{
4344
var result = await context.AuthenticateAsync(scheme);
4445
if (result != null && result.Succeeded)
4546
{
4647
newPrincipal = SecurityHelper.MergeUserPrincipal(newPrincipal, result.Principal);
48+
49+
if (minExpiresUtc is null || result.Properties?.ExpiresUtc < minExpiresUtc)
50+
{
51+
minExpiresUtc = result.Properties?.ExpiresUtc;
52+
}
4753
}
4854
}
4955

5056
if (newPrincipal != null)
5157
{
5258
context.User = newPrincipal;
53-
return AuthenticateResult.Success(new AuthenticationTicket(newPrincipal, string.Join(";", policy.AuthenticationSchemes)));
59+
var ticket = new AuthenticationTicket(newPrincipal, string.Join(";", policy.AuthenticationSchemes));
60+
// ExpiresUtc is the easiest property to reason about when dealing with multiple schemes
61+
// SignalR will use this property to evaluate auth expiration for long running connections
62+
ticket.Properties.ExpiresUtc = minExpiresUtc;
63+
return AuthenticateResult.Success(ticket);
5464
}
5565
else
5666
{

0 commit comments

Comments
 (0)