Remigiusz ZalewskiRemigiusz Zalewski

Api Key Authentication in ASP .NET Core

api-key-authentication-aspnet-core

Introduction

Not every API needs users, tokens, and a login flow. An internal service that another service calls, a webhook receiver, a partner integration - these often just need "prove you are an allowed caller", and an API key in a header does that with almost no ceremony. The client sends a secret string, the server checks it, done.

This video implements API key authentication in ASP.NET Core on .NET 9 three different ways, because where you put the check matters: middleware for the whole app, an MVC authorization filter for controllers, and an endpoint filter for minimal API routes. All three read the header name and expected key from configuration through the Options pattern.

🎬 Watch the full video here:


Where the key and header name live

Both values come from appsettings.json, bound to a typed options class rather than read as loose strings:

public class ApiKeyOptions
{
    public const string AuthenticationApiKey = "Authentication:ApiKey";
    public required string HeaderName { get; set; }
    public required string Key { get; set; }
}
builder.Services.Configure<ApiKeyOptions>(
    builder.Configuration.GetSection(ApiKeyOptions.AuthenticationApiKey));

Every enforcement approach below injects IOptions<ApiKeyOptions>. In production the actual key value belongs in a secret store (user secrets locally, Key Vault or environment variables in the cloud), never committed in appsettings.json.

Approach 1: middleware

Middleware runs for every request in the pipeline. It reads the configured header, compares it to the configured key, and short-circuits with 401 if it does not match:

if (!context.Request.Headers.TryGetValue(_options.HeaderName, out var extractedApiKey)
    || extractedApiKey != _options.Key)
{
    context.Response.StatusCode = StatusCodes.Status401Unauthorized;
    await context.Response.WriteAsync("Api Key is invalid");
    return;
}

await _next(context);

Use middleware when the entire API is key-protected and there are no public endpoints. It is the bluntest instrument - there is no per-route opt-out without adding path checks inside it, which gets ugly fast.

Approach 2: MVC authorization filter

For controllers, an IAuthorizationFilter runs as part of MVC's filter pipeline and can be applied exactly where you want it:

public class ApiKeyAuthFilter : IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        if (/* header missing or wrong */)
            context.Result = new UnauthorizedObjectResult("Api Key is invalid");
    }
}

Because it has a constructor dependency (IOptions<ApiKeyOptions>), register it in DI and apply it with [ServiceFilter(typeof(ApiKeyAuthFilter))] on a controller or action. Now [AllowAnonymous]-style selectivity is trivial: put the attribute on the controllers that need it and leave the rest open.

Approach 3: minimal API endpoint filter

Minimal APIs have their own filter abstraction, IEndpointFilter, added per route or per route group:

app.MapGet("/weatherforecastminimal", () => /* ... */)
   .AddEndpointFilter<ApiKeyEndpointFilter>();

The filter's InvokeAsync checks the header and either writes a 401 or calls next(context) to continue. Apply it to a MapGroup and every route in that group is protected in one line. This is the idiomatic choice for a minimal API codebase.

A real gotcha in the demo

The endpoint filter in the video checks the key, writes the 401 body on failure, but then still calls await next(context). To actually short-circuit, it needs to return immediately after writing the unauthorized response instead of falling through to the next delegate. It is a one-word fix and a good reminder that with filters, not calling next is how you stop the pipeline.

Constant-time comparison

extractedApiKey != _options.Key is a normal string comparison, which can leak timing information to an attacker probing the key byte by byte. For a real deployment, compare with a fixed-time method (CryptographicOperations.FixedTimeEquals over the UTF-8 bytes) so every comparison takes the same time regardless of how many characters match.

When API keys are the wrong tool

  • You need to know which user is acting, not just which application. Use real user auth.
  • Keys are going to end up in browser code. A key in client-side JavaScript is public - anyone can read it.
  • You need granular, revocable, per-scope permissions. That is OAuth's job.

API keys shine for trusted server-to-server calls where the client can keep a secret.

Common pitfalls

  • Committing the key. Treat it like a password: secret store, not source control.
  • No revocation story. A single hardcoded key means a leak forces a redeploy. Real systems store hashed keys in a database so individual keys can be rotated and revoked.
  • Forgetting to short-circuit. Writing a 401 body but still invoking the rest of the pipeline runs the endpoint anyway.
  • Plain == comparison. Use a fixed-time comparison.
  • Mixing approaches. Pick one enforcement point for a given surface; stacking middleware and a filter doing the same check is just confusing.

Key Takeaways

  • API key auth is a good fit for server-to-server APIs where OAuth is overkill and you only need to identify the calling application.
  • Bind the header name and key to a typed ApiKeyOptions via the Options pattern; keep the real value in a secret store.
  • Middleware protects the whole app; an MVC IAuthorizationFilter (via [ServiceFilter]) protects chosen controllers; an IEndpointFilter protects chosen minimal API routes or groups.
  • To short-circuit a filter, write the response and return - do not call next.
  • Compare keys in constant time and design for rotation and revocation from the start.
  • Never ship an API key in client-side code.

Get the Full Source Code

The complete runnable solution - all three enforcement approaches, the options binding, and both controller and minimal API endpoints to test against - is available to Patreon supporters. If you want to try each approach with a real header instead of rebuilding it from the walkthrough above, you can find it on Patreon.

Resources