diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticateResultFeature.cs b/src/Http/Authentication.Abstractions/src/IAuthenticateResultFeature.cs
new file mode 100644
index 000000000000..1d9ad0034446
--- /dev/null
+++ b/src/Http/Authentication.Abstractions/src/IAuthenticateResultFeature.cs
@@ -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
+{
+ ///
+ /// Used to capture the from the authorization middleware.
+ ///
+ public interface IAuthenticateResultFeature
+ {
+ ///
+ /// The from the authorization middleware.
+ /// Set to null if the property is set after the authorization middleware.
+ ///
+ AuthenticateResult? AuthenticateResult { get; set; }
+ }
+}
diff --git a/src/Http/Authentication.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Authentication.Abstractions/src/PublicAPI.Unshipped.txt
index 7dc5c58110bf..1e3de58fe613 100644
--- a/src/Http/Authentication.Abstractions/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Authentication.Abstractions/src/PublicAPI.Unshipped.txt
@@ -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
diff --git a/src/Security/Authentication/Core/src/AuthenticationFeatures.cs b/src/Security/Authentication/Core/src/AuthenticationFeatures.cs
new file mode 100644
index 000000000000..bbe75a8261b8
--- /dev/null
+++ b/src/Security/Authentication/Core/src/AuthenticationFeatures.cs
@@ -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
+{
+ ///
+ /// Keeps the User and AuthenticationResult consistent with each other
+ ///
+ 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;
+ }
+ }
+ }
+}
diff --git a/src/Security/Authentication/Core/src/AuthenticationMiddleware.cs b/src/Security/Authentication/Core/src/AuthenticationMiddleware.cs
index 8a1fa97b0a82..b2b810cf4ec2 100644
--- a/src/Security/Authentication/Core/src/AuthenticationMiddleware.cs
+++ b/src/Security/Authentication/Core/src/AuthenticationMiddleware.cs
@@ -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
@@ -71,6 +72,12 @@ public async Task Invoke(HttpContext context)
{
context.User = result.Principal;
}
+ if (result?.Succeeded ?? false)
+ {
+ var authFeatures = new AuthenticationFeatures(result);
+ context.Features.Set(authFeatures);
+ context.Features.Set(authFeatures);
+ }
}
await _next(context);
diff --git a/src/Security/Authentication/test/AuthenticationMiddlewareTests.cs b/src/Security/Authentication/test/AuthenticationMiddlewareTests.cs
index 232b9fed0e88..cf48902da024 100644
--- a/src/Security/Authentication/test/AuthenticationMiddlewareTests.cs
+++ b/src/Security/Authentication/test/AuthenticationMiddlewareTests.cs
@@ -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
@@ -54,6 +57,126 @@ public async Task OnlyInvokesCanHandleRequestHandlers()
Assert.Equal(607, (int)response.StatusCode);
}
+ [Fact]
+ public async Task IAuthenticateResultFeature_SetOnSuccessfulAuthenticate()
+ {
+ var authenticationService = new Mock();
+ authenticationService.Setup(s => s.AuthenticateAsync(It.IsAny(), It.IsAny()))
+ .Returns(Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), "custom"))));
+ var schemeProvider = new Mock();
+ 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();
+ 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();
+ authenticationService.Setup(s => s.AuthenticateAsync(It.IsAny(), It.IsAny()))
+ .Returns(Task.FromResult(AuthenticateResult.Fail("not authenticated")));
+ var schemeProvider = new Mock();
+ 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();
+ Assert.Null(authenticateResultFeature);
+ }
+
+ [Fact]
+ public async Task IAuthenticateResultFeature_NullResultWhenUserSetAfter()
+ {
+ var authenticationService = new Mock();
+ authenticationService.Setup(s => s.AuthenticateAsync(It.IsAny(), It.IsAny()))
+ .Returns(Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), "custom"))));
+ var schemeProvider = new Mock();
+ 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();
+ 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();
+ authenticationService.Setup(s => s.AuthenticateAsync(It.IsAny(), It.IsAny()))
+ .Returns(Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), "custom"))));
+ var schemeProvider = new Mock();
+ 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();
+ 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 registerServices = null,
+ IAuthenticationService authenticationService = null)
+ {
+ // ServiceProvider
+ var serviceCollection = new ServiceCollection();
+
+ authenticationService = authenticationService ?? Mock.Of();
+
+ 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) { }
}
@@ -77,7 +200,7 @@ public StatusCodeHandler(int code)
{
_code = code;
}
-
+
public Task AuthenticateAsync()
{
throw new NotImplementedException();
diff --git a/src/Security/Authorization/Policy/src/AuthenticationFeatures.cs b/src/Security/Authorization/Policy/src/AuthenticationFeatures.cs
new file mode 100644
index 000000000000..c8660f679930
--- /dev/null
+++ b/src/Security/Authorization/Policy/src/AuthenticationFeatures.cs
@@ -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
+{
+ ///
+ /// Keeps the User and AuthenticationResult consistent with each other
+ ///
+ 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;
+ }
+ }
+ }
+}
diff --git a/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs b/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs
index 0fddd96878f7..a7bfa785adc0 100644
--- a/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs
+++ b/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs
@@ -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
@@ -29,7 +31,7 @@ public class AuthorizationMiddleware
///
/// The next middleware in the application middleware pipeline.
/// The .
- 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));
@@ -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();
var authenticateResult = await policyEvaluator.AuthenticateAsync(policy, context);
- // Allow Anonymous skips all authorization
+ if (authenticateResult?.Succeeded ?? false)
+ {
+ if (context.Features.Get() is IAuthenticateResultFeature authenticateResultFeature)
+ {
+ authenticateResultFeature.AuthenticateResult = authenticateResult;
+ }
+ else
+ {
+ var authFeatures = new AuthenticationFeatures(authenticateResult);
+ context.Features.Set(authFeatures);
+ context.Features.Set(authFeatures);
+ }
+ }
+
+ // Allow Anonymous still wants to run authorization to populate the User but skips any failure/challenge handling
if (endpoint?.Metadata.GetMetadata() != null)
{
await _next(context);
@@ -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();
await authorizationMiddlewareResultHandler.HandleAsync(_next, context, policy, authorizeResult);
}
diff --git a/src/Security/Authorization/Policy/src/PolicyEvaluator.cs b/src/Security/Authorization/Policy/src/PolicyEvaluator.cs
index 06da4e969cb0..79e39e358e1d 100644
--- a/src/Security/Authorization/Policy/src/PolicyEvaluator.cs
+++ b/src/Security/Authorization/Policy/src/PolicyEvaluator.cs
@@ -38,19 +38,29 @@ public virtual async Task 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
{
diff --git a/src/Security/Authorization/test/AuthorizationMiddlewareTests.cs b/src/Security/Authorization/test/AuthorizationMiddlewareTests.cs
index 89de50e3d4ce..2ba4f6d4e453 100644
--- a/src/Security/Authorization/test/AuthorizationMiddlewareTests.cs
+++ b/src/Security/Authorization/test/AuthorizationMiddlewareTests.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
@@ -9,7 +10,6 @@
using Microsoft.AspNetCore.Authorization.Test.TestObjects;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Options;
using Moq;
using Xunit;
@@ -54,7 +54,7 @@ public async Task NoEndpointWithFallback_AnonymousUser_Challenges()
// Assert
Assert.False(next.Called);
}
-
+
[Fact]
public async Task HasEndpointWithoutAuth_AnonymousUser_Allows()
{
@@ -156,7 +156,7 @@ public async Task HasEndpointWithAuth_ChallengesAuthenticationSchemes()
Assert.False(next.Called);
Assert.True(authenticationService.ChallengeCalled);
}
-
+
[Fact]
public async Task HasEndpointWithAuth_AnonymousUser_ChallengePerScheme()
{
@@ -367,7 +367,7 @@ public async Task AuthZResourceShouldBeEndpointByDefaultWithCompatSwitch()
// Assert
Assert.Equal(endpoint, resource);
}
-
+
[Fact]
public async Task Invoke_RequireUnknownRoleShouldForbid()
{
@@ -435,6 +435,179 @@ public async Task Invoke_InvalidClaimShouldForbid()
Assert.True(authenticationService.ForbidCalled);
}
+ [Fact]
+ public async Task IAuthenticateResultFeature_SetOnSuccessfulAuthorize()
+ {
+ // Arrange
+ var policy = new AuthorizationPolicyBuilder().RequireClaim("Permission", "CanViewPage").Build();
+ var policyProvider = new Mock();
+ policyProvider.Setup(p => p.GetDefaultPolicyAsync()).ReturnsAsync(policy);
+ var next = new TestRequestDelegate();
+
+ var middleware = CreateMiddleware(next.Invoke, policyProvider.Object);
+ var context = GetHttpContext(endpoint: CreateEndpoint(new AuthorizeAttribute()));
+
+ // Act
+ await middleware.Invoke(context);
+
+ // Assert
+ Assert.True(next.Called);
+ var authenticateResultFeature = context.Features.Get();
+ Assert.NotNull(authenticateResultFeature);
+ Assert.NotNull(authenticateResultFeature.AuthenticateResult);
+ Assert.Same(context.User, authenticateResultFeature.AuthenticateResult.Principal);
+ }
+
+ [Fact]
+ public async Task IAuthenticateResultFeature_NotSetOnUnsuccessfulAuthorize()
+ {
+ // Arrange
+ var policy = new AuthorizationPolicyBuilder().RequireRole("Wut").AddAuthenticationSchemes("NotImplemented").Build();
+ var policyProvider = new Mock();
+ policyProvider.Setup(p => p.GetDefaultPolicyAsync()).ReturnsAsync(policy);
+ var next = new TestRequestDelegate();
+ var authenticationService = new TestAuthenticationService();
+
+ var middleware = CreateMiddleware(next.Invoke, policyProvider.Object);
+ var context = GetHttpContext(endpoint: CreateEndpoint(new AuthorizeAttribute(), new AllowAnonymousAttribute()), authenticationService: authenticationService);
+
+ // Act
+ await middleware.Invoke(context);
+
+ // Assert
+ Assert.True(next.Called);
+ var authenticateResultFeature = context.Features.Get();
+ Assert.Null(authenticateResultFeature);
+ Assert.True(authenticationService.AuthenticateCalled);
+ }
+
+ [Fact]
+ public async Task IAuthenticateResultFeature_ContainsLowestExpiration()
+ {
+ // Arrange
+ var policy = new AuthorizationPolicyBuilder().RequireRole("Wut").AddAuthenticationSchemes("Basic", "Bearer").Build();
+ var policyProvider = new Mock();
+ policyProvider.Setup(p => p.GetDefaultPolicyAsync()).ReturnsAsync(policy);
+ var next = new TestRequestDelegate();
+
+ var firstExpiration = new DateTimeOffset(2021, 5, 12, 2, 3, 4, TimeSpan.Zero);
+ var secondExpiration = new DateTimeOffset(2021, 5, 11, 2, 3, 4, TimeSpan.Zero);
+ var authenticationService = new Mock();
+ authenticationService.Setup(s => s.AuthenticateAsync(It.IsAny(), "Basic"))
+ .ReturnsAsync((HttpContext c, string scheme) =>
+ {
+ var res = AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(c.User.Identities.FirstOrDefault(i => i.AuthenticationType == scheme)), scheme));
+ res.Properties.ExpiresUtc = firstExpiration;
+ return res;
+ });
+ authenticationService.Setup(s => s.AuthenticateAsync(It.IsAny(), "Bearer"))
+ .ReturnsAsync((HttpContext c, string scheme) =>
+ {
+ var res = AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(c.User.Identities.FirstOrDefault(i => i.AuthenticationType == scheme)), scheme));
+ res.Properties.ExpiresUtc = secondExpiration;
+ return res;
+ });
+
+ var middleware = CreateMiddleware(next.Invoke, policyProvider.Object);
+ var context = GetHttpContext(endpoint: CreateEndpoint(new AuthorizeAttribute()), authenticationService: authenticationService.Object);
+
+ // Act
+ await middleware.Invoke(context);
+
+ // Assert
+ var authenticateResultFeature = context.Features.Get();
+ Assert.NotNull(authenticateResultFeature);
+ Assert.NotNull(authenticateResultFeature.AuthenticateResult);
+ Assert.Same(context.User, authenticateResultFeature.AuthenticateResult.Principal);
+ Assert.Equal(secondExpiration, authenticateResultFeature.AuthenticateResult?.Properties?.ExpiresUtc);
+ }
+
+ [Fact]
+ public async Task IAuthenticateResultFeature_NullResultWhenUserSetAfter()
+ {
+ // Arrange
+ var policy = new AuthorizationPolicyBuilder().RequireClaim("Permission", "CanViewPage").Build();
+ var policyProvider = new Mock();
+ policyProvider.Setup(p => p.GetDefaultPolicyAsync()).ReturnsAsync(policy);
+ var next = new TestRequestDelegate();
+
+ var middleware = CreateMiddleware(next.Invoke, policyProvider.Object);
+ var context = GetHttpContext(endpoint: CreateEndpoint(new AuthorizeAttribute()));
+
+ // Act
+ await middleware.Invoke(context);
+
+ // Assert
+ Assert.True(next.Called);
+ var authenticateResultFeature = context.Features.Get();
+ Assert.NotNull(authenticateResultFeature);
+ Assert.NotNull(authenticateResultFeature.AuthenticateResult);
+ Assert.Same(context.User, authenticateResultFeature.AuthenticateResult.Principal);
+
+ context.User = new ClaimsPrincipal();
+ Assert.Null(authenticateResultFeature.AuthenticateResult);
+ }
+
+ [Fact]
+ public async Task IAuthenticateResultFeature_SettingResultSetsUser()
+ {
+ // Arrange
+ var policy = new AuthorizationPolicyBuilder().RequireClaim("Permission", "CanViewPage").Build();
+ var policyProvider = new Mock();
+ policyProvider.Setup(p => p.GetDefaultPolicyAsync()).ReturnsAsync(policy);
+ var next = new TestRequestDelegate();
+
+ var middleware = CreateMiddleware(next.Invoke, policyProvider.Object);
+ var context = GetHttpContext(endpoint: CreateEndpoint(new AuthorizeAttribute()));
+
+ // Act
+ await middleware.Invoke(context);
+
+ // Assert
+ Assert.True(next.Called);
+ var authenticateResultFeature = context.Features.Get();
+ Assert.NotNull(authenticateResultFeature);
+ Assert.NotNull(authenticateResultFeature.AuthenticateResult);
+ Assert.Same(context.User, authenticateResultFeature.AuthenticateResult.Principal);
+
+ var newTicket = new AuthenticationTicket(new ClaimsPrincipal(), "");
+ authenticateResultFeature.AuthenticateResult = AuthenticateResult.Success(newTicket);
+ Assert.Same(context.User, newTicket.Principal);
+ }
+
+ class TestAuthResultFeature : IAuthenticateResultFeature
+ {
+ public AuthenticateResult AuthenticateResult { get; set; }
+ }
+
+ [Fact]
+ public async Task IAuthenticateResultFeature_UsesExistingFeature()
+ {
+ // Arrange
+ var policy = new AuthorizationPolicyBuilder().RequireClaim("Permission", "CanViewPage").Build();
+ var policyProvider = new Mock();
+ policyProvider.Setup(p => p.GetDefaultPolicyAsync()).ReturnsAsync(policy);
+ var next = new TestRequestDelegate();
+
+ var middleware = CreateMiddleware(next.Invoke, policyProvider.Object);
+ var context = GetHttpContext(endpoint: CreateEndpoint(new AuthorizeAttribute()));
+ var testAuthenticateResultFeature = new TestAuthResultFeature();
+ var authenticateResult = AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), ""));
+ testAuthenticateResultFeature.AuthenticateResult = authenticateResult;
+ context.Features.Set(testAuthenticateResultFeature);
+
+ // Act
+ await middleware.Invoke(context);
+
+ // Assert
+ Assert.True(next.Called);
+ var authenticateResultFeature = context.Features.Get();
+ Assert.NotNull(authenticateResultFeature);
+ Assert.NotNull(authenticateResultFeature.AuthenticateResult);
+ Assert.Same(testAuthenticateResultFeature, authenticateResultFeature);
+ Assert.NotSame(authenticateResult, authenticateResultFeature.AuthenticateResult);
+ }
+
private AuthorizationMiddleware CreateMiddleware(RequestDelegate requestDelegate = null, IAuthorizationPolicyProvider policyProvider = null)
{
requestDelegate = requestDelegate ?? ((context) => Task.CompletedTask);