In a typical ASP.NET Core web service request pipeline, you'll find authentication pretty early on, then some authorization and finally the execution of the desired action.
For most cases, that's great. However, you surely have encountered situations where all you needed was a simple yes/no validation - often for services that don't return stored information but simply perform some action based on your inputs. Integrating that in full-blown approaches like OpenId flows can often be cumbersome. Imagine you'll want to store your build artifacts on some web service you own - passing a single, secret Api key through your CI pipeline is much easier than setting up complicated, multi-step authentication setups.
Generally speaking, when your action does neither retrieve information nor alter existing data, you're safe to use an Api key.
With ASP.NET Core, you'd do that by creating a custom Policy that guards certain actions. These components are essential:
- An IAuthorizationRequirement, which defines the requirement
- Accompanied by an AuthorizationHandler, which checks the requirement
- A bit of plumbing in your app to make them work
Let's walk through the implementation!
First, define an ApiKeyRequirement:
public class ApiKeyRequirement : IAuthorizationRequirement | |
{ | |
public IReadOnlyList<string> ApiKeys { get; set; } | |
public ApiKeyRequirement(IEnumerable<string> apiKeys) | |
{ | |
ApiKeys = apiKeys?.ToList() ?? new List<string>(); | |
} | |
} |
It's an implementation of the IAuthorizationRequirement interface and simply stores the list of all valid Api keys in-memory.
public class ApiKeyRequirementHandler : AuthorizationHandler<ApiKeyRequirement> | |
{ | |
public const string API_KEY_HEADER_NAME = "X-API-KEY"; | |
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ApiKeyRequirement requirement) | |
{ | |
SucceedRequirementIfApiKeyPresentAndValid(context, requirement); | |
return Task.CompletedTask; | |
} | |
private void SucceedRequirementIfApiKeyPresentAndValid(AuthorizationHandlerContext context, ApiKeyRequirement requirement) | |
{ | |
if (context.Resource is AuthorizationFilterContext authorizationFilterContext) | |
{ | |
var apiKey = authorizationFilterContext.HttpContext.Request.Headers[API_KEY_HEADER_NAME].FirstOrDefault(); | |
if (apiKey != null && requirement.ApiKeys.Any(requiredApiKey => apiKey == requiredApiKey)) | |
{ | |
context.Succeed(requirement); | |
} | |
} | |
} | |
} |
The corresponding ApiKeyRequirementHandler is responsible for evaluating incoming requests for their compliance with the requirement, in this case by checking if the Api key is given in the request header.
public void ConfigureServices(IServiceCollection services) | |
{ | |
services.AddTransient<IAuthorizationHandler, ApiKeyRequirementHandler>(); | |
services.AddAuthorization(authConfig => | |
{ | |
authConfig.AddPolicy("ApiKeyPolicy", | |
policyBuilder => policyBuilder | |
.AddRequirements(new ApiKeyRequirement(new[] { "my-secret-key" }))); | |
}); | |
} |
This has to be wired together in your Startup class. First, you have to add a custom authorization policy with a unique name - ApiKeyPolicy in this case. An instance of the ApiKeyRequirement is added to it. Secondly, you have to register the ApiKeyRequirementHandler in your service collection.
When everything is correctly set up and configured, you can now reference this authorization policy in your controllers:
public class MyController : Controller | |
{ | |
[Authorize(Policy = "ApiKeyPolicy")] | |
public async Task<IActionResult> DoSensitiveOperation() | |
{ | |
// This action can only be called if the request has a correct | |
// api key attached | |
} | |
} |
Please consider this post simply as an example! For production use, you'd want to implement at least storing the Api keys hashed in your database, with the option to revoke them and with some connection between users and Api keys
Happy Authenticating!
PS: If you're looking for an approach to implement custom authentication, you can take a look at how that is implemented with using Http Basic Authentication as example.
Edit, 30.01.2020: It's always great to get feedback, especially when a blog post was able to help somebody! Valentin from randommer.io used this example to quickly secure their API with keys.