I'm trying to add authentication to a ASP.NET Core 3.1 web service, that looks for a specific custom request header:
[Route("api/[controller]")]
[ApiController]
public class MyController : ControllerBase
{
[HttpPut]
[Authorize(Policy = "MustSupplyAuthenticationToken")]
public async Task<ActionResult> putMyStuff()
{
// ...
I've configured "MustSupplyAuthenticationToken" to use my AuthenticationTokenRequirement class, and wired IAuthorizationHandler to use my AuthenticationTokenHandler (and wired up HttpContextFactory because I'm going to need it.):
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddHttpContextAccessor();
services.AddAuthorization(options =>
{
options.AddPolicy("MustSupplyAuthenticationToken",
policy => policy.Requirements.Add(new AuthenticationTokenRequirement("MY_SECRET_TOKEN")));
});
services.AddTransient<IAuthorizationHandler, AuthenticationTokenHandler>();
}
My AuthenticationTokenRequirement is simple:
public class AuthenticationTokenRequirement : IAuthorizationRequirement
{
public readonly string authenticationToken;
public AuthenticationTokenRequirement(string authenticationToken)
{
this.authenticationToken = authenticationToken;
}
}
And my AuthenticationTokenHandler isn't much more complicated:
public class AuthenticationTokenHandler : AuthorizationHandler<AuthenticationTokenRequirement>
{
private readonly IHttpContextAccessor httpContextAccessor;
public AuthenticationTokenHandler(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
AuthenticationTokenRequirement requirement)
{
var httpContext = this.httpContextAccessor.HttpContext;
var request = httpContext.Request;
var authenticationToken = request.getRequestHeader("authenticationToken");
if (authenticationToken == null)
context.Fail();
else if (authenticationToken != requirement.authenticationToken)
context.Fail();
else
context.Succeed(requirement);
return Task.CompletedTask;
}
}
And the surprising thing is that it all works. HandleRequirementAsync() is called, and when I call context.Fail() access is denied and when I call context.Succeed() access is allowed.
The only problem is that when I call context.Fail() the response is coming back with HTTP 500 - and I need it to come back with HTTP 401. (And when I get this working, I'm going to need a different policy that returns a 403.)
Am I doing something wrong, and am getting a 500 because of some other error?
Or is a failed authentication policy is supposed return a 500?
What do I need to do to get it to return 401?
FWIW: I'm seeing this in my server logs:
2019-12-27T15:49:04.2221595-06:00 80000045-0001-f900-b63f-84710c7967bb [ERR] An unhandled exception has occurred while executing the request. (48a46595)
System.InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).
at Microsoft.AspNetCore.Authentication.AuthenticationService.ChallengeAsync(HttpContext context, String scheme, AuthenticationProperties properties)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.<Invoke>g__AwaitMatcher|8_0(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task`1 matcherTask)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
This could be why I'm seeing 500.
But only when I call context.Fail(). When I call context.Succeed() I don't.
So why am I getting "No authenticationScheme was specified" when I fail the requirement?