diff --git a/src/Security/Authorization/Core/src/AuthorizationFailure.cs b/src/Security/Authorization/Core/src/AuthorizationFailure.cs
index 722329b865fb..64a5c8d1093d 100644
--- a/src/Security/Authorization/Core/src/AuthorizationFailure.cs
+++ b/src/Security/Authorization/Core/src/AuthorizationFailure.cs
@@ -15,7 +15,7 @@ public class AuthorizationFailure
private AuthorizationFailure() { }
///
- /// Failure was due to being called.
+ /// Failure was due to being called.
///
public bool FailCalled { get; private set; }
@@ -25,13 +25,29 @@ private AuthorizationFailure() { }
public IEnumerable FailedRequirements { get; private set; } = Array.Empty();
///
- /// Return a failure due to being called.
+ /// Allows to flow more detailed reasons for why authorization failed.
+ ///
+ public IEnumerable Reasons { get; private set; } = Array.Empty();
+
+ ///
+ /// Return a failure due to being called.
///
/// The failure.
public static AuthorizationFailure ExplicitFail()
+ => new AuthorizationFailure
+ {
+ FailCalled = true
+ };
+
+ ///
+ /// Return a failure due to being called.
+ ///
+ /// The failure.
+ public static AuthorizationFailure Failed(IEnumerable reasons)
=> new AuthorizationFailure
{
FailCalled = true,
+ Reasons = reasons
};
///
diff --git a/src/Security/Authorization/Core/src/AuthorizationFailureReason.cs b/src/Security/Authorization/Core/src/AuthorizationFailureReason.cs
new file mode 100644
index 000000000000..b8c30f67d1d9
--- /dev/null
+++ b/src/Security/Authorization/Core/src/AuthorizationFailureReason.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Authorization
+{
+ ///
+ /// Encapsulates a reason why authorization failed.
+ ///
+ public class AuthorizationFailureReason
+ {
+ ///
+ /// Creates a new failure reason.
+ ///
+ /// The handler responsible for this failure reason.
+ /// The message describing the failure.
+ public AuthorizationFailureReason(IAuthorizationHandler handler, string message)
+ {
+ Handler = handler;
+ Message = message;
+ }
+
+ ///
+ /// A message describing the failure reason.
+ ///
+ public string Message { get; set; }
+
+ ///
+ /// The responsible for this failure reason.
+ ///
+ public IAuthorizationHandler Handler { get; set; }
+ }
+}
diff --git a/src/Security/Authorization/Core/src/AuthorizationHandlerContext.cs b/src/Security/Authorization/Core/src/AuthorizationHandlerContext.cs
index 2ad35047c4b3..c89cc5cd2160 100644
--- a/src/Security/Authorization/Core/src/AuthorizationHandlerContext.cs
+++ b/src/Security/Authorization/Core/src/AuthorizationHandlerContext.cs
@@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.Authorization
public class AuthorizationHandlerContext
{
private readonly HashSet _pendingRequirements;
+ private readonly List _failedReasons;
private bool _failCalled;
private bool _succeedCalled;
@@ -37,6 +38,7 @@ public AuthorizationHandlerContext(
User = user;
Resource = resource;
_pendingRequirements = new HashSet(requirements);
+ _failedReasons = new List();
}
///
@@ -59,6 +61,11 @@ public AuthorizationHandlerContext(
///
public virtual IEnumerable PendingRequirements { get { return _pendingRequirements; } }
+ ///
+ /// Gets the reasons why authorization has failed.
+ ///
+ public virtual IEnumerable FailureReasons { get { return _failedReasons; } }
+
///
/// Flag indicating whether the current authorization processing has failed.
///
@@ -84,6 +91,20 @@ public virtual void Fail()
_failCalled = true;
}
+ ///
+ /// Called to indicate will
+ /// never return true, even if all requirements are met.
+ ///
+ /// Optional for why authorization failed.
+ public virtual void Fail(AuthorizationFailureReason reason)
+ {
+ Fail();
+ if (reason != null)
+ {
+ _failedReasons.Add(reason);
+ }
+ }
+
///
/// Called to mark the specified as being
/// successfully evaluated.
diff --git a/src/Security/Authorization/Core/src/DefaultAuthorizationEvaluator.cs b/src/Security/Authorization/Core/src/DefaultAuthorizationEvaluator.cs
index 0055568f6f44..eb612716d282 100644
--- a/src/Security/Authorization/Core/src/DefaultAuthorizationEvaluator.cs
+++ b/src/Security/Authorization/Core/src/DefaultAuthorizationEvaluator.cs
@@ -17,7 +17,7 @@ public AuthorizationResult Evaluate(AuthorizationHandlerContext context)
=> context.HasSucceeded
? AuthorizationResult.Success()
: AuthorizationResult.Failed(context.HasFailed
- ? AuthorizationFailure.ExplicitFail()
+ ? AuthorizationFailure.Failed(context.FailureReasons)
: AuthorizationFailure.Failed(context.PendingRequirements));
}
}
diff --git a/src/Security/Authorization/Core/src/PublicAPI.Unshipped.txt b/src/Security/Authorization/Core/src/PublicAPI.Unshipped.txt
index 613684928ccb..bed63d21e9d2 100644
--- a/src/Security/Authorization/Core/src/PublicAPI.Unshipped.txt
+++ b/src/Security/Authorization/Core/src/PublicAPI.Unshipped.txt
@@ -1,3 +1,13 @@
#nullable enable
*REMOVED*static Microsoft.AspNetCore.Authorization.AuthorizationServiceExtensions.AuthorizeAsync(this Microsoft.AspNetCore.Authorization.IAuthorizationService! service, System.Security.Claims.ClaimsPrincipal! user, object! resource, Microsoft.AspNetCore.Authorization.IAuthorizationRequirement! requirement) -> System.Threading.Tasks.Task!
+Microsoft.AspNetCore.Authorization.AuthorizationFailure.Reasons.get -> System.Collections.Generic.IEnumerable!
+Microsoft.AspNetCore.Authorization.AuthorizationFailureReason
+Microsoft.AspNetCore.Authorization.AuthorizationFailureReason.AuthorizationFailureReason(Microsoft.AspNetCore.Authorization.IAuthorizationHandler! handler, string! message) -> void
+Microsoft.AspNetCore.Authorization.AuthorizationFailureReason.Handler.get -> Microsoft.AspNetCore.Authorization.IAuthorizationHandler!
+Microsoft.AspNetCore.Authorization.AuthorizationFailureReason.Handler.set -> void
+Microsoft.AspNetCore.Authorization.AuthorizationFailureReason.Message.get -> string!
+Microsoft.AspNetCore.Authorization.AuthorizationFailureReason.Message.set -> void
+static Microsoft.AspNetCore.Authorization.AuthorizationFailure.Failed(System.Collections.Generic.IEnumerable! reasons) -> Microsoft.AspNetCore.Authorization.AuthorizationFailure!
static Microsoft.AspNetCore.Authorization.AuthorizationServiceExtensions.AuthorizeAsync(this Microsoft.AspNetCore.Authorization.IAuthorizationService! service, System.Security.Claims.ClaimsPrincipal! user, object? resource, Microsoft.AspNetCore.Authorization.IAuthorizationRequirement! requirement) -> System.Threading.Tasks.Task!
+virtual Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext.Fail(Microsoft.AspNetCore.Authorization.AuthorizationFailureReason! reason) -> void
+virtual Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext.FailureReasons.get -> System.Collections.Generic.IEnumerable!
diff --git a/src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs b/src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs
index 41e42e0c952c..ebd2dbcbd78a 100644
--- a/src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs
+++ b/src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs
@@ -174,6 +174,54 @@ public Task HandleAsync(AuthorizationHandlerContext context)
}
}
+ private class ReasonableFailHandler : IAuthorizationHandler
+ {
+ private string _reason;
+
+ public ReasonableFailHandler(string reason) => _reason = reason;
+
+ public bool Invoked { get; set; }
+
+ public Task HandleAsync(AuthorizationHandlerContext context)
+ {
+ Invoked = true;
+ context.Fail(new AuthorizationFailureReason(this, _reason));
+ return Task.FromResult(0);
+ }
+ }
+
+ [Fact]
+ public async Task CanFailWithReasons()
+ {
+ var handler1 = new ReasonableFailHandler("1");
+ var handler2 = new FailHandler();
+ var handler3 = new ReasonableFailHandler("3");
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddSingleton(handler1);
+ services.AddSingleton(handler2);
+ services.AddSingleton(handler3);
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement()));
+ });
+ });
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(new ClaimsPrincipal(), "Custom");
+
+ // Assert
+ Assert.False(allowed.Succeeded);
+ Assert.NotNull(allowed.Failure);
+ Assert.Equal(2, allowed.Failure.Reasons.Count());
+ var first = allowed.Failure.Reasons.First();
+ Assert.Equal("1", first.Message);
+ Assert.Equal(handler1, first.Handler);
+ var second = allowed.Failure.Reasons.Last();
+ Assert.Equal("3", second.Message);
+ Assert.Equal(handler3, second.Handler);
+ }
+
[Fact]
public async Task Authorize_ShouldFailWhenAllRequirementsNotHandled()
{
diff --git a/src/Security/Authorization/test/DenyAnonymousAuthorizationRequirementTests.cs b/src/Security/Authorization/test/DenyAnonymousAuthorizationRequirementTests.cs
index ed0dac0de827..11824a78a67b 100644
--- a/src/Security/Authorization/test/DenyAnonymousAuthorizationRequirementTests.cs
+++ b/src/Security/Authorization/test/DenyAnonymousAuthorizationRequirementTests.cs
@@ -1,9 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System;
-using System.Collections.Generic;
-using System.Text;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Xunit;
diff --git a/src/Security/Authorization/test/NameAuthorizationRequirementTests.cs b/src/Security/Authorization/test/NameAuthorizationRequirementTests.cs
index cc653d27634d..f8ef42b7d161 100644
--- a/src/Security/Authorization/test/NameAuthorizationRequirementTests.cs
+++ b/src/Security/Authorization/test/NameAuthorizationRequirementTests.cs
@@ -1,9 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System;
-using System.Collections.Generic;
-using System.Text;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Xunit;
diff --git a/src/Security/Authorization/test/OperationAuthorizationRequirementTests.cs b/src/Security/Authorization/test/OperationAuthorizationRequirementTests.cs
index 75b3c7392da9..0c70f5860e9f 100644
--- a/src/Security/Authorization/test/OperationAuthorizationRequirementTests.cs
+++ b/src/Security/Authorization/test/OperationAuthorizationRequirementTests.cs
@@ -1,9 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System;
-using System.Collections.Generic;
-using System.Text;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Xunit;
diff --git a/src/Security/Authorization/test/RolesAuthorizationRequirementTests.cs b/src/Security/Authorization/test/RolesAuthorizationRequirementTests.cs
index b2cab60b3f36..f69212e04704 100644
--- a/src/Security/Authorization/test/RolesAuthorizationRequirementTests.cs
+++ b/src/Security/Authorization/test/RolesAuthorizationRequirementTests.cs
@@ -1,9 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System;
-using System.Collections.Generic;
-using System.Text;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Xunit;
diff --git a/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/Handlers/SampleWithFailureReasonRequirementHandler.cs b/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/Handlers/SampleWithFailureReasonRequirementHandler.cs
new file mode 100644
index 000000000000..29ef971649d4
--- /dev/null
+++ b/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/Handlers/SampleWithFailureReasonRequirementHandler.cs
@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Threading.Tasks;
+using CustomAuthorizationFailureResponse.Authorization.Requirements;
+using Microsoft.AspNetCore.Authorization;
+
+namespace CustomAuthorizationFailureResponse.Authorization.Handlers
+{
+ public class SampleWithFailureReasonRequirementHandler : AuthorizationHandler
+ {
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SampleFailReasonRequirement requirement)
+ {
+ context.Fail(new AuthorizationFailureReason(this, "This is a way to provide more failure reasons."));
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/Requirements/SampleFailReasonRequirement.cs b/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/Requirements/SampleFailReasonRequirement.cs
new file mode 100644
index 000000000000..6926dcb70504
--- /dev/null
+++ b/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/Requirements/SampleFailReasonRequirement.cs
@@ -0,0 +1,11 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Authorization;
+
+namespace CustomAuthorizationFailureResponse.Authorization.Requirements
+{
+ public class SampleFailReasonRequirement : IAuthorizationRequirement
+ {
+ }
+}
diff --git a/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/SampleAuthorizationMiddlewareResultHandler.cs b/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/SampleAuthorizationMiddlewareResultHandler.cs
index 2a128498c20a..be7e8d311f1d 100644
--- a/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/SampleAuthorizationMiddlewareResultHandler.cs
+++ b/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/SampleAuthorizationMiddlewareResultHandler.cs
@@ -31,6 +31,14 @@ public async Task HandleAsync(
// if the authorization was forbidden, let's use custom logic to handle that.
if (policyAuthorizationResult.Forbidden && policyAuthorizationResult.AuthorizationFailure != null)
{
+ if (policyAuthorizationResult.AuthorizationFailure.Reasons.Any())
+ {
+ await httpContext.Response.WriteAsync(policyAuthorizationResult.AuthorizationFailure.Reasons.First().Message);
+
+ // return right away as the default implementation would overwrite the status code
+ return;
+ }
+
// as an example, let's return 404 if specific requirement has failed
if (policyAuthorizationResult.AuthorizationFailure.FailedRequirements.Any(requirement => requirement is SampleRequirement))
{
diff --git a/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/SamplePolicyNames.cs b/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/SamplePolicyNames.cs
index bd09ea18b96e..392313b31ca7 100644
--- a/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/SamplePolicyNames.cs
+++ b/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/SamplePolicyNames.cs
@@ -7,5 +7,6 @@ public static class SamplePolicyNames
{
public const string CustomPolicy = "Custom";
public const string CustomPolicyWithCustomForbiddenMessage = "CustomPolicyWithCustomForbiddenMessage";
+ public const string FailureReasonPolicy = "FailureReason";
}
}
diff --git a/src/Security/samples/CustomAuthorizationFailureResponse/Controllers/SampleController.cs b/src/Security/samples/CustomAuthorizationFailureResponse/Controllers/SampleController.cs
index ed6834b501b9..7f43c30e91ad 100644
--- a/src/Security/samples/CustomAuthorizationFailureResponse/Controllers/SampleController.cs
+++ b/src/Security/samples/CustomAuthorizationFailureResponse/Controllers/SampleController.cs
@@ -24,5 +24,12 @@ public string GetWithCustomPolicy()
{
return "Hello world from GetWithCustomPolicy";
}
+
+ [HttpGet("failureReason")]
+ [Authorize(Policy = SamplePolicyNames.FailureReasonPolicy)]
+ public string FailureReason()
+ {
+ return "Hello world from FailureReason";
+ }
}
}
diff --git a/src/Security/samples/CustomAuthorizationFailureResponse/Startup.cs b/src/Security/samples/CustomAuthorizationFailureResponse/Startup.cs
index f970cde5d43d..aac2e2eab250 100644
--- a/src/Security/samples/CustomAuthorizationFailureResponse/Startup.cs
+++ b/src/Security/samples/CustomAuthorizationFailureResponse/Startup.cs
@@ -37,6 +37,9 @@ public void ConfigureServices(IServiceCollection services)
{
options.AddPolicy(SamplePolicyNames.CustomPolicy, policy =>
policy.AddRequirements(new SampleRequirement()));
+
+ options.AddPolicy(SamplePolicyNames.FailureReasonPolicy, policy =>
+ policy.AddRequirements(new SampleFailReasonRequirement()));
options.AddPolicy(SamplePolicyNames.CustomPolicyWithCustomForbiddenMessage, policy =>
policy.AddRequirements(new SampleWithCustomMessageRequirement()));
@@ -44,6 +47,7 @@ public void ConfigureServices(IServiceCollection services)
services.AddTransient();
services.AddTransient();
+ services.AddTransient();
services.AddTransient();
}