- Home
- .NET tutorials
- Add API key authentication to an Minimal API endpoint
Add API key authentication to an Minimal API endpoint
Published: Monday 8 September 2025
Minimal API has simplicity to its advantage. But that doesn't mean it's insecure.
Protected by an API key
I have an application that receives an API request when someone unsubscribes to a newsletter. I want to make sure that it's only the third-party newsletter provider that I receive API requests from.
Adding options
The first thing we want to do is to add a configuration value to appsettings.json:
{
"Api": {
"Key": "PTsSjOPzSPrsxD0n7AuSHUJynoZSiX"
}
}
Now you could inject the IConfiguration instance and then read the value from that. But I like to store it in the class and register it as an option in Program.cs. It's a bit more code, but I think it makes it more reusable.
ApiOptions class:
// ApiOptions.cs
public class ApiOptions
{
public string Key { get; set; } = string.Empty;
}
The next thing is to ensure this class is registered as an option in Program.cs. You have to bind it to the section in your appsettings or config values:
// Program.cs
builder.Services.AddOptions<ApiOptions>()
.BindConfiguration("Api");
Adding an authentication handler
With the configuration set up, it's time to add an authentication handler. This checks and ensures that the api-key request header exists in the request. If it doesn't, we instanly fail the authentication.
api-key which is in the request header exactly matches the API key stored in the configuration.
// ApiKeyAuthenticationHandler.cs
public class ApiKeyAuthenticationHandler
: AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly IOptionsMonitor<ApiOptions> _apiOptions;
public static readonly string SchemeName = "ApiKey";
public ApiKeyAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
IOptionsMonitor<ApiOptions> apiOptions,
ILoggerFactory loggerFactory,
UrlEncoder urlEncoder
) : base(options, loggerFactory, urlEncoder)
{
_apiOptions = apiOptions;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.TryGetValue("api-key", out var apiKeyHeader))
{
return Task.FromResult(AuthenticateResult.Fail("API key is missing"));
}
if (apiKeyHeader != _apiOptions.CurrentValue.Key)
{
return Task.FromResult(AuthenticateResult.Fail("Invalid API key"));
}
var identity = new ClaimsIdentity(
[new Claim(ClaimTypes.Name, SchemeName)], SchemeName
);
return Task.FromResult(AuthenticateResult.Success(
new AuthenticationTicket(new ClaimsPrincipal(identity), SchemeName)));
}
}
Register the authentication handler
We then want to register the authentication handler and we do that in Program.cs.
SchemeName in ApiKeyAuthenticationHandler, so that's how we use it. We then use the policy name in the Minimal API so we know which authentication handler to use.
// Program.cs
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ApiKeyPolicy", policy =>
{
policy.AddAuthenticationSchemes(ApiKeyAuthenticationHandler.SchemeName);
policy.RequireAuthenticatedUser();
});
});
builder.Services
.AddAuthentication()
.AddScheme<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(
ApiKeyAuthenticationHandler.SchemeName, null);
Using it in a Minimal API endpoint
Then it's a case of using the RequireAuthorization method and providing the policy name in your Minimal API endpoint to protect it.
app.MapGet("/hello-world", () => "Hello world")
.RequireAuthorization("ApiKeyPolicy");
When we run /hello-world now, it will only throw a 200 response if the api-key request header exactly matches PTsSjOPzSPrsxD0n7AuSHUJynoZSiX. If it doesn't, or the api-key request header does not exist, it will throw a 401 Unauthorized response.
How to protect all endpoints by default
If you want to be extra safe and protect all endpoints in your API, you can add a fallback policy.
// Program.cs
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ApiKeyPolicy", policy =>
{
policy.AddAuthenticationSchemes(ApiKeyAuthenticationHandler.SchemeName);
policy.RequireAuthenticatedUser();
});
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(ApiKeyAuthenticationHandler.SchemeName)
.RequireAuthenticatedUser()
.Build();
});
Now when we run this endpoint, we'll only get a 200 response with the correct api-key request header, despite the fact the RequireAuthorization method is not present:
// Program.cs
app.MapGet("/hello-world-no-authorization", () => "Hello world");
The problem with this method
The problem with this method that it protectes all endpoints in your API. This includes the Scalar OpenAPI document that I like to use.
/openapi or /scalar and we are setting the user as an anonymous user:
// ApiKeyAuthenticationHandler.cs
public class ApiKeyAuthenticationHandler
: AuthenticationHandler<AuthenticationSchemeOptions>
{
...
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (Request.Path.Value!.StartsWith("/openapi") ||
Request.Path.Value!.StartsWith("/scalar"))
{
return Task.FromResult(AuthenticateResult.Success(
new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity()), SchemeName)));
}
...
}
}
But we have to modify the fallback policy to ensure that anonymous users are allowed:
// Program.cs
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(ApiKeyAuthenticationHandler.SchemeName)
.RequireAssertion(async context =>
{
var httpContext = context.Resource as HttpContext;
if (httpContext == null)
{
return false;
}
return (await httpContext.AuthenticateAsync()).Succeeded;
})
.Build();
Watch the video
To learn more, watch this video where you'll see how we build the authentication handler, add it to the application and see how it works with a real request.
And when you watch the video, you might want to download the code example to make it easier to follow along and find out how authentication works with an API key.
Final thoughts
Setting up authentication with Minimal API endpoints is the same as a controller. You create a policy and register your authentication scheme.
Related tutorials