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);