Skip to content

Add a feature for accessing the AuthenticateResult #33408

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// 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 Microsoft.AspNetCore.Http.Features.Authentication;

namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Used to capture the <see cref="AuthenticateResult"/> from the authorization middleware.
/// </summary>
public interface IAuthenticateResultFeature
{
/// <summary>
/// The <see cref="AuthenticateResult"/> from the authorization middleware.
/// Set to null if the <see cref="IHttpAuthenticationFeature.User"/> property is set after the authorization middleware.
/// </summary>
AuthenticateResult? AuthenticateResult { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
#nullable enable
Microsoft.AspNetCore.Authentication.IAuthenticateResultFeature
Microsoft.AspNetCore.Authentication.IAuthenticateResultFeature.AuthenticateResult.get -> Microsoft.AspNetCore.Authentication.AuthenticateResult?
Microsoft.AspNetCore.Authentication.IAuthenticateResultFeature.AuthenticateResult.set -> void
42 changes: 42 additions & 0 deletions src/Security/Authentication/Core/src/AuthenticationFeatures.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// 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.Security.Claims;
using Microsoft.AspNetCore.Http.Features.Authentication;

namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Keeps the User and AuthenticationResult consistent with each other
/// </summary>
internal sealed class AuthenticationFeatures : IAuthenticateResultFeature, IHttpAuthenticationFeature
{
private ClaimsPrincipal? _user;
private AuthenticateResult? _result;

public AuthenticationFeatures(AuthenticateResult result)
{
AuthenticateResult = result;
}

public AuthenticateResult? AuthenticateResult
{
get => _result;
set
{
_result = value;
_user = _result?.Principal;
}
}

public ClaimsPrincipal? User
{
get => _user;
set
{
_user = value;
_result = null;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features.Authentication;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Authentication
Expand Down Expand Up @@ -71,6 +72,12 @@ public async Task Invoke(HttpContext context)
{
context.User = result.Principal;
}
if (result?.Succeeded ?? false)
{
var authFeatures = new AuthenticationFeatures(result);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allocations!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't look at the rest of the Auth middleware stuff then 😢

context.Features.Set<IHttpAuthenticationFeature>(authFeatures);
context.Features.Set<IAuthenticateResultFeature>(authFeatures);
}
}

await _next(context);
Expand Down
125 changes: 124 additions & 1 deletion src/Security/Authentication/test/AuthenticationMiddlewareTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Authentication
Expand Down Expand Up @@ -54,6 +57,126 @@ public async Task OnlyInvokesCanHandleRequestHandlers()
Assert.Equal(607, (int)response.StatusCode);
}

[Fact]
public async Task IAuthenticateResultFeature_SetOnSuccessfulAuthenticate()
{
var authenticationService = new Mock<IAuthenticationService>();
authenticationService.Setup(s => s.AuthenticateAsync(It.IsAny<HttpContext>(), It.IsAny<string>()))
.Returns(Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), "custom"))));
var schemeProvider = new Mock<IAuthenticationSchemeProvider>();
schemeProvider.Setup(p => p.GetDefaultAuthenticateSchemeAsync())
.Returns(Task.FromResult(new AuthenticationScheme("custom", "custom", typeof(JwtBearerHandler))));
var middleware = new AuthenticationMiddleware(c => Task.CompletedTask, schemeProvider.Object);
var context = GetHttpContext(authenticationService: authenticationService.Object);

// Act
await middleware.Invoke(context);

// Assert
var authenticateResultFeature = context.Features.Get<IAuthenticateResultFeature>();
Assert.NotNull(authenticateResultFeature);
Assert.NotNull(authenticateResultFeature.AuthenticateResult);
Assert.True(authenticateResultFeature.AuthenticateResult.Succeeded);
Assert.Same(context.User, authenticateResultFeature.AuthenticateResult.Principal);
}

[Fact]
public async Task IAuthenticateResultFeature_NotSetOnUnsuccessfulAuthenticate()
{
var authenticationService = new Mock<IAuthenticationService>();
authenticationService.Setup(s => s.AuthenticateAsync(It.IsAny<HttpContext>(), It.IsAny<string>()))
.Returns(Task.FromResult(AuthenticateResult.Fail("not authenticated")));
var schemeProvider = new Mock<IAuthenticationSchemeProvider>();
schemeProvider.Setup(p => p.GetDefaultAuthenticateSchemeAsync())
.Returns(Task.FromResult(new AuthenticationScheme("custom", "custom", typeof(JwtBearerHandler))));
var middleware = new AuthenticationMiddleware(c => Task.CompletedTask, schemeProvider.Object);
var context = GetHttpContext(authenticationService: authenticationService.Object);

// Act
await middleware.Invoke(context);

// Assert
var authenticateResultFeature = context.Features.Get<IAuthenticateResultFeature>();
Assert.Null(authenticateResultFeature);
}

