Reputation: 15159
I have a simple ASP.NET Core API (.net 5) application with security configured like this:
services.AddControllers(options =>
options.Filters.Add(new MyExceptionFilter()));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(Configuration.GetSection("AAD"));
services.AddAuthorization(options =>
{
options.AddPolicy("MyPolicy", policy =>
policy.RequireAssertion(context =>
context.User.HasClaim(c => c.Type == "myclaim" && c.Value == "myvalue")));
});
The policy is then applied to a controller:
[ApiController]
[Route("[controller]")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Policy = "MyPolicy")]
public class DefaultController : ControllerBase {}
When a token has no required claim the framework throws System.UnauthorizedAccessException
:
System.UnauthorizedAccessException: IDW10201: Neither scope or roles claim was found in the bearer token.
at Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilderExtensions.<>c__DisplayClass3_1.<<AddMicrosoftIdentityWebApiImplementation>b__1>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.AuthenticateAsync()
at Microsoft.AspNetCore.Authentication.AuthenticationService.AuthenticateAsync(HttpContext context, String scheme)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
Which is OK. What's not OK is that it then translates to HTTP 500 although I expect HTTP 403. The problem is that standard exception filters (inherited from Microsoft.AspNetCore.Mvc.Filters.ExceptionFilterAttribute
) don't catch it, probably because it's happening too early in the pipeline.
Why is it designed this way? How can I map exceptions from the authentication phase to whatever I need?
Upvotes: 1
Views: 1508
Reputation: 1670
Ok... I got that here. Thanks for updated the code, couldn't trace back this deep without it.
The code make use of AddMicrosoftIdentityWebApi
, which belongs to Microsoft.Identity.Web
package. And behind the screen, make use of JwtBearerOptions
and JwtBearerHandler
.
On the config of AddMicrosoftIdentityWebApi
, JwtBearerEvents
was hard code to require scope and role claims, as you can see here.
http://schemas.microsoft.com/identity/claims/scope
and http://schemas.microsoft.com/ws/2008/06/identity/claims/role
was specific for .net world, and scp
and roles
for Oauth standard world. The content of Jwt token input must have at least one of four of these scopes, otherwise, the exception would raise (as JwtBearerEvents
call to validate here, that directly point to here, which was hard-coded as the first link above.)
The point is, the exception would throw on AuthenticationMiddleware
, which was pretty high on our pipeline, catch UnauthorizedAccessException
here to return 403 might swallow any custom logic that make use of UnauthorizedAccessException
we would like to throw from application logic (in my case, I use this kind of exception in my own project).
So the most suitable one would be ensure that the jwt token input have one of four scope as I mention above (it's required by Microsoft.Identity.Web
package itself). Or make use of a middleware placing above AuthenticationMiddleware
, and catch UnauthorizedAccessException
exception only with message contain IDW10201
, then write the response back from 500 to 403. (I still prefer Exception filter below Controller level, as I can separate which exception come from my logic, or from somewhere else).
Upvotes: 2