Adding User Claims via API keys in WebApi 2
Adding a Custom Authentication Filter
Update
Please for the love of all that is holy, don't do this. There are many great solutions out there now. I recommend Azure AD (cheap), Auth0(freeish), or Identity Server(open sou). This was a pretty terrible implementation that was meant as a stopgap.
Scenario
I have an API that I wish to lock down via an API key the user will embed in the request header. We will accomplish this using the WebApi.AuthenticationFilter NuGet package to create a custom AuthenticationFilterAttribute which will check the incoming request header for an "api-key" key and related value.
Project Technologies
We are assuming an ASP.NET 4.6.1 Web Application using the Web API Template and the Swashbuckle (Swagger for WebApi) NuGet package installed.
Add WebApi.AuthenticationFilter Package
PM> Install-Package WebApi.AuthenticationFilter
This package describes itself as The missing AuthenticationFilterAttribute from Web API 2. The documentation covers the basics of setup which I will also cover for our purposes.
Building the Filter
Async implementations are possible but unnecessary for this example, so we'll implement the synchronous method. The class will inherit from AuthenticationFilterAttribute, provided by the NuGet package we downloaded.
In this example, we are hard-coding an API key to Identity mapping for brevity, but this could just as easily be retrieved from a database somewhere.
It's worth noting that I'll be returning an Unauthorized Http status code upon failure to authenticate the API key. Depending on your approach, you may wish to utilize security-through-obscurity and return a NotFound result instead.
public class ApiKeyAuthenticationFilter : AuthenticationFilterAttribute
{
public override void OnAuthentication(HttpAuthenticationContext context)
{
if (!Authenticate(context))
{
context.ErrorResult = new StatusCodeResult(HttpStatusCode.Unauthorized,
context.Request);
}
}
private bool Authenticate(HttpAuthenticationContext context)
{
// Get the value for the "api-key" header key
// TODO: replace hard coded literal with AppSetting
var apikey = context.Request?
.Headers?
.SingleOrDefault(x => x.Key == "api-key")
.Value?
.FirstOrDefault();
// TODO: replace hard coded literal with AppSetting or Database check
if (apikey.IsNullOrWhiteSpace() || apikey != "password") return false;
// Authentication logic here (ideally, assign apikeys in a database somewhere)
var username = "someauthuser";
// Create the claim for the username
var usernameClaim = new Claim(ClaimTypes.Name, username);
// Create the claim for the user role
var roleClaim = new Claim(ClaimTypes.Role, "AuthorizedApiKeys");
// TODO: add additional claims such as email / dob / etc
// Build the identity
var identity = new ClaimsIdentity(new[] { usernameClaim, roleClaim }, "ApiKey");
// Assign/Build the pricipal
context.Principal = new ClaimsPrincipal(identity);
// User is authenticated
return true;
}
}
Adding the filter for API calls
Next, we'll add the following line to the WebApiConfig, which is typically found in the
App_Start directory. This will instruct the system to run all WebApi Calls through the filter we just created.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// boilerplate configurations omitted for brevity
config.Filters.Add(new ApiKeyAuthenticationFilter());
}
}
Conclusion
With these changes, every request passing through the API will require the key/value pair for "api-key" that we've specified in the custom filter. With this approach we can now tie any information we want to the user identity for future filtering by controller or method. For example, a specific api key could restrict api access to read-only or only access certain controllers or methods.
Software Development Nerd