Skip to content

Commit aa04f8e

Browse files
HaoKpranavkm
authored andcommitted
Add FailureReasons (#35425)
1 parent f920602 commit aa04f8e

16 files changed

+182
-15
lines changed

src/Security/Authorization/Core/src/AuthorizationFailure.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public class AuthorizationFailure
1515
private AuthorizationFailure() { }
1616

1717
/// <summary>
18-
/// Failure was due to <see cref="AuthorizationHandlerContext.Fail"/> being called.
18+
/// Failure was due to <see cref="AuthorizationHandlerContext.Fail()"/> being called.
1919
/// </summary>
2020
public bool FailCalled { get; private set; }
2121

@@ -25,13 +25,29 @@ private AuthorizationFailure() { }
2525
public IEnumerable<IAuthorizationRequirement> FailedRequirements { get; private set; } = Array.Empty<IAuthorizationRequirement>();
2626

2727
/// <summary>
28-
/// Return a failure due to <see cref="AuthorizationHandlerContext.Fail"/> being called.
28+
/// Allows <see cref="IAuthorizationHandler"/> to flow more detailed reasons for why authorization failed.
29+
/// </summary>
30+
public IEnumerable<AuthorizationFailureReason> FailureReasons { get; private set; } = Array.Empty<AuthorizationFailureReason>();
31+
32+
/// <summary>
33+
/// Return a failure due to <see cref="AuthorizationHandlerContext.Fail()"/> being called.
2934
/// </summary>
3035
/// <returns>The failure.</returns>
3136
public static AuthorizationFailure ExplicitFail()
37+
=> new AuthorizationFailure
38+
{
39+
FailCalled = true
40+
};
41+
42+
/// <summary>
43+
/// Return a failure due to <see cref="AuthorizationHandlerContext.Fail(AuthorizationFailureReason)"/> being called.
44+
/// </summary>
45+
/// <returns>The failure.</returns>
46+
public static AuthorizationFailure Failed(IEnumerable<AuthorizationFailureReason> reasons)
3247
=> new AuthorizationFailure
3348
{
3449
FailCalled = true,
50+
FailureReasons = reasons
3551
};
3652

3753
/// <summary>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Authorization
5+
{
6+
/// <summary>
7+
/// Encapsulates a reason why authorization failed.
8+
/// </summary>
9+
public class AuthorizationFailureReason
10+
{
11+
/// <summary>
12+
/// Creates a new failure reason.
13+
/// </summary>
14+
/// <param name="handler">The handler responsible for this failure reason.</param>
15+
/// <param name="message">The message describing the failure.</param>
16+
public AuthorizationFailureReason(IAuthorizationHandler handler, string message)
17+
{
18+
Handler = handler;
19+
Message = message;
20+
}
21+
22+
/// <summary>
23+
/// A message describing the failure reason.
24+
/// </summary>
25+
public string Message { get; }
26+
27+
/// <summary>
28+
/// The <see cref="IAuthorizationHandler"/> responsible for this failure reason.
29+
/// </summary>
30+
public IAuthorizationHandler Handler { get; }
31+
}
32+
}

src/Security/Authorization/Core/src/AuthorizationHandlerContext.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.Authorization
1414
public class AuthorizationHandlerContext
1515
{
1616
private readonly HashSet<IAuthorizationRequirement> _pendingRequirements;
17+
private List<AuthorizationFailureReason>? _failedReasons;
1718
private bool _failCalled;
1819
private bool _succeedCalled;
1920

@@ -59,6 +60,12 @@ public AuthorizationHandlerContext(
5960
/// </summary>
6061
public virtual IEnumerable<IAuthorizationRequirement> PendingRequirements { get { return _pendingRequirements; } }
6162

63+
/// <summary>
64+
/// Gets the reasons why authorization has failed.
65+
/// </summary>
66+
public virtual IEnumerable<AuthorizationFailureReason> FailureReasons
67+
=> (IEnumerable<AuthorizationFailureReason>?)_failedReasons ?? Array.Empty<AuthorizationFailureReason>();
68+
6269
/// <summary>
6370
/// Flag indicating whether the current authorization processing has failed.
6471
/// </summary>
@@ -84,6 +91,25 @@ public virtual void Fail()
8491
_failCalled = true;
8592
}
8693

94+
/// <summary>
95+
/// Called to indicate <see cref="AuthorizationHandlerContext.HasSucceeded"/> will
96+
/// never return true, even if all requirements are met.
97+
/// </summary>
98+
/// <param name="reason">Optional <see cref="AuthorizationFailureReason"/> for why authorization failed.</param>
99+
public virtual void Fail(AuthorizationFailureReason reason)
100+
{
101+
Fail();
102+
if (reason != null)
103+
{
104+
if (_failedReasons == null)
105+
{
106+
_failedReasons = new List<AuthorizationFailureReason>();
107+
}
108+
109+
_failedReasons.Add(reason);
110+
}
111+
}
112+
87113
/// <summary>
88114
/// Called to mark the specified <paramref name="requirement"/> as being
89115
/// successfully evaluated.

src/Security/Authorization/Core/src/DefaultAuthorizationEvaluator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public AuthorizationResult Evaluate(AuthorizationHandlerContext context)
1717
=> context.HasSucceeded
1818
? AuthorizationResult.Success()
1919
: AuthorizationResult.Failed(context.HasFailed
20-
? AuthorizationFailure.ExplicitFail()
20+
? AuthorizationFailure.Failed(context.FailureReasons)
2121
: AuthorizationFailure.Failed(context.PendingRequirements));
2222
}
2323
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
11
#nullable enable
22
*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.AuthorizationResult!>!
3+
Microsoft.AspNetCore.Authorization.AuthorizationFailure.FailureReasons.get -> System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authorization.AuthorizationFailureReason!>!
4+
Microsoft.AspNetCore.Authorization.AuthorizationFailureReason
5+
Microsoft.AspNetCore.Authorization.AuthorizationFailureReason.AuthorizationFailureReason(Microsoft.AspNetCore.Authorization.IAuthorizationHandler! handler, string! message) -> void
6+
Microsoft.AspNetCore.Authorization.AuthorizationFailureReason.Handler.get -> Microsoft.AspNetCore.Authorization.IAuthorizationHandler!
7+
Microsoft.AspNetCore.Authorization.AuthorizationFailureReason.Message.get -> string!
8+
static Microsoft.AspNetCore.Authorization.AuthorizationFailure.Failed(System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authorization.AuthorizationFailureReason!>! reasons) -> Microsoft.AspNetCore.Authorization.AuthorizationFailure!
39
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.AuthorizationResult!>!
10+
virtual Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext.Fail(Microsoft.AspNetCore.Authorization.AuthorizationFailureReason! reason) -> void
11+
virtual Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext.FailureReasons.get -> System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authorization.AuthorizationFailureReason!>!

src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,54 @@ public Task HandleAsync(AuthorizationHandlerContext context)
174174
}
175175
}
176176

