Allow custom handling of authorization failures (with sample app) (#21117)

This commit is contained in:
Dawid Szmidka 2020-05-19 10:56:21 +03:00 committed by GitHub
parent 263c376a80
commit 6c7a8bb397
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 624 additions and 76 deletions

View File

@ -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}

View File

@ -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<Microsoft.AspNetCore.Authentication.AuthenticateResult> 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

View File

@ -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<IAuthorizationMiddlewareResultHandler>();
await authorizationMiddlewareResultHandler.HandleAsync(_next, context, policy, authorizeResult);
}
}
}

View File

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

View File

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

View File

@ -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.
/// </param>
/// <returns>Returns <see cref="PolicyAuthorizationResult.Success"/> if authorization succeeds.
/// Otherwise returns <see cref="PolicyAuthorizationResult.Forbid"/> if <see cref="AuthenticateResult.Succeeded"/>, otherwise
/// Otherwise returns <see cref="PolicyAuthorizationResult.Forbid(AuthorizationFailure)"/> if <see cref="AuthenticateResult.Succeeded"/>, otherwise
/// returns <see cref="PolicyAuthorizationResult.Challenge"/></returns>
Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object resource);
}
}
}

View File

@ -22,14 +22,22 @@ namespace Microsoft.AspNetCore.Authorization.Policy
/// </summary>
public bool Succeeded { get; private set; }
/// <summary>
/// Contains information about why authorization failed.
/// </summary>
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 };
}
}
}

View File

@ -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.
/// </param>
/// <returns>Returns <see cref="PolicyAuthorizationResult.Success"/> if authorization succeeds.
/// Otherwise returns <see cref="PolicyAuthorizationResult.Forbid"/> if <see cref="AuthenticateResult.Succeeded"/>, otherwise
/// Otherwise returns <see cref="PolicyAuthorizationResult.Forbid(AuthorizationFailure)"/> if <see cref="AuthenticateResult.Succeeded"/>, otherwise
/// returns <see cref="PolicyAuthorizationResult.Challenge"/></returns>
public virtual async Task<PolicyAuthorizationResult> 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();
}
}
}
}

View File

@ -26,7 +26,8 @@ namespace Microsoft.Extensions.DependencyInjection
}
services.TryAddSingleton<AuthorizationPolicyMarkerService>();
services.TryAdd(ServiceDescriptor.Transient<IPolicyEvaluator, PolicyEvaluator>());
services.TryAddTransient<IPolicyEvaluator, PolicyEvaluator>();
services.TryAddTransient<IAuthorizationMiddlewareResultHandler, AuthorizationMiddlewareResultHandler>();
return services;
}

View File

@ -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<RequestDelegate>();
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<HttpContext>()), Times.Once);
}
[Fact]
public async Task NotCallRequestDelegate_If_PolicyAuthorizationResultWasChallenged()
{
var requestDelegate = new Mock<RequestDelegate>();
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<HttpContext>()), Times.Never);
}
[Fact]
public async Task NotCallRequestDelegate_If_PolicyAuthorizationResultWasForbidden()
{
var requestDelegate = new Mock<RequestDelegate>();
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<HttpContext>()), Times.Never);
}
[Fact]
public async Task ChallangeEachAuthenticationScheme_If_PolicyAuthorizationResultWasChallenged()
{
var authenticationServiceMock = new Mock<IAuthenticationService>();
var requestDelegate = new Mock<RequestDelegate>();
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<string>(), 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<IAuthenticationService>();
var requestDelegate = new Mock<RequestDelegate>();
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<IAuthenticationService>();
var requestDelegate = new Mock<RequestDelegate>();
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<string>(), 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<IAuthenticationService>();
var requestDelegate = new Mock<RequestDelegate>();
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<IAuthenticationService>().Object);
var serviceProvider = services.BuildServiceProvider();
return new DefaultHttpContext { RequestServices = serviceProvider };
}
private AuthorizationMiddlewareResultHandler CreateAuthorizationMiddlewareResultHandler() => new AuthorizationMiddlewareResultHandler();
}
}

