From 6c7a8bb397de1fcfeeb6510ea39ef6eb02c15352 Mon Sep 17 00:00:00 2001 From: Dawid Szmidka Date: Tue, 19 May 2020 10:56:21 +0300 Subject: [PATCH] Allow custom handling of authorization failures (with sample app) (#21117) --- src/Security/AuthSamples.sln | 15 ++ ...NetCore.Authorization.Policy.netcoreapp.cs | 12 ++ .../Policy/src/AuthorizationMiddleware.cs | 38 +---- .../AuthorizationMiddlewareResultHandler.cs | 47 ++++++ .../IAuthorizationMiddlewareResultHandler.cs | 11 ++ .../Policy/src/IPolicyEvaluator.cs | 4 +- .../Policy/src/PolicyAuthorizationResult.cs | 12 +- .../Policy/src/PolicyEvaluator.cs | 10 +- .../src/PolicyServiceCollectionExtensions.cs | 3 +- ...thorizationMiddlewareResultHandlerTests.cs | 150 ++++++++++++++++++ .../test/AuthorizationMiddlewareTests.cs | 3 +- .../test/PolicyEvaluatorTests.cs | 24 +++ .../SampleAuthenticationHandler.cs | 22 +++ .../SampleAuthenticationSchemes.cs | 7 + .../Handlers/SampleRequirementHandler.cs | 15 ++ ...mpleWithCustomMessageRequirementHandler.cs | 15 ++ .../Requirements/SampleRequirement.cs | 8 + .../SampleWithCustomMessageRequirement.cs | 8 + ...pleAuthorizationMiddlewareResultHandler.cs | 54 +++++++ .../Authorization/SamplePolicyNames.cs | 8 + .../Controllers/SampleController.cs | 25 +++ .../CustomAuthorizationFailureResponse.csproj | 15 ++ .../Extensions/ServiceCollectionExtensions.cs | 21 +++ .../Program.cs | 20 +++ .../Startup.cs | 54 +++++++ .../appsettings.Development.json | 9 ++ .../appsettings.json | 7 + .../AuthSamples.FunctionalTests.csproj | 42 ++--- ...CustomAuthorizationFailureResponseTests.cs | 41 +++++ 29 files changed, 624 insertions(+), 76 deletions(-) create mode 100644 src/Security/Authorization/Policy/src/AuthorizationMiddlewareResultHandler.cs create mode 100644 src/Security/Authorization/Policy/src/IAuthorizationMiddlewareResultHandler.cs create mode 100644 src/Security/Authorization/test/AuthorizationMiddlewareResultHandlerTests.cs create mode 100644 src/Security/samples/CustomAuthorizationFailureResponse/Authentication/SampleAuthenticationHandler.cs create mode 100644 src/Security/samples/CustomAuthorizationFailureResponse/Authentication/SampleAuthenticationSchemes.cs create mode 100644 src/Security/samples/CustomAuthorizationFailureResponse/Authorization/Handlers/SampleRequirementHandler.cs create mode 100644 src/Security/samples/CustomAuthorizationFailureResponse/Authorization/Handlers/SampleWithCustomMessageRequirementHandler.cs create mode 100644 src/Security/samples/CustomAuthorizationFailureResponse/Authorization/Requirements/SampleRequirement.cs create mode 100644 src/Security/samples/CustomAuthorizationFailureResponse/Authorization/Requirements/SampleWithCustomMessageRequirement.cs create mode 100644 src/Security/samples/CustomAuthorizationFailureResponse/Authorization/SampleAuthorizationMiddlewareResultHandler.cs create mode 100644 src/Security/samples/CustomAuthorizationFailureResponse/Authorization/SamplePolicyNames.cs create mode 100644 src/Security/samples/CustomAuthorizationFailureResponse/Controllers/SampleController.cs create mode 100644 src/Security/samples/CustomAuthorizationFailureResponse/CustomAuthorizationFailureResponse.csproj create mode 100644 src/Security/samples/CustomAuthorizationFailureResponse/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/Security/samples/CustomAuthorizationFailureResponse/Program.cs create mode 100644 src/Security/samples/CustomAuthorizationFailureResponse/Startup.cs create mode 100644 src/Security/samples/CustomAuthorizationFailureResponse/appsettings.Development.json create mode 100644 src/Security/samples/CustomAuthorizationFailureResponse/appsettings.json create mode 100644 src/Security/test/AuthSamples.FunctionalTests/CustomAuthorizationFailureResponseTests.cs diff --git a/src/Security/AuthSamples.sln b/src/Security/AuthSamples.sln index 6c83c5096d..b6af0f75a4 100644 --- a/src/Security/AuthSamples.sln +++ b/src/Security/AuthSamples.sln @@ -68,6 +68,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomPolicyProvider", "sam EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StaticFilesAuth", "samples\StaticFilesAuth\StaticFilesAuth.csproj", "{E1E8A599-AB42-4551-8C24-BE4404B65283}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CustomAuthorizationFailureResponse", "samples\CustomAuthorizationFailureResponse\CustomAuthorizationFailureResponse.csproj", "{EA51BBBC-58AC-42F8-97C1-5CF3C9725513}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -402,6 +404,18 @@ Global {E1E8A599-AB42-4551-8C24-BE4404B65283}.Release|x64.Build.0 = Release|Any CPU {E1E8A599-AB42-4551-8C24-BE4404B65283}.Release|x86.ActiveCfg = Release|Any CPU {E1E8A599-AB42-4551-8C24-BE4404B65283}.Release|x86.Build.0 = Release|Any CPU + {EA51BBBC-58AC-42F8-97C1-5CF3C9725513}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA51BBBC-58AC-42F8-97C1-5CF3C9725513}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA51BBBC-58AC-42F8-97C1-5CF3C9725513}.Debug|x64.ActiveCfg = Debug|Any CPU + {EA51BBBC-58AC-42F8-97C1-5CF3C9725513}.Debug|x64.Build.0 = Debug|Any CPU + {EA51BBBC-58AC-42F8-97C1-5CF3C9725513}.Debug|x86.ActiveCfg = Debug|Any CPU + {EA51BBBC-58AC-42F8-97C1-5CF3C9725513}.Debug|x86.Build.0 = Debug|Any CPU + {EA51BBBC-58AC-42F8-97C1-5CF3C9725513}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA51BBBC-58AC-42F8-97C1-5CF3C9725513}.Release|Any CPU.Build.0 = Release|Any CPU + {EA51BBBC-58AC-42F8-97C1-5CF3C9725513}.Release|x64.ActiveCfg = Release|Any CPU + {EA51BBBC-58AC-42F8-97C1-5CF3C9725513}.Release|x64.Build.0 = Release|Any CPU + {EA51BBBC-58AC-42F8-97C1-5CF3C9725513}.Release|x86.ActiveCfg = Release|Any CPU + {EA51BBBC-58AC-42F8-97C1-5CF3C9725513}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -434,6 +448,7 @@ Global {82C0816D-7051-4DDB-9B9E-6777973AD7AE} = {142C8260-90B5-4D72-9564-17BFDD72F496} {38C0E122-64D0-497F-ABB0-C6A9C3349F02} = {CA4538F5-9DA8-4139-B891-A13279889F79} {E1E8A599-AB42-4551-8C24-BE4404B65283} = {CA4538F5-9DA8-4139-B891-A13279889F79} + {EA51BBBC-58AC-42F8-97C1-5CF3C9725513} = {CA4538F5-9DA8-4139-B891-A13279889F79} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {39E3AF62-B1FD-4156-92AA-F4FA99B5AD89} diff --git a/src/Security/Authorization/Policy/ref/Microsoft.AspNetCore.Authorization.Policy.netcoreapp.cs b/src/Security/Authorization/Policy/ref/Microsoft.AspNetCore.Authorization.Policy.netcoreapp.cs index 2e16cc4375..0fa980dadc 100644 --- a/src/Security/Authorization/Policy/ref/Microsoft.AspNetCore.Authorization.Policy.netcoreapp.cs +++ b/src/Security/Authorization/Policy/ref/Microsoft.AspNetCore.Authorization.Policy.netcoreapp.cs @@ -9,9 +9,19 @@ namespace Microsoft.AspNetCore.Authorization [System.Diagnostics.DebuggerStepThroughAttribute] public System.Threading.Tasks.Task Invoke(Microsoft.AspNetCore.Http.HttpContext context) { throw null; } } + public partial interface IAuthorizationMiddlewareResultHandler + { + System.Threading.Tasks.Task HandleAsync(Microsoft.AspNetCore.Http.RequestDelegate next, Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Authorization.AuthorizationPolicy policy, Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult authorizeResult); + } } namespace Microsoft.AspNetCore.Authorization.Policy { + public partial class AuthorizationMiddlewareResultHandler : Microsoft.AspNetCore.Authorization.IAuthorizationMiddlewareResultHandler + { + public AuthorizationMiddlewareResultHandler() { } + [System.Diagnostics.DebuggerStepThroughAttribute] + public System.Threading.Tasks.Task HandleAsync(Microsoft.AspNetCore.Http.RequestDelegate next, Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Authorization.AuthorizationPolicy policy, Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult authorizeResult) { throw null; } + } public partial interface IPolicyEvaluator { System.Threading.Tasks.Task AuthenticateAsync(Microsoft.AspNetCore.Authorization.AuthorizationPolicy policy, Microsoft.AspNetCore.Http.HttpContext context); @@ -20,11 +30,13 @@ namespace Microsoft.AspNetCore.Authorization.Policy public partial class PolicyAuthorizationResult { internal PolicyAuthorizationResult() { } + public Microsoft.AspNetCore.Authorization.AuthorizationFailure AuthorizationFailure { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } public bool Challenged { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } public bool Forbidden { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } public bool Succeeded { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } public static Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult Challenge() { throw null; } public static Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult Forbid() { throw null; } + public static Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult Forbid(Microsoft.AspNetCore.Authorization.AuthorizationFailure authorizationFailure) { throw null; } public static Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult Success() { throw null; } } public partial class PolicyEvaluator : Microsoft.AspNetCore.Authorization.Policy.IPolicyEvaluator diff --git a/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs b/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs index af115e9daa..8963a130bf 100644 --- a/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs +++ b/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs @@ -2,9 +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.Threading.Tasks; -using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization.Policy; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -66,40 +64,8 @@ namespace Microsoft.AspNetCore.Authorization // Note that the resource will be null if there is no matched endpoint var authorizeResult = await policyEvaluator.AuthorizeAsync(policy, authenticateResult, context, resource: endpoint); - if (authorizeResult.Challenged) - { - if (policy.AuthenticationSchemes.Count > 0) - { - foreach (var scheme in policy.AuthenticationSchemes) - { - await context.ChallengeAsync(scheme); - } - } - else - { - await context.ChallengeAsync(); - } - - return; - } - else if (authorizeResult.Forbidden) - { - if (policy.AuthenticationSchemes.Count > 0) - { - foreach (var scheme in policy.AuthenticationSchemes) - { - await context.ForbidAsync(scheme); - } - } - else - { - await context.ForbidAsync(); - } - - return; - } - - await _next(context); + var authorizationMiddlewareResultHandler = context.RequestServices.GetRequiredService(); + await authorizationMiddlewareResultHandler.HandleAsync(_next, context, policy, authorizeResult); } } } diff --git a/src/Security/Authorization/Policy/src/AuthorizationMiddlewareResultHandler.cs b/src/Security/Authorization/Policy/src/AuthorizationMiddlewareResultHandler.cs new file mode 100644 index 0000000000..7c7d4592b4 --- /dev/null +++ b/src/Security/Authorization/Policy/src/AuthorizationMiddlewareResultHandler.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authorization.Policy +{ + public class AuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler + { + public async Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult) + { + if (authorizeResult.Challenged) + { + if (policy.AuthenticationSchemes.Count > 0) + { + foreach (var scheme in policy.AuthenticationSchemes) + { + await context.ChallengeAsync(scheme); + } + } + else + { + await context.ChallengeAsync(); + } + + return; + } + else if (authorizeResult.Forbidden) + { + if (policy.AuthenticationSchemes.Count > 0) + { + foreach (var scheme in policy.AuthenticationSchemes) + { + await context.ForbidAsync(scheme); + } + } + else + { + await context.ForbidAsync(); + } + + return; + } + + await next(context); + } + } +} diff --git a/src/Security/Authorization/Policy/src/IAuthorizationMiddlewareResultHandler.cs b/src/Security/Authorization/Policy/src/IAuthorizationMiddlewareResultHandler.cs new file mode 100644 index 0000000000..af07449df4 --- /dev/null +++ b/src/Security/Authorization/Policy/src/IAuthorizationMiddlewareResultHandler.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization.Policy; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authorization +{ + public interface IAuthorizationMiddlewareResultHandler + { + Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult); + } +} diff --git a/src/Security/Authorization/Policy/src/IPolicyEvaluator.cs b/src/Security/Authorization/Policy/src/IPolicyEvaluator.cs index dd5e6fc038..b6103950bc 100644 --- a/src/Security/Authorization/Policy/src/IPolicyEvaluator.cs +++ b/src/Security/Authorization/Policy/src/IPolicyEvaluator.cs @@ -33,8 +33,8 @@ namespace Microsoft.AspNetCore.Authorization.Policy /// If a resource is not required for policy evaluation you may pass null as the value. /// /// Returns if authorization succeeds. - /// Otherwise returns if , otherwise + /// Otherwise returns if , otherwise /// returns Task AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object resource); } -} \ No newline at end of file +} diff --git a/src/Security/Authorization/Policy/src/PolicyAuthorizationResult.cs b/src/Security/Authorization/Policy/src/PolicyAuthorizationResult.cs index d7d481dcd6..a87f0451b9 100644 --- a/src/Security/Authorization/Policy/src/PolicyAuthorizationResult.cs +++ b/src/Security/Authorization/Policy/src/PolicyAuthorizationResult.cs @@ -22,14 +22,22 @@ namespace Microsoft.AspNetCore.Authorization.Policy /// public bool Succeeded { get; private set; } + /// + /// Contains information about why authorization failed. + /// + public AuthorizationFailure AuthorizationFailure { get; private set; } + public static PolicyAuthorizationResult Challenge() => new PolicyAuthorizationResult { Challenged = true }; public static PolicyAuthorizationResult Forbid() - => new PolicyAuthorizationResult { Forbidden = true }; + => Forbid(null); + + public static PolicyAuthorizationResult Forbid(AuthorizationFailure authorizationFailure) + => new PolicyAuthorizationResult { Forbidden = true, AuthorizationFailure = authorizationFailure }; public static PolicyAuthorizationResult Success() => new PolicyAuthorizationResult { Succeeded = true }; } -} \ No newline at end of file +} diff --git a/src/Security/Authorization/Policy/src/PolicyEvaluator.cs b/src/Security/Authorization/Policy/src/PolicyEvaluator.cs index 3100ff4d3e..509eb6a4de 100644 --- a/src/Security/Authorization/Policy/src/PolicyEvaluator.cs +++ b/src/Security/Authorization/Policy/src/PolicyEvaluator.cs @@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.Authorization.Policy } } - return (context.User?.Identity?.IsAuthenticated ?? false) + return (context.User?.Identity?.IsAuthenticated ?? false) ? AuthenticateResult.Success(new AuthenticationTicket(context.User, "context.User")) : AuthenticateResult.NoResult(); } @@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Authorization.Policy /// If a resource is not required for policy evaluation you may pass null as the value. /// /// Returns if authorization succeeds. - /// Otherwise returns if , otherwise + /// Otherwise returns if , otherwise /// returns public virtual async Task AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object resource) { @@ -88,9 +88,9 @@ namespace Microsoft.AspNetCore.Authorization.Policy } // If authentication was successful, return forbidden, otherwise challenge - return (authenticationResult.Succeeded) - ? PolicyAuthorizationResult.Forbid() + return (authenticationResult.Succeeded) + ? PolicyAuthorizationResult.Forbid(result.Failure) : PolicyAuthorizationResult.Challenge(); } } -} \ No newline at end of file +} diff --git a/src/Security/Authorization/Policy/src/PolicyServiceCollectionExtensions.cs b/src/Security/Authorization/Policy/src/PolicyServiceCollectionExtensions.cs index d24a5a243f..3a251dd09d 100644 --- a/src/Security/Authorization/Policy/src/PolicyServiceCollectionExtensions.cs +++ b/src/Security/Authorization/Policy/src/PolicyServiceCollectionExtensions.cs @@ -26,7 +26,8 @@ namespace Microsoft.Extensions.DependencyInjection } services.TryAddSingleton(); - services.TryAdd(ServiceDescriptor.Transient()); + services.TryAddTransient(); + services.TryAddTransient(); return services; } diff --git a/src/Security/Authorization/test/AuthorizationMiddlewareResultHandlerTests.cs b/src/Security/Authorization/test/AuthorizationMiddlewareResultHandlerTests.cs new file mode 100644 index 0000000000..7b70c40e72 --- /dev/null +++ b/src/Security/Authorization/test/AuthorizationMiddlewareResultHandlerTests.cs @@ -0,0 +1,150 @@ +// 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; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization.Policy; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Authorization.Test +{ + public class AuthorizationMiddlewareResultHandlerTests + { + [Fact] + public async Task CallRequestDelegate_If_PolicyAuthorizationResultSucceeded() + { + var requestDelegate = new Mock(); + var httpContext = CreateHttpContext(); + var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + var policyAuthorizationResult = PolicyAuthorizationResult.Success(); + var handler = CreateAuthorizationMiddlewareResultHandler(); + + await handler.HandleAsync(requestDelegate.Object, httpContext, policy, policyAuthorizationResult); + + requestDelegate.Verify(next => next(It.IsAny()), Times.Once); + } + + [Fact] + public async Task NotCallRequestDelegate_If_PolicyAuthorizationResultWasChallenged() + { + var requestDelegate = new Mock(); + var httpContext = CreateHttpContext(); + var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + var policyAuthorizationResult = PolicyAuthorizationResult.Challenge(); + var handler = CreateAuthorizationMiddlewareResultHandler(); + + await handler.HandleAsync(requestDelegate.Object, httpContext, policy, policyAuthorizationResult); + + requestDelegate.Verify(next => next(It.IsAny()), Times.Never); + } + + [Fact] + public async Task NotCallRequestDelegate_If_PolicyAuthorizationResultWasForbidden() + { + var requestDelegate = new Mock(); + var httpContext = CreateHttpContext(); + var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + var policyAuthorizationResult = PolicyAuthorizationResult.Forbid(); + var handler = CreateAuthorizationMiddlewareResultHandler(); + + await handler.HandleAsync(requestDelegate.Object, httpContext, policy, policyAuthorizationResult); + + requestDelegate.Verify(next => next(It.IsAny()), Times.Never); + } + + [Fact] + public async Task ChallangeEachAuthenticationScheme_If_PolicyAuthorizationResultWasChallenged() + { + var authenticationServiceMock = new Mock(); + var requestDelegate = new Mock(); + var httpContext = CreateHttpContext(authenticationServiceMock.Object); + var firstScheme = Guid.NewGuid().ToString(); + var secondScheme = Guid.NewGuid().ToString(); + var thirdScheme = Guid.NewGuid().ToString(); + var policy = new AuthorizationPolicyBuilder() + .RequireAuthenticatedUser() + .AddAuthenticationSchemes(firstScheme, secondScheme, thirdScheme) + .Build(); + var policyAuthorizationResult = PolicyAuthorizationResult.Challenge(); + var handler = CreateAuthorizationMiddlewareResultHandler(); + + await handler.HandleAsync(requestDelegate.Object, httpContext, policy, policyAuthorizationResult); + + authenticationServiceMock.Verify(service => service.ChallengeAsync(httpContext, It.IsAny(), null), Times.Exactly(3)); + authenticationServiceMock.Verify(service => service.ChallengeAsync(httpContext, firstScheme, null), Times.Once); + authenticationServiceMock.Verify(service => service.ChallengeAsync(httpContext, secondScheme, null), Times.Once); + authenticationServiceMock.Verify(service => service.ChallengeAsync(httpContext, thirdScheme, null), Times.Once); + } + + [Fact] + public async Task ChallangeWithoutAuthenticationScheme_If_PolicyAuthorizationResultWasChallenged() + { + var authenticationServiceMock = new Mock(); + var requestDelegate = new Mock(); + var httpContext = CreateHttpContext(authenticationServiceMock.Object); + var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + var policyAuthorizationResult = PolicyAuthorizationResult.Challenge(); + var handler = CreateAuthorizationMiddlewareResultHandler(); + + await handler.HandleAsync(requestDelegate.Object, httpContext, policy, policyAuthorizationResult); + + authenticationServiceMock.Verify(service => service.ChallengeAsync(httpContext, null, null), Times.Once); + } + + [Fact] + public async Task ForbidEachAuthenticationScheme_If_PolicyAuthorizationResultWasForbidden() + { + var authenticationServiceMock = new Mock(); + var requestDelegate = new Mock(); + var httpContext = CreateHttpContext(authenticationServiceMock.Object); + var firstScheme = Guid.NewGuid().ToString(); + var secondScheme = Guid.NewGuid().ToString(); + var thirdScheme = Guid.NewGuid().ToString(); + var policy = new AuthorizationPolicyBuilder() + .RequireAuthenticatedUser() + .AddAuthenticationSchemes(firstScheme, secondScheme, thirdScheme) + .Build(); + var policyAuthorizationResult = PolicyAuthorizationResult.Forbid(); + var handler = CreateAuthorizationMiddlewareResultHandler(); + + await handler.HandleAsync(requestDelegate.Object, httpContext, policy, policyAuthorizationResult); + + authenticationServiceMock.Verify(service => service.ForbidAsync(httpContext, It.IsAny(), null), Times.Exactly(3)); + authenticationServiceMock.Verify(service => service.ForbidAsync(httpContext, firstScheme, null), Times.Once); + authenticationServiceMock.Verify(service => service.ForbidAsync(httpContext, secondScheme, null), Times.Once); + authenticationServiceMock.Verify(service => service.ForbidAsync(httpContext, thirdScheme, null), Times.Once); + } + + [Fact] + public async Task ForbidWithoutAuthenticationScheme_If_PolicyAuthorizationResultWasForbidden() + { + var authenticationServiceMock = new Mock(); + var requestDelegate = new Mock(); + var httpContext = CreateHttpContext(authenticationServiceMock.Object); + var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + var policyAuthorizationResult = PolicyAuthorizationResult.Forbid(); + var handler = CreateAuthorizationMiddlewareResultHandler(); + + await handler.HandleAsync(requestDelegate.Object, httpContext, policy, policyAuthorizationResult); + + authenticationServiceMock.Verify(service => service.ForbidAsync(httpContext, null, null), Times.Once); + } + + private HttpContext CreateHttpContext(IAuthenticationService authenticationService = null) + { + var services = new ServiceCollection(); + + services.AddTransient(provider => authenticationService ?? new Mock().Object); + + var serviceProvider = services.BuildServiceProvider(); + + return new DefaultHttpContext { RequestServices = serviceProvider }; + } + + private AuthorizationMiddlewareResultHandler CreateAuthorizationMiddlewareResultHandler() => new AuthorizationMiddlewareResultHandler(); + } +} diff --git a/src/Security/Authorization/test/AuthorizationMiddlewareTests.cs b/src/Security/Authorization/test/AuthorizationMiddlewareTests.cs index 075a6ee655..e1e50f0602 100644 --- a/src/Security/Authorization/test/AuthorizationMiddlewareTests.cs +++ b/src/Security/Authorization/test/AuthorizationMiddlewareTests.cs @@ -5,9 +5,9 @@ using System; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization.Policy; using Microsoft.AspNetCore.Authorization.Test.TestObjects; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.DependencyInjection; using Moq; using Xunit; @@ -449,6 +449,7 @@ namespace Microsoft.AspNetCore.Authorization.Test authenticationService = authenticationService ?? Mock.Of(); serviceCollection.AddSingleton(authenticationService); + serviceCollection.AddTransient(); serviceCollection.AddOptions(); serviceCollection.AddLogging(); serviceCollection.AddAuthorization(); diff --git a/src/Security/Authorization/test/PolicyEvaluatorTests.cs b/src/Security/Authorization/test/PolicyEvaluatorTests.cs index 4f374a884e..ca3b581858 100644 --- a/src/Security/Authorization/test/PolicyEvaluatorTests.cs +++ b/src/Security/Authorization/test/PolicyEvaluatorTests.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Moq; using Xunit; namespace Microsoft.AspNetCore.Authorization.Policy.Test @@ -120,6 +121,28 @@ namespace Microsoft.AspNetCore.Authorization.Policy.Test Assert.True(result.Forbidden); } + [Fact] + public async Task AuthorizeForbidsAndFailureIsIncludedIfAuthenticationSuceeds() + { + // Arrange + var evaluator = BuildEvaluator(); + var context = new DefaultHttpContext(); + var policy = new AuthorizationPolicyBuilder() + .AddRequirements(new DummyRequirement()) + .RequireAssertion(_ => false) + .Build(); + + // Act + var result = await evaluator.AuthorizeAsync(policy, AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), "scheme")), context, resource: null); + + // Assert + Assert.False(result.Succeeded); + Assert.False(result.Challenged); + Assert.True(result.Forbidden); + Assert.NotNull(result.AuthorizationFailure); + Assert.Contains(result.AuthorizationFailure.FailedRequirements, requirement => requirement is DummyRequirement); + } + private IPolicyEvaluator BuildEvaluator(Action setupServices = null) { var services = new ServiceCollection() @@ -204,5 +227,6 @@ namespace Microsoft.AspNetCore.Authorization.Policy.Test } } + private class DummyRequirement : IAuthorizationRequirement {} } } diff --git a/src/Security/samples/CustomAuthorizationFailureResponse/Authentication/SampleAuthenticationHandler.cs b/src/Security/samples/CustomAuthorizationFailureResponse/Authentication/SampleAuthenticationHandler.cs new file mode 100644 index 0000000000..ac6cf5b619 --- /dev/null +++ b/src/Security/samples/CustomAuthorizationFailureResponse/Authentication/SampleAuthenticationHandler.cs @@ -0,0 +1,22 @@ +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace CustomAuthorizationFailureResponse.Authentication +{ + public class SampleAuthenticationHandler : AuthenticationHandler + { + private readonly ClaimsPrincipal _id; + + public SampleAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) + { + _id = new ClaimsPrincipal(new ClaimsIdentity("Api")); + } + + protected override Task HandleAuthenticateAsync() + => Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(_id, "Api"))); + } +} diff --git a/src/Security/samples/CustomAuthorizationFailureResponse/Authentication/SampleAuthenticationSchemes.cs b/src/Security/samples/CustomAuthorizationFailureResponse/Authentication/SampleAuthenticationSchemes.cs new file mode 100644 index 0000000000..ccb894cc65 --- /dev/null +++ b/src/Security/samples/CustomAuthorizationFailureResponse/Authentication/SampleAuthenticationSchemes.cs @@ -0,0 +1,7 @@ +namespace CustomAuthorizationFailureResponse.Authentication +{ + public static class SampleAuthenticationSchemes + { + public const string CustomScheme = "CustomScheme"; + } +} diff --git a/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/Handlers/SampleRequirementHandler.cs b/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/Handlers/SampleRequirementHandler.cs new file mode 100644 index 0000000000..0a03bc602f --- /dev/null +++ b/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/Handlers/SampleRequirementHandler.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using CustomAuthorizationFailureResponse.Authorization.Requirements; +using Microsoft.AspNetCore.Authorization; + +namespace CustomAuthorizationFailureResponse.Authorization.Handlers +{ + public class SampleRequirementHandler : AuthorizationHandler + { + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SampleRequirement requirement) + { + // assuming the requirement was not met + return Task.CompletedTask; + } + } +} diff --git a/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/Handlers/SampleWithCustomMessageRequirementHandler.cs b/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/Handlers/SampleWithCustomMessageRequirementHandler.cs new file mode 100644 index 0000000000..e691db8673 --- /dev/null +++ b/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/Handlers/SampleWithCustomMessageRequirementHandler.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using CustomAuthorizationFailureResponse.Authorization.Requirements; +using Microsoft.AspNetCore.Authorization; + +namespace CustomAuthorizationFailureResponse.Authorization.Handlers +{ + public class SampleWithCustomMessageRequirementHandler : AuthorizationHandler + { + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SampleWithCustomMessageRequirement requirement) + { + // assuming the requirement was not met + return Task.CompletedTask; + } + } +} diff --git a/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/Requirements/SampleRequirement.cs b/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/Requirements/SampleRequirement.cs new file mode 100644 index 0000000000..b0b11832ea --- /dev/null +++ b/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/Requirements/SampleRequirement.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Authorization; + +namespace CustomAuthorizationFailureResponse.Authorization.Requirements +{ + public class SampleRequirement : IAuthorizationRequirement + { + } +} diff --git a/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/Requirements/SampleWithCustomMessageRequirement.cs b/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/Requirements/SampleWithCustomMessageRequirement.cs new file mode 100644 index 0000000000..275a176957 --- /dev/null +++ b/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/Requirements/SampleWithCustomMessageRequirement.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Authorization; + +namespace CustomAuthorizationFailureResponse.Authorization.Requirements +{ + public class SampleWithCustomMessageRequirement : IAuthorizationRequirement + { + } +} diff --git a/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/SampleAuthorizationMiddlewareResultHandler.cs b/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/SampleAuthorizationMiddlewareResultHandler.cs new file mode 100644 index 0000000000..199a44798d --- /dev/null +++ b/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/SampleAuthorizationMiddlewareResultHandler.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using CustomAuthorizationFailureResponse.Authorization.Requirements; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization.Policy; +using Microsoft.AspNetCore.Http; + +namespace CustomAuthorizationFailureResponse.Authorization +{ + public class SampleAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler + { + private readonly IAuthorizationMiddlewareResultHandler _handler; + + public SampleAuthorizationMiddlewareResultHandler(IAuthorizationMiddlewareResultHandler handler) + { + _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + } + + public async Task HandleAsync( + RequestDelegate requestDelegate, + HttpContext httpContext, + AuthorizationPolicy authorizationPolicy, + PolicyAuthorizationResult policyAuthorizationResult) + { + // if the authorization was forbidden, let's use custom logic to handle that. + if (policyAuthorizationResult.Forbidden && policyAuthorizationResult.AuthorizationFailure != null) + { + // as an example, let's return 404 if specific requirement has failed + if (policyAuthorizationResult.AuthorizationFailure.FailedRequirements.Any(requirement => requirement is SampleRequirement)) + { + httpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; + await httpContext.Response.WriteAsync(Startup.CustomForbiddenMessage); + + // return right away as the default implementation would overwrite the status code + return; + } + else if (policyAuthorizationResult.AuthorizationFailure.FailedRequirements.Any(requirement => requirement is SampleWithCustomMessageRequirement)) + { + // if other requirements failed, let's just use a custom message + // but we have to use OnStarting callback because the default handlers will want to modify i.e. status code of the response + // and modifications of the response are not allowed once the writing has started + var message = Startup.CustomForbiddenMessage; + + httpContext.Response.OnStarting(() => httpContext.Response.BodyWriter.WriteAsync(Encoding.UTF8.GetBytes(message)).AsTask()); + } + } + + await _handler.HandleAsync(requestDelegate, httpContext, authorizationPolicy, policyAuthorizationResult); + } + } +} diff --git a/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/SamplePolicyNames.cs b/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/SamplePolicyNames.cs new file mode 100644 index 0000000000..5c395ce6e7 --- /dev/null +++ b/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/SamplePolicyNames.cs @@ -0,0 +1,8 @@ +namespace CustomAuthorizationFailureResponse.Authorization +{ + public static class SamplePolicyNames + { + public const string CustomPolicy = "Custom"; + public const string CustomPolicyWithCustomForbiddenMessage = "CustomPolicyWithCustomForbiddenMessage"; + } +} diff --git a/src/Security/samples/CustomAuthorizationFailureResponse/Controllers/SampleController.cs b/src/Security/samples/CustomAuthorizationFailureResponse/Controllers/SampleController.cs new file mode 100644 index 0000000000..efb908fb96 --- /dev/null +++ b/src/Security/samples/CustomAuthorizationFailureResponse/Controllers/SampleController.cs @@ -0,0 +1,25 @@ +using CustomAuthorizationFailureResponse.Authorization; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace CustomAuthorizationFailureResponse.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class SampleController : ControllerBase + { + [HttpGet("customPolicyWithCustomForbiddenMessage")] + [Authorize(Policy = SamplePolicyNames.CustomPolicyWithCustomForbiddenMessage)] + public string GetWithCustomPolicyWithCustomForbiddenMessage() + { + return "Hello world from GetWithCustomPolicyWithCustomForbiddenMessage"; + } + + [HttpGet("customPolicy")] + [Authorize(Policy = SamplePolicyNames.CustomPolicy)] + public string GetWithCustomPolicy() + { + return "Hello world from GetWithCustomPolicy"; + } + } +} diff --git a/src/Security/samples/CustomAuthorizationFailureResponse/CustomAuthorizationFailureResponse.csproj b/src/Security/samples/CustomAuthorizationFailureResponse/CustomAuthorizationFailureResponse.csproj new file mode 100644 index 0000000000..280dc59e64 --- /dev/null +++ b/src/Security/samples/CustomAuthorizationFailureResponse/CustomAuthorizationFailureResponse.csproj @@ -0,0 +1,15 @@ + + + + $(DefaultNetCoreTargetFramework) + true + + + + + + + + + + diff --git a/src/Security/samples/CustomAuthorizationFailureResponse/Extensions/ServiceCollectionExtensions.cs b/src/Security/samples/CustomAuthorizationFailureResponse/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000..418d989b3b --- /dev/null +++ b/src/Security/samples/CustomAuthorizationFailureResponse/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,21 @@ +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; + +namespace CustomAuthorizationFailureResponse.Extensions +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection Decorate(this IServiceCollection services) + { + var descriptors = services.Where(descriptor => descriptor.ServiceType == typeof(TServiceType)).ToList(); + foreach(var descriptor in descriptors) + { + var index = services.IndexOf(descriptor); + services[index] = ServiceDescriptor.Describe(typeof(TServiceType), provider => ActivatorUtilities.CreateInstance(provider, typeof(TServiceImplementation), ActivatorUtilities.GetServiceOrCreateInstance(provider, descriptor.ImplementationType)), descriptor.Lifetime); + } + + return services; + } + } +} diff --git a/src/Security/samples/CustomAuthorizationFailureResponse/Program.cs b/src/Security/samples/CustomAuthorizationFailureResponse/Program.cs new file mode 100644 index 0000000000..ae98ba02ed --- /dev/null +++ b/src/Security/samples/CustomAuthorizationFailureResponse/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace CustomAuthorizationFailureResponse +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/src/Security/samples/CustomAuthorizationFailureResponse/Startup.cs b/src/Security/samples/CustomAuthorizationFailureResponse/Startup.cs new file mode 100644 index 0000000000..eaf2bbdc81 --- /dev/null +++ b/src/Security/samples/CustomAuthorizationFailureResponse/Startup.cs @@ -0,0 +1,54 @@ +using CustomAuthorizationFailureResponse.Authentication; +using CustomAuthorizationFailureResponse.Authorization; +using CustomAuthorizationFailureResponse.Authorization.Handlers; +using CustomAuthorizationFailureResponse.Authorization.Requirements; +using CustomAuthorizationFailureResponse.Extensions; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace CustomAuthorizationFailureResponse +{ + public class Startup + { + public const string CustomForbiddenMessage = "Some info about the error"; + + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + + services + .AddAuthentication(SampleAuthenticationSchemes.CustomScheme) + .AddScheme(SampleAuthenticationSchemes.CustomScheme, o => { }); + + services.AddAuthorization(options => options.AddPolicy(SamplePolicyNames.CustomPolicy, policy => policy.AddRequirements(new SampleRequirement()))); + services.AddAuthorization(options => options.AddPolicy(SamplePolicyNames.CustomPolicyWithCustomForbiddenMessage, policy => policy.AddRequirements(new SampleWithCustomMessageRequirement()))); + + services.AddTransient(); + services.Decorate(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + }); + } + } +} diff --git a/src/Security/samples/CustomAuthorizationFailureResponse/appsettings.Development.json b/src/Security/samples/CustomAuthorizationFailureResponse/appsettings.Development.json new file mode 100644 index 0000000000..8983e0fc1c --- /dev/null +++ b/src/Security/samples/CustomAuthorizationFailureResponse/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/src/Security/samples/CustomAuthorizationFailureResponse/appsettings.json b/src/Security/samples/CustomAuthorizationFailureResponse/appsettings.json new file mode 100644 index 0000000000..6cf0b42b23 --- /dev/null +++ b/src/Security/samples/CustomAuthorizationFailureResponse/appsettings.json @@ -0,0 +1,7 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + } +} diff --git a/src/Security/test/AuthSamples.FunctionalTests/AuthSamples.FunctionalTests.csproj b/src/Security/test/AuthSamples.FunctionalTests/AuthSamples.FunctionalTests.csproj index 1753f9f548..1c6b96c02b 100644 --- a/src/Security/test/AuthSamples.FunctionalTests/AuthSamples.FunctionalTests.csproj +++ b/src/Security/test/AuthSamples.FunctionalTests/AuthSamples.FunctionalTests.csproj @@ -14,6 +14,7 @@ + @@ -33,6 +34,7 @@ <_PublishFiles Include="$(ArtifactsBinDir)ClaimsTransformation\$(Configuration)\$(DefaultNetCoreTargetFramework)\ClaimsTransformation.deps.json" /> <_PublishFiles Include="$(ArtifactsBinDir)Cookies\$(Configuration)\$(DefaultNetCoreTargetFramework)\Cookies.deps.json" /> + <_PublishFiles Include="$(ArtifactsBinDir)CustomAuthorizationFailureResponse\$(Configuration)\$(DefaultNetCoreTargetFramework)\CustomAuthorizationFailureResponse.deps.json" /> <_PublishFiles Include="$(ArtifactsBinDir)CustomPolicyProvider\$(Configuration)\$(DefaultNetCoreTargetFramework)\CustomPolicyProvider.deps.json" /> <_PublishFiles Include="$(ArtifactsBinDir)DynamicSchemes\$(Configuration)\$(DefaultNetCoreTargetFramework)\DynamicSchemes.deps.json" /> <_PublishFiles Include="$(ArtifactsBinDir)Identity.ExternalClaims\$(Configuration)\$(DefaultNetCoreTargetFramework)\Identity.ExternalClaims.deps.json" /> @@ -40,43 +42,25 @@ <_PublishFiles Include="$(ArtifactsBinDir)StaticFilesAuth\$(Configuration)\$(DefaultNetCoreTargetFramework)\StaticFilesAuth.deps.json" /> <_claimsWwwrootFiles Include="$(MSBuildThisFileDirectory)..\..\samples\ClaimsTransformation\wwwroot\**\*.*" /> <_cookiesWwwrootFiles Include="$(MSBuildThisFileDirectory)..\..\samples\Cookies\wwwroot\**\*.*" /> + <_customAuthorizationFailureResponseFiles Include="$(MSBuildThisFileDirectory)..\..\samples\CustomAuthorizationFailureResponse\**\*.*" /> <_customProviderFiles Include="$(MSBuildThisFileDirectory)..\..\samples\CustomPolicyProvider\**\*.*" /> <_schemesWwwrootFiles Include="$(MSBuildThisFileDirectory)..\..\samples\DynamicSchemes\wwwroot\**\*.*" /> <_identityWwwrootFiles Include="$(MSBuildThisFileDirectory)..\..\samples\Identity.ExternalClaims\wwwroot\**\*.*" /> <_pathWwwrootFiles Include="$(MSBuildThisFileDirectory)..\..\samples\PathSchemeSelection\wwwroot\**\*.*" /> <_staticFiles Include="$(MSBuildThisFileDirectory)..\..\samples\StaticFilesAuth\**\*.*" /> - - - - - - - - + + + + + + + + + - + diff --git a/src/Security/test/AuthSamples.FunctionalTests/CustomAuthorizationFailureResponseTests.cs b/src/Security/test/AuthSamples.FunctionalTests/CustomAuthorizationFailureResponseTests.cs new file mode 100644 index 0000000000..06258daab6 --- /dev/null +++ b/src/Security/test/AuthSamples.FunctionalTests/CustomAuthorizationFailureResponseTests.cs @@ -0,0 +1,41 @@ +// 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.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Testing; +using Xunit; + +namespace AuthSamples.FunctionalTests +{ + public class CustomAuthorizationFailureResponseTests : IClassFixture> + { + private HttpClient Client { get; } + + public CustomAuthorizationFailureResponseTests(WebApplicationFactory fixture) + { + Client = fixture.CreateClient(); + } + + [Fact] + public async Task SampleGetWithCustomPolicyWithCustomForbiddenMessage_Returns403WithCustomMessage() + { + var response = await Client.GetAsync("api/Sample/customPolicyWithCustomForbiddenMessage"); + var content = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + Assert.Equal(CustomAuthorizationFailureResponse.Startup.CustomForbiddenMessage, content); + } + + [Fact] + public async Task SampleGetWithCustomPolicy_Returns404WithCustomMessage() + { + var response = await Client.GetAsync("api/Sample/customPolicy"); + var content = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + Assert.Equal(CustomAuthorizationFailureResponse.Startup.CustomForbiddenMessage, content); + } + } +}