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.