View File

@ -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<IAuthenticationService>();
serviceCollection.AddSingleton(authenticationService);
serviceCollection.AddTransient<IAuthorizationMiddlewareResultHandler, AuthorizationMiddlewareResultHandler>();
serviceCollection.AddOptions();
serviceCollection.AddLogging();
serviceCollection.AddAuthorization();

View File

@ -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<IServiceCollection> setupServices = null)
{
var services = new ServiceCollection()
@ -204,5 +227,6 @@ namespace Microsoft.AspNetCore.Authorization.Policy.Test
}
}
private class DummyRequirement : IAuthorizationRequirement {}
}
}

View File

@ -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<AuthenticationSchemeOptions>
{
private readonly ClaimsPrincipal _id;
public SampleAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
_id = new ClaimsPrincipal(new ClaimsIdentity("Api"));
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
=> Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(_id, "Api")));
}
}

View File

@ -0,0 +1,7 @@
namespace CustomAuthorizationFailureResponse.Authentication
{
public static class SampleAuthenticationSchemes
{
public const string CustomScheme = "CustomScheme";
}
}

View File

@ -0,0 +1,15 @@
using System.Threading.Tasks;
using CustomAuthorizationFailureResponse.Authorization.Requirements;
using Microsoft.AspNetCore.Authorization;
namespace CustomAuthorizationFailureResponse.Authorization.Handlers
{
public class SampleRequirementHandler : AuthorizationHandler<SampleRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SampleRequirement requirement)
{
// assuming the requirement was not met
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,15 @@
using System.Threading.Tasks;
using CustomAuthorizationFailureResponse.Authorization.Requirements;
using Microsoft.AspNetCore.Authorization;
namespace CustomAuthorizationFailureResponse.Authorization.Handlers
{
public class SampleWithCustomMessageRequirementHandler : AuthorizationHandler<SampleWithCustomMessageRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SampleWithCustomMessageRequirement requirement)
{
// assuming the requirement was not met
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Authorization;
namespace CustomAuthorizationFailureResponse.Authorization.Requirements
{
public class SampleRequirement : IAuthorizationRequirement
{
}
}

View File

@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Authorization;
namespace CustomAuthorizationFailureResponse.Authorization.Requirements
{
public class SampleWithCustomMessageRequirement : IAuthorizationRequirement
{
}
}

View File

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

View File

@ -0,0 +1,8 @@
namespace CustomAuthorizationFailureResponse.Authorization
{
public static class SamplePolicyNames
{
public const string CustomPolicy = "Custom";
public const string CustomPolicyWithCustomForbiddenMessage = "CustomPolicyWithCustomForbiddenMessage";
}
}

View File

@ -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";
}
}
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<IsTestAssetProject>true</IsTestAssetProject>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore" />
<Reference Include="Microsoft.AspNetCore.Authentication" />
<Reference Include="Microsoft.AspNetCore.Authorization" />
<Reference Include="Microsoft.AspNetCore.Mvc" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,21 @@
using System;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
namespace CustomAuthorizationFailureResponse.Extensions
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection Decorate<TServiceType, TServiceImplementation>(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;
}
}
}

View File

@ -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<Startup>();
});
}
}

View File

@ -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<AuthenticationSchemeOptions, SampleAuthenticationHandler>(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<IAuthorizationHandler, SampleRequirementHandler>();
services.Decorate<IAuthorizationMiddlewareResultHandler, SampleAuthorizationMiddlewareResultHandler>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
}
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@ -0,0 +1,7 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
}
}

View File