177+
private class ReasonableFailHandler : IAuthorizationHandler
178+
{
179+
private string _reason;
180+
181+
public ReasonableFailHandler(string reason) => _reason = reason;
182+
183+
public bool Invoked { get; set; }
184+
185+
public Task HandleAsync(AuthorizationHandlerContext context)
186+
{
187+
Invoked = true;
188+
context.Fail(new AuthorizationFailureReason(this, _reason));
189+
return Task.FromResult(0);
190+
}
191+
}
192+
193+
[Fact]
194+
public async Task CanFailWithReasons()
195+
{
196+
var handler1 = new ReasonableFailHandler("1");
197+
var handler2 = new FailHandler();
198+
var handler3 = new ReasonableFailHandler("3");
199+
var authorizationService = BuildAuthorizationService(services =>
200+
{
201+
services.AddSingleton<IAuthorizationHandler>(handler1);
202+
services.AddSingleton<IAuthorizationHandler>(handler2);
203+
services.AddSingleton<IAuthorizationHandler>(handler3);
204+
services.AddAuthorization(options =>
205+
{
206+
options.AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement()));
207+
});
208+
});
209+
210+
// Act
211+
var allowed = await authorizationService.AuthorizeAsync(new ClaimsPrincipal(), "Custom");
212+
213+
// Assert
214+
Assert.False(allowed.Succeeded);
215+
Assert.NotNull(allowed.Failure);
216+
Assert.Equal(2, allowed.Failure.FailureReasons.Count());
217+
var first = allowed.Failure.FailureReasons.First();
218+
Assert.Equal("1", first.Message);
219+
Assert.Equal(handler1, first.Handler);
220+
var second = allowed.Failure.FailureReasons.Last();
221+
Assert.Equal("3", second.Message);
222+
Assert.Equal(handler3, second.Handler);
223+
}
224+
177225
[Fact]
178226
public async Task Authorize_ShouldFailWhenAllRequirementsNotHandled()
179227
{

src/Security/Authorization/test/DenyAnonymousAuthorizationRequirementTests.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
5-
using System.Collections.Generic;
6-
using System.Text;
74
using Microsoft.AspNetCore.Authorization.Infrastructure;
85
using Xunit;
96

src/Security/Authorization/test/NameAuthorizationRequirementTests.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
5-
using System.Collections.Generic;
6-
using System.Text;
74
using Microsoft.AspNetCore.Authorization.Infrastructure;
85
using Xunit;
96

src/Security/Authorization/test/OperationAuthorizationRequirementTests.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
5-
using System.Collections.Generic;
6-
using System.Text;
74
using Microsoft.AspNetCore.Authorization.Infrastructure;
85
using Xunit;
96

src/Security/Authorization/test/RolesAuthorizationRequirementTests.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
5-
using System.Collections.Generic;
6-
using System.Text;
74
using Microsoft.AspNetCore.Authorization.Infrastructure;
85
using Xunit;
96

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Threading.Tasks;
5+
using CustomAuthorizationFailureResponse.Authorization.Requirements;
6+
using Microsoft.AspNetCore.Authorization;
7+
8+
namespace CustomAuthorizationFailureResponse.Authorization.Handlers
9+
{
10+
public class SampleWithFailureReasonRequirementHandler : AuthorizationHandler<SampleFailReasonRequirement>
11+
{
12+
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SampleFailReasonRequirement requirement)
13+
{
14+
context.Fail(new AuthorizationFailureReason(this, "This is a way to provide more failure reasons."));
15+
return Task.CompletedTask;
16+
}
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Authorization;
5+
6+
namespace CustomAuthorizationFailureResponse.Authorization.Requirements
7+
{
8+
public class SampleFailReasonRequirement : IAuthorizationRequirement
9+
{
10+
}
11+
}

src/Security/samples/CustomAuthorizationFailureResponse/Authorization/SampleAuthorizationMiddlewareResultHandler.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ public async Task HandleAsync(
3131
// if the authorization was forbidden, let's use custom logic to handle that.
3232
if (policyAuthorizationResult.Forbidden && policyAuthorizationResult.AuthorizationFailure != null)
3333
{
34+
if (policyAuthorizationResult.AuthorizationFailure.FailureReasons.Any())
35+
{
36+
await httpContext.Response.WriteAsync(policyAuthorizationResult.AuthorizationFailure.FailureReasons.First().Message);
37+
38+
// return right away as the default implementation would overwrite the status code
39+
return;
40+
}
41+
3442
// as an example, let's return 404 if specific requirement has failed
3543
if (policyAuthorizationResult.AuthorizationFailure.FailedRequirements.Any(requirement => requirement is SampleRequirement))
3644
{

src/Security/samples/CustomAuthorizationFailureResponse/Authorization/SamplePolicyNames.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ public static class SamplePolicyNames
77
{
88
public const string CustomPolicy = "Custom";
99
public const string CustomPolicyWithCustomForbiddenMessage = "CustomPolicyWithCustomForbiddenMessage";
10+
public const string FailureReasonPolicy = "FailureReason";
1011
}
1112
}

src/Security/samples/CustomAuthorizationFailureResponse/Controllers/SampleController.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,12 @@ public string GetWithCustomPolicy()
2424
{
2525
return "Hello world from GetWithCustomPolicy";
2626
}
27+
28+
[HttpGet("failureReason")]
29+
[Authorize(Policy = SamplePolicyNames.FailureReasonPolicy)]
30+
public string FailureReason()
31+
{
32+
return "Hello world from FailureReason";
33+
}
2734
}
2835
}

src/Security/samples/CustomAuthorizationFailureResponse/Startup.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,17 @@ public void ConfigureServices(IServiceCollection services)
3737
{
3838
options.AddPolicy(SamplePolicyNames.CustomPolicy, policy =>
3939
policy.AddRequirements(new SampleRequirement()));
40+
41+
options.AddPolicy(SamplePolicyNames.FailureReasonPolicy, policy =>
42+
policy.AddRequirements(new SampleFailReasonRequirement()));
4043

4144
options.AddPolicy(SamplePolicyNames.CustomPolicyWithCustomForbiddenMessage, policy =>
4245
policy.AddRequirements(new SampleWithCustomMessageRequirement()));
4346
});
4447

4548
services.AddTransient<IAuthorizationHandler, SampleRequirementHandler>();
4649
services.AddTransient<IAuthorizationHandler, SampleWithCustomMessageRequirementHandler>();
50+
services.AddTransient<IAuthorizationHandler, SampleWithFailureReasonRequirementHandler>();
4751
services.AddTransient<IAuthorizationMiddlewareResultHandler, SampleAuthorizationMiddlewareResultHandler>();
4852
}
4953

0 commit comments

Comments
 (0)