Custom Authentication in ASP.NET MVC Core

ASP.NET Core has really good out-of-the-box support for authorization and authentication via ASP.NET Identity. However, sometimes that’s not enough, and you need to roll your own logic.

In my case, I was writing an API for sending emails, which many different clients would connect to. Each client would be given an “API Key” that would identify and authorize their requests. I could have implemented this using HTTP Basic authentication, or using OAuth tokens, but instead I thought it’d be good to get my hands dirty in the base classes.

So, the scenario is that a client sends a request with a known header value, as in:

$Headers = @{
    Authorization = "abcdefg1234567"
}

$Data = @{
    To = "you@example.com";
    From = "me@example.com";
    Subject = "Hello!";
    Body = "How you doing?"
}

Invoke-WebRequest -Uri "https://myservice.com/emails/send" -Headers $Headers -Data $Data

The actual value of the key and the logic behind it isn’t relevant to this article (I’m using RSA private keys under the hood), but the point is that we have a method in our code to verify the authentication key.

To do this, we need to implement custom authorization middleware. This will be composed of three parts:

  • The middleware class
  • The middleware options
  • The authentication handler

The middleware inherits from Microsoft.AspNetCore.Authentication.AuthenticationMiddleware<TOptions>, where TOptions inherits from Microsoft.AspNetCore.Builder.AuthenticationOptions. It’s responsible for building new instances of our authentication handler, and for injecting any required dependencies into the handler. For the purposes of this example, let’s suppose that I have an interface to validate my API keys, like so:

public interface IApiKeyValidator
{
    Task<bool> ValidateAsync(string apiKey);
}

Let’s assume that I’ve implemented this somehow (maybe using Azure KeyVault), so my challenge now is to hook my custom logic up to MVC, so that its built-in authentication can kick in. The end result is that I can decorate my controllers with:

[Authorize(ActiveAuthenticationSchemes = "apikey")]

You can think of the middleware as the glue that binds the MVC framework with our business logic. We could start by writing:

public class ApiKeyAuthenticationOptions : AuthenticationOptions
{
    public const string DefaultHeaderName = "Authorization";
    public string HeaderName { get; set; } = DefaultHeaderName;
}

public class ApiKeyAuthenticationMiddleware : AuthenticationMiddleware<ApiKeyAuthenticationOptions>
{
    private IApiKeyValidator _validator;
    public ApiKeyAuthenticationMiddleware(
       IApiKeyValidator validator,  // custom dependency
       RequestDelegate next,
       IOptions<ApiKeyAuthenticationOptions> options,
       ILoggerFactory loggerFactory,
       UrlEncoder encoder)
       : base(next, options, loggerFactory, encoder)
    {
        _validator = validator;
    }

    protected override AuthenticationHandler<ApiKeyAuthenticationOptions> CreateHandler()
    {
        return new ApiKeyAuthenticationHandler(_validator);
    }
}

This is all just glue code. The real meat lies in ApiKeyAuthenticationHandler, which can be stubbed out as follows:

public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
    private IApiKeyValidator _validator;
    public ApiKeyAuthenticationHandler(IApiKeyValidator validator)
    {
        _validator = validator;
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        throw new NotImplementedException();
    }
}

As you can probably tell, our logic lives in the HandleAuthenticateAsync method. In here, we can basically do whatever we want – we can inject any dependency that the application knows about through the middleware, and the handler has access to the full request context. We could check the URL, querystring, form values, anything. For simplicity, here’s a really cut-down implementation:

StringValues headerValue;
if (!Context.Headers.TryGetValue(Options.HeaderName, out headerValue))
{
    return AuthenticateResult.Fail("Missing or malformed 'Authorization' header.");
}

var apiKey = headerValue.First();
if (!_validator.ValidateAsync(apiKey))
{
    return AuthenticateResult.Fail("Invalid API key.");
}

// success! Now we just need to create the auth ticket
var identity = new ClaimsIdentity("apikey"); // the name of our auth scheme
// you could add any custom claims here
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), null, "apikey");
return AuthenticateResult.Success(ticket);

I’ve kept this example simple, but at this point you have to power to construct any sort of claims identity that you like, with as much or as little information as you need.

Finally, we just need to tell MVC about this middleware in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    // the implementation of this is left to the reader's imagination
    services.AddTransient<IApiKeyValidator, MyApiKeyValidatorImpl>();
    // etc...
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    app.UseMiddleware<ApiKeyAuthenticationMiddleware>();
    // etc...
}

And that’s it! Custom business logic, dependency injection and MVC all working together beautifully.

Advertisements

8 thoughts on “Custom Authentication in ASP.NET MVC Core”

  1. Hello!
    Thank you for this article.

    I have one issue:
    Then i try to run application, i riceive error:

    System.InvalidOperationException: No authentication handler is configured to authenticate for the scheme: apikey

    1. try adding a constructor to ApiKeyAuthenticationOptions to set the AuthenticationScheme

      public ApiKeyAuthenticationOptions() : base()
      {
      AuthenticationScheme = “apikey”;
      }

    1. Thanks to Keith for useful topic. I’ve made following changes:

      StringValues headerValue;
      if (!Context.Request.Headers.TryGetValue(Options.HeaderName, out headerValue))
      {
      return AuthenticateResult.Fail(“Missing or malformed ‘x-api-key’ header.”);
      }

      // Get API key
      var apiKey = headerValue.First();

      // Get client by API key
      var client = _dbContext.ApiClients
      .Include(c => c.Roles)
      .ThenInclude(r => r.Role)
      .FirstOrDefault(c => c.Key == apiKey);
      if (client == null)
      {
      Logger.LogError(“Invalid API key: ” + headerValue);
      return AuthenticateResult.Fail(“Invalid API key.”);
      }

      // Get role claims
      List claims = null;
      if (client.Roles != null)
      claims = client.Roles.Select(r => new Claim(ClaimTypes.Role, r.Role.Name)).ToList();

      // Create identity
      var identity = new ClaimsIdentity(claims, Options.AuthenticationScheme);

      var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), null, Options.AuthenticationScheme);

      return AuthenticateResult.Success(ticket);

      to make possible usage of built-in syntax:

      [Authorize(Roles = “Terminal”)]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s