ASP.NET - Custom Authentication Schemes
Custom authentication schemes allow an ASP.NET Core application to define its own way of identifying users or clients. Instead of relying only on built-in methods like cookies or JWT, developers can create rules that match their specific security requirements.
Why Custom Schemes Are Needed
Built-in authentication works well for common cases, but some systems need special logic. Examples include API keys in headers, internal service tokens, legacy systems or proprietary authentication formats. A custom scheme gives full control over how credentials are read and validated.
How Authentication Schemes Work
An authentication scheme is a named configuration that tells ASP.NET Core how to authenticate a request. When a request arrives, the framework invokes the handler linked to the scheme. The handler checks the request, validates credentials and creates a user identity if authentication succeeds.
Authentication Handler Role
The handler is the core of a custom scheme. It reads data from the request, such as headers or query values, and decides whether the request is valid. If validation fails, the handler rejects the request. If it succeeds, it builds a ClaimsPrincipal that represents the authenticated user.
When the Scheme Is Triggered
The scheme runs when authentication middleware is enabled and an endpoint requires authentication. Endpoints can explicitly request a specific scheme or use it as the default for the entire application.
Why Custom Schemes Are Powerful
They keep security logic centralized and reusable. Instead of checking credentials in every endpoint, the scheme handles authentication once and applies it consistently. This improves maintainability and reduces security mistakes.
Example (Simple API Key Authentication Scheme)
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Text.Encodings.Web;
public class ApiKeyHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public ApiKeyHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock) { }
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.TryGetValue("X-API-KEY", out var apiKey))
return Task.FromResult(AuthenticateResult.Fail("Missing API Key"));
if (apiKey != "secret123")
return Task.FromResult(AuthenticateResult.Fail("Invalid API Key"));
var claims = new[] { new Claim(ClaimTypes.Name, "ApiClient") };
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication("ApiKey")
.AddScheme<AuthenticationSchemeOptions, ApiKeyHandler>("ApiKey", null);
var app = builder.Build();
app.UseAuthentication();
app.MapGet("/secure", () => "Authenticated request");
app.Run();
Behavior
-
Client sends
X-API-KEYheader -
Middleware validates the key
-
Request proceeds only if authentication succeeds
This approach is commonly used for internal APIs and service-to-service security.