[Fact]
public async Task IAuthenticateResultFeature_NullResultWhenUserSetAfter()
{
var authenticationService = new Mock<IAuthenticationService>();
authenticationService.Setup(s => s.AuthenticateAsync(It.IsAny<HttpContext>(), It.IsAny<string>()))
.Returns(Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), "custom"))));
var schemeProvider = new Mock<IAuthenticationSchemeProvider>();
schemeProvider.Setup(p => p.GetDefaultAuthenticateSchemeAsync())
.Returns(Task.FromResult(new AuthenticationScheme("custom", "custom", typeof(JwtBearerHandler))));
var middleware = new AuthenticationMiddleware(c => Task.CompletedTask, schemeProvider.Object);
var context = GetHttpContext(authenticationService: authenticationService.Object);

// Act
await middleware.Invoke(context);

// Assert
var authenticateResultFeature = context.Features.Get<IAuthenticateResultFeature>();
Assert.NotNull(authenticateResultFeature);
Assert.NotNull(authenticateResultFeature.AuthenticateResult);
Assert.True(authenticateResultFeature.AuthenticateResult.Succeeded);
Assert.Same(context.User, authenticateResultFeature.AuthenticateResult.Principal);

context.User = new ClaimsPrincipal();
Assert.Null(authenticateResultFeature.AuthenticateResult);
}

[Fact]
public async Task IAuthenticateResultFeature_SettingResultSetsUser()
{
var authenticationService = new Mock<IAuthenticationService>();
authenticationService.Setup(s => s.AuthenticateAsync(It.IsAny<HttpContext>(), It.IsAny<string>()))
.Returns(Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), "custom"))));
var schemeProvider = new Mock<IAuthenticationSchemeProvider>();
schemeProvider.Setup(p => p.GetDefaultAuthenticateSchemeAsync())
.Returns(Task.FromResult(new AuthenticationScheme("custom", "custom", typeof(JwtBearerHandler))));
var middleware = new AuthenticationMiddleware(c => Task.CompletedTask, schemeProvider.Object);
var context = GetHttpContext(authenticationService: authenticationService.Object);

// Act
await middleware.Invoke(context);

// Assert
var authenticateResultFeature = context.Features.Get<IAuthenticateResultFeature>();
Assert.NotNull(authenticateResultFeature);
Assert.NotNull(authenticateResultFeature.AuthenticateResult);
Assert.True(authenticateResultFeature.AuthenticateResult.Succeeded);
Assert.Same(context.User, authenticateResultFeature.AuthenticateResult.Principal);

var newTicket = new AuthenticationTicket(new ClaimsPrincipal(), "");
authenticateResultFeature.AuthenticateResult = AuthenticateResult.Success(newTicket);
Assert.Same(context.User, newTicket.Principal);
}

private HttpContext GetHttpContext(
Action<IServiceCollection> registerServices = null,
IAuthenticationService authenticationService = null)
{
// ServiceProvider
var serviceCollection = new ServiceCollection();

authenticationService = authenticationService ?? Mock.Of<IAuthenticationService>();

serviceCollection.AddSingleton(authenticationService);
serviceCollection.AddOptions();
serviceCollection.AddLogging();
serviceCollection.AddAuthentication();
registerServices?.Invoke(serviceCollection);

var serviceProvider = serviceCollection.BuildServiceProvider();

//// HttpContext
var httpContext = new DefaultHttpContext();
httpContext.RequestServices = serviceProvider;

return httpContext;
}

private class ThreeOhFiveHandler : StatusCodeHandler {
public ThreeOhFiveHandler() : base(305) { }
}
Expand All @@ -77,7 +200,7 @@ public StatusCodeHandler(int code)
{
_code = code;
}