@ -14,6 +14,7 @@
<ProjectReference Include="$(RepoRoot)src\Hosting\Server.IntegrationTesting\src\Microsoft.AspNetCore.Server.IntegrationTesting.csproj" />
<ProjectReference Include="..\..\samples\Cookies\Cookies.csproj" />
<ProjectReference Include="..\..\samples\ClaimsTransformation\ClaimsTransformation.csproj" />
<ProjectReference Include="..\..\samples\CustomAuthorizationFailureResponse\CustomAuthorizationFailureResponse.csproj" />
<ProjectReference Include="..\..\samples\CustomPolicyProvider\CustomPolicyProvider.csproj" />
<ProjectReference Include="..\..\samples\DynamicSchemes\DynamicSchemes.csproj" />
<ProjectReference Include="..\..\samples\Identity.ExternalClaims\Identity.ExternalClaims.csproj" />
@ -33,6 +34,7 @@
<ItemGroup>
<_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\**\*.*" />
</ItemGroup>
<Copy
SourceFiles="@(_PublishFiles)"
DestinationFolder="$(PublishDir)" />
<Copy
SourceFiles="@(_claimsWwwrootFiles)"
DestinationFolder="$(PublishDir)\ClaimsTransformation\wwwroot" />
<Copy
SourceFiles="@(_cookiesWwwrootFiles)"
DestinationFolder="$(PublishDir)\Cookies\wwwroot" />
<Copy
SourceFiles="@(_customProviderFiles)"
DestinationFolder="$(PublishDir)\CustomPolicyProvider\\%(RecursiveDir)" />
<Copy
SourceFiles="@(_schemesWwwrootFiles)"
DestinationFolder="$(PublishDir)\DynamicSchemes\wwwroot" />
<Copy
SourceFiles="@(_pathWwwrootFiles)"
DestinationFolder="$(PublishDir)\Identity.ExternalClaims\wwwroot" />
<Copy
SourceFiles="@(_schemesWwwrootFiles)"
DestinationFolder="$(PublishDir)\PathSchemeSelection\wwwroot" />
<Copy
SourceFiles="@(_staticFiles)"
DestinationFolder="$(PublishDir)\StaticFilesAuth\\%(RecursiveDir)" />
<Copy SourceFiles="@(_PublishFiles)" DestinationFolder="$(PublishDir)" />
<Copy SourceFiles="@(_claimsWwwrootFiles)" DestinationFolder="$(PublishDir)\ClaimsTransformation\wwwroot" />
<Copy SourceFiles="@(_cookiesWwwrootFiles)" DestinationFolder="$(PublishDir)\Cookies\wwwroot" />
<Copy SourceFiles="@(_customAuthorizationFailureResponseFiles)" DestinationFolder="$(PublishDir)\CustomAuthorizationFailureResponse\\%(RecursiveDir)" />
<Copy SourceFiles="@(_customProviderFiles)" DestinationFolder="$(PublishDir)\CustomPolicyProvider\\%(RecursiveDir)" />
<Copy SourceFiles="@(_schemesWwwrootFiles)" DestinationFolder="$(PublishDir)\DynamicSchemes\wwwroot" />
<Copy SourceFiles="@(_pathWwwrootFiles)" DestinationFolder="$(PublishDir)\Identity.ExternalClaims\wwwroot" />
<Copy SourceFiles="@(_schemesWwwrootFiles)" DestinationFolder="$(PublishDir)\PathSchemeSelection\wwwroot" />
<Copy SourceFiles="@(_staticFiles)" DestinationFolder="$(PublishDir)\StaticFilesAuth\\%(RecursiveDir)" />
<!-- Drop a dummy sln to specify content root location -->
<WriteLinesToFile
File="$(PublishDir)\contentroot.sln"
Lines="Ignored"
Overwrite="true"
Encoding="Unicode" />
<WriteLinesToFile File="$(PublishDir)\contentroot.sln" Lines="Ignored" Overwrite="true" Encoding="Unicode" />
</Target>
</Project>

View File

@ -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<WebApplicationFactory<CustomAuthorizationFailureResponse.Startup>>
{
private HttpClient Client { get; }
public CustomAuthorizationFailureResponseTests(WebApplicationFactory<CustomAuthorizationFailureResponse.Startup> 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);
}
}
}