0

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?

Ian Kemp
  • 28,293
  • 19
  • 112
  • 138
Jeff Dege
  • 11,190
  • 22
  • 96
  • 165
  • Have a look at this: https://stackoverflow.com/questions/52008000/how-to-correctly-setup-policy-authorization-for-web-api-in-net-core/52042420. – Kirk Larkin Dec 27 '19 at 22:10

1 Answers1

0

OK, for what I'm trying to do policies are not at all the correct approach.

What I need to do is to configure an authentication scheme.

After pulling out all of the authorization policy stuff above, I added this.

In the AuthorizeAttribute, I specify an authentication scheme:

[HttpPut]
[Authorize(AuthenticationSchemes = "AuthenticationTokenScheme")]
public async Task<ActionResult> putTicketDeliveryModel()
{
    // ...

In ConfigureServices, I add the scheme:

services.AddAuthentication()
    .AddScheme<AuthenticationTokenOptions, AuthenticationTokenHandler>("AuthenticationTokenScheme", _ => { });

And then I implement my AuthenticationTokenOptions and AuthenticationTokenHandler classes:

public class AuthenticationTokenOptions : AuthenticationSchemeOptions
{
}

public class AuthenticationTokenHandler : AuthenticationHandler<AuthenticationTokenOptions>
{
    private readonly string expectedAuthenticationToken;

    public AuthenticationTokenHandler(IOptionsMonitor<AuthenticationTokenOptions> optionsMonitor,
        ILoggerFactory loggerFactory, UrlEncoder urlEncoder, ISystemClock systemClock,
        IConfiguration config)
        : base(optionsMonitor, loggerFactory, urlEncoder, systemClock)
    {
        this.expectedAuthenticationToken = config.GetSection("ExpectedAuthenticationToken").Get<string>();
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var authenticationToken = this.Request.getRequestHeader("authenticationToken");

        if (string.IsNullOrWhiteSpace(authenticationToken))
            return Task.FromResult(AuthenticateResult.NoResult());

        if (this.expectedAuthenticationToken != authenticationToken)
            return Task.FromResult(AuthenticateResult.Fail("Unknown Client"));

        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(Enumerable.Empty<Claim>(), Scheme.Name));
        var authenticationTicket = new AuthenticationTicket(claimsPrincipal, Scheme.Name);

        return Task.FromResult(AuthenticateResult.Success(authenticationTicket));
    }
}

This returns 401, if I pass the wrong token.

Still haven't figured out how to return 403.

Jeff Dege
  • 11,190
  • 22
  • 96
  • 165