public Task<AuthenticateResult> AuthenticateAsync()
{
throw new NotImplementedException();
Expand Down
43 changes: 43 additions & 0 deletions src/Security/Authorization/Policy/src/AuthenticationFeatures.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http.Features.Authentication;

namespace Microsoft.AspNetCore.Authorization.Policy
{
/// <summary>
/// Keeps the User and AuthenticationResult consistent with each other
/// </summary>
internal sealed class AuthenticationFeatures : IAuthenticateResultFeature, IHttpAuthenticationFeature
{
private ClaimsPrincipal? _user;
private AuthenticateResult? _result;

public AuthenticationFeatures(AuthenticateResult result)
{
AuthenticateResult = result;
}

public AuthenticateResult? AuthenticateResult
{
get => _result;
set
{
_result = value;
_user = _result?.Principal;
}
}

public ClaimsPrincipal? User
{
get => _user;
set
{
_user = value;
_result = null;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization.Policy;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features.Authentication;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Authorization
Expand All @@ -29,7 +31,7 @@ public class AuthorizationMiddleware
/// </summary>
/// <param name="next">The next middleware in the application middleware pipeline.</param>
/// <param name="policyProvider">The <see cref="IAuthorizationPolicyProvider"/>.</param>
public AuthorizationMiddleware(RequestDelegate next, IAuthorizationPolicyProvider policyProvider)
public AuthorizationMiddleware(RequestDelegate next, IAuthorizationPolicyProvider policyProvider)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
_policyProvider = policyProvider ?? throw new ArgumentNullException(nameof(policyProvider));
Expand Down Expand Up @@ -64,12 +66,26 @@ public async Task Invoke(HttpContext context)
return;
}

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

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

// Allow Anonymous skips all authorization
if (authenticateResult?.Succeeded ?? false)
{
if (context.Features.Get<IAuthenticateResultFeature>() is IAuthenticateResultFeature authenticateResultFeature)
{
authenticateResultFeature.AuthenticateResult = authenticateResult;
}
else
{
var authFeatures = new AuthenticationFeatures(authenticateResult);
context.Features.Set<IHttpAuthenticationFeature>(authFeatures);
context.Features.Set<IAuthenticateResultFeature>(authFeatures);
}
}

// Allow Anonymous still wants to run authorization to populate the User but skips any failure/challenge handling
if (endpoint?.Metadata.GetMetadata<IAllowAnonymous>() != null)
{
await _next(context);
Expand All @@ -85,8 +101,8 @@ public async Task Invoke(HttpContext context)
{
resource = context;
}
var authorizeResult = await policyEvaluator.AuthorizeAsync(policy, authenticateResult, context, resource);

var authorizeResult = await policyEvaluator.AuthorizeAsync(policy, authenticateResult!, context, resource);
var authorizationMiddlewareResultHandler = context.RequestServices.GetRequiredService<IAuthorizationMiddlewareResultHandler>();
await authorizationMiddlewareResultHandler.HandleAsync(_next, context, policy, authorizeResult);
}
Expand Down
12 changes: 11 additions & 1 deletion src/Security/Authorization/Policy/src/PolicyEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,29 @@ public virtual async Task<AuthenticateResult> AuthenticateAsync(AuthorizationPol
if (policy.AuthenticationSchemes != null && policy.AuthenticationSchemes.Count > 0)
{
ClaimsPrincipal? newPrincipal = null;
DateTimeOffset? minExpiresUtc = null;
foreach (var scheme in policy.AuthenticationSchemes)
{
var result = await context.AuthenticateAsync(scheme);
if (result != null && result.Succeeded)
{
newPrincipal = SecurityHelper.MergeUserPrincipal(newPrincipal, result.Principal);

if (minExpiresUtc is null || result.Properties?.ExpiresUtc < minExpiresUtc)
{
minExpiresUtc = result.Properties?.ExpiresUtc;
}
}
}

if (newPrincipal != null)
{
context.User = newPrincipal;
return AuthenticateResult.Success(new AuthenticationTicket(newPrincipal, string.Join(";", policy.AuthenticationSchemes)));
var ticket = new AuthenticationTicket(newPrincipal, string.Join(";", policy.AuthenticationSchemes));
// ExpiresUtc is the easiest property to reason about when dealing with multiple schemes
// SignalR will use this property to evaluate auth expiration for long running connections
ticket.Properties.ExpiresUtc = minExpiresUtc;
return AuthenticateResult.Success(ticket);
}
else
{
Expand Down
Loading