I wrote an authorization middleware for API key in ASP.NET Core. My goal is to secure all API endpoints so that they require a valid API key in the 'x-api-key'
header. My problem is, that when I am not sure how to use it right.
I don't know if I am missing something or should I add more code. The token is empty (returns 401 code).
Example of using it:
[HttpGet]
[ApiKeyAuthorization]
[Route("subgroups/{parentId}")]
public IActionResult GetSubgroups(int parentId)
{
// some code...
}
The code behind:
public class ApiKeyMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
public ApiKeyMiddleware(RequestDelegate next, IConfiguration configuration)
{
_next = next;
_configuration = configuration;
}
public async Task Invoke(HttpContext context)
{
var apiKey = context.Request.Headers["x-api-key"].FirstOrDefault();
if (!string.IsNullOrEmpty(apiKey) && _configuration["AllowedApiKeys"]!.Contains(apiKey))
{
await _next(context);
}
else
{
context.Response.StatusCode = 401; // Unauthorized
await context.Response.WriteAsync("Unauthorized");
}
}
}
[AttributeUsage(AttributeTargets.Method)]
public class ApiKeyAuthorizationAttribute : Attribute, IAuthorizationFilter
{
private readonly IConfiguration? _configuration;
public void OnAuthorization(AuthorizationFilterContext context)
{
var apiKey = context.HttpContext.Request.Headers["x-api-key"].FirstOrDefault();
if (string.IsNullOrEmpty(apiKey) || !IsApiKeyValid(apiKey))
{
context.Result = new UnauthorizedResult();
}
}
private bool IsApiKeyValid(string apiKey)
{
var allowedApiKeys = _configuration.GetSection("AllowedApiKeys").Get<string[]>();
return allowedApiKeys.Contains(apiKey);
}
}
Please follow my steps to implement this function.
My Project Structure
ApiKeyMiddleware.cs
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc;
namespace WebApplication5
{
public class ApiKeyMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
public ApiKeyMiddleware(RequestDelegate next, IConfiguration configuration)
{
_next = next;
_configuration = configuration;
}
public async Task Invoke(HttpContext context)
{
var apiKey = context.Request.Headers["x-api-key"].FirstOrDefault();
if (!string.IsNullOrEmpty(apiKey) && _configuration.GetSection("AllowedApiKeys").Get<string[]>()!.Contains(apiKey))
{
await _next(context);
}
else
{
context.Response.StatusCode = 401; // Unauthorized
await context.Response.WriteAsync("Unauthorized");
}
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class ApiKeyMiddlewareExtensions
{
public static IApplicationBuilder UseApiKeyMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ApiKeyMiddleware>();
}
}
}
ApiKeyAuthenticationHandler.cs
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Text.Encodings.Web;
namespace WebApplication5
{
public class ApiKeyAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
public string? ApiKey { get; set; }
}
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationSchemeOptions>
{
private readonly IConfiguration _configuration;
//TODO Change to whatever name you want to use
private const string ApiKeyHeaderName = "x-api-key";
public ApiKeyAuthenticationHandler(
IOptionsMonitor<ApiKeyAuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
IConfiguration configuration)
: base(options, logger, encoder, clock)
{
_configuration = configuration;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.ContainsKey(ApiKeyHeaderName))
{
return Task.FromResult(AuthenticateResult.Fail("Header was not found"));
}
string token = Request.Headers[ApiKeyHeaderName].ToString();
if (string.IsNullOrEmpty(token) || !IsApiKeyValid(token))
{
return Task.FromResult(AuthenticateResult.Fail("Token is invalid"));
}
else {
Claim[] claims = new[] {
new Claim(ClaimTypes.NameIdentifier, "jason p"),
new Claim(ClaimTypes.Email, "jasonp***@gmail.com"),
};
ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, nameof(ApiKeyAuthenticationHandler));
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}
private bool IsApiKeyValid(string apiKey)
{
var allowedApiKeys = _configuration.GetSection("AllowedApiKeys").Get<string[]>();
return allowedApiKeys.Contains(apiKey);
}
}
}
Add AddAuthentication
and use the apikey middleware in Program.cs file.
namespace WebApplication5
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddAuthentication("ApiKey").AddScheme<ApiKeyAuthenticationSchemeOptions, ApiKeyAuthenticationHandler>("ApiKey",opts => opts.ApiKey = builder.Configuration.GetValue<string>("AllowedApiKeys")
);
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseApiKeyMiddleware();
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
}
My appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"AllowedApiKeys": ["aaa","bbb","ccc"]
}
And we can use [Authorize(AuthenticationSchemes = "ApiKey")]
to protect the api controller.
Test Result
Use the wrong key aa
first, then use the correct one aaa
later to check it.