- 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.
The realisation is that when you build API endpoints, we need to add some authentication.
And like with controllers, you can add authentication to Minimal API endpoints.
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.
One way to do that is to give them an API key that they send in the request header. For each request, we check to see if the API key that we've stored in the application matches the one in the request header. Only if it matches it will the request be processed.
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.
I created this 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.
The next check is to ensure that the api-key
which is in the request header exactly matches the API key stored in the configuration.
Only then will we authenticate the request:
// 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
.
First, we want to add authorisation and a policy within that. With the policy, we'll set a name and the scheme name of the authentication handler. We set a scheme name under 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.
Afterwards, we add authentication and register the authentication handler with the same scheme name.
// 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.
With this, we have also used the API key authentication handler as the 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.
But you can modify the authentication handler to allow certain endpoints through. In this instance, we've modified the handler so it allows all traffic where the request path starts with either /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.
But just because it's minimal doesn't mean security is missing from it. It works in exactly the same way as if you were setting it up with a controller. Just with a lot less code.
Latest tutorials

