
In this tutorial, we will be looking into how to secure a C# ASP.NET Core API using key authentication. We will be exploring two methods of securing our C# API: a custom attribute and a custom middleware.
By the end of this tutorial, we will have secured an API that cannot be accessed without an API authentication key. We will use Postman to ensure our API key is working as expected.
We will also explore some key considerations of using this type of authentication for APIs. Including how secure API key authentication really is, common use cases of API key authentication and when API keys should not be used.
Jump to section:
Secure a C# web API with an API key via the request header
Secure a C# web API with an API key via a query string parameter
What is an API key?
An API key is a code that gets passed to an API via another application. An API may have restricted some or all of its endpoints to require an API key to prevent abusive or malicious use. An API key typically has a set of priviledges with it that would allow the calling application access to some or all of an API’s endpoints.
An example of this is Google Maps and how it requires an API for every map that is embedded to a website. Without this key, the map will not work:
https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap
What are API keys used for?
API keys are used to identify the application or project making the call to an API, they cannot however be used to identify the user(s) of the project making the call to the API.
Here are further examples of what API keys are used for:
- Preventing anonymous access to your API
- Control the number of calls made to your API by a particular application/project
- Identify usage patterns of your API on an application/project basis
- Filter API logs by application/project
As API keys can be used to identify the project making a call, they can also be used to restrict the usage of an API by various applications to a specific IP range.
How secure are API keys?
Short answer – not very. API keys are not considered secure as they are usually accessible to clients, making them easier to steal. API keys are usually sent to an API via a header or the query string of a URL. Servers often keep logs of requests that are made and headers and urls are very often included in those logs.
Once a key has been obtained by someone, they can often use it indefinitely. Sure, access to an API key can be revoked. But because an API key is used to authorize a project and not a user, how would the API developer know that a key is maliciously being used and thus know to revoke its access?
When NOT to use API keys
Because of the security concerns surrounding API keys, they should not be considered for identifying users. API keys are used to define applications or projects, but not the users that use those applications. The creators of a project are also not identifiyable through API keys.
API keys should also not be used to perform secure authorization. Token based authentication is more appropriate for the authorization of users as tokens can often include user details and priviledges. I will cover token-based authentication in a future blog post.
Secure a C# web API with an API key via the request header
API key via the request header using custom middleware
Here, I’ve created a folder titled Middleware and placed inside it the following ApiKeyMiddleware class:
public class ApiKeyMiddleware { private readonly RequestDelegate _next; private const string _apiKey = "x-api-key"; public ApiKeyMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { if (!context.Request.Headers.TryGetValue(_apiKey, out var extractedApiKey)) { context.Response.StatusCode = 401; await context.Response.WriteAsync("No API key was provided."); return; } var configuration = context.RequestServices.GetRequiredService<IConfiguration>(); var apiKey = configuration.GetValue<string>(_apiKey); if (!apiKey.Equals(extractedApiKey)) { context.Response.StatusCode = 401; await context.Response.WriteAsync("Invalid API key."); return; } await _next(context); } }
The middleware attempts to extract the API key from the request header, based on the key x-api-key; it is a common web convention to use this as the name for an API key. If no key is found, a 401 status code is returned, indicating unauthorized.
If an API key is present, it is compared against a value in appsettings. If they don’t match, 401 unauthorized is returned once again. This means we’ll need to add our API key to appsettings.json:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"X-API-Key": "secure_api_key"
}
Finally, we add our custom middleware to the request pipeline via Program.cs:
using Middleware.Middleware;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
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.UseAuthorization();
app.UseMiddleware<ApiKeyMiddleware>();
app.MapControllers();
app.Run();
There is no need to decorate our API endpoints using this custom middleware. As our middleware is in the request pipeline, this means it will execute on every request made to our API.
In its current implementation, this would secure the entire API, meaning no endpoint would be accessible without an API key. We could modify our middleware to cater for specific requests, however creating a custom attribute would provide much more flexibility.
API key via the request header using custom attribute
As opposed to the custom middleware that secures the entire API, implementation via a custom attribute means that we can decorate our classes and/or methods as we see fit.
We could have one class where all methods require an API key and another class that doesn’t require an API key. We could also use it on a per method basis, meaning that different methods of the same class may or may not require an API key.
Firstly, create a folder titled Attributes and inside it place the following ApiKeyAttribute class:
[AttributeUsage(validOn: AttributeTargets.Class | AttributeTargets.Method)] public class ApiKeyAttribute : System.Attribute, IAsyncActionFilter { private const string _apiKey = "x-api-key"; public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { if (!context.HttpContext.Request.Headers.TryGetValue(_apiKey, out var extractedApiKey)) { context.HttpContext.Response.StatusCode = 401; await context.HttpContext.Response.WriteAsync("No API key was provided."); return; } var configuration = context.HttpContext.RequestServices.GetRequiredService<IConfiguration>(); var apiKey = configuration.GetValue<string>(_apiKey); if (!apiKey.Equals(extractedApiKey)) { context.HttpContext.Response.StatusCode = 401; await context.HttpContext.Response.WriteAsync("Invalid API key."); return; } await next(); } }
Note that we have decorated our class with AttributeUsage to indicate our attribute may be used on both classes or methods.
Apart from this detail, the class works in the same way as our custom middleware created earlier whereby an API key is attempted to be parsed. If no key is present, 401 unauthorized is returned. Otherwise, if the key is present but does not match with that in appsettings, 401 unauthorized is returned.
Now, we can use our custom attribute to decorate whichever endpoints we like. We can apply it to individual methods:
[ApiKey] [HttpGet(Name = "GetWeatherForecast")] public IEnumerable<WeatherForecast> Get() { return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)] }) .ToArray(); }
We can also apply our attribute to an entire class:
[ApiKey] [ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; private readonly ILogger<WeatherForecastController> _logger; public WeatherForecastController(ILogger<WeatherForecastController> logger) { _logger = logger; } [HttpGet(Name = "GetWeatherForecast")] public IEnumerable<WeatherForecast> Get() { return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)] }) .ToArray(); } }
Testing API key in the request header using Postman
We can test both our custom middleware and our custom attribute implementations using Postman. With the API running and no API added to the request header, let’s make a request to the default WeatherforecastController:

As expected, we are returned a 401 unauthorized status code, along with the message that we defined in our class.
Now we’ll add the API key to the request header. In Postman, navigate to the Headers tab and add a new row. The key should match that of our class, in this case x-api-key. We’ll first see what happens when we provide an incorrect key:

The API has recognised our key but we are still unauthorized as the key is invalid. Now we will enter the correct API key:

Now we get a 200 success status code along with the data from the API.
Secure a C# web API with an API key via a query string parameter
API key via a query string parameter using custom middleware
Passing the API key via the query string is done in exactly the same was as passing it through the request header apart from one small change.
We’ll create our Middleware folder again and inside it place the following ApiKeyMiddleware class:
public class ApiKeyMiddleware
{
private readonly RequestDelegate _next;
private const string _apiKey = "x-api-key";
public ApiKeyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
if (!context.Request.Query.TryGetValue(_apiKey, out var extractedApiKey))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("No API key was provided.");
return;
}
var configuration = context.RequestServices.GetRequiredService<IConfiguration>();
var apiKey = configuration.GetValue<string>(_apiKey);
if (!apiKey.Equals(extractedApiKey))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Invalid API key.");
return;
}
await _next(context);
}
}
Note the small change we’ve had to make whereby we try to parse the key from the query string instead of from the request header.
No we just need to add our API key to appsettings and add our middleware to the request pipeline:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"X-API-Key": "secure_api_key"
}
using Middleware.Middleware;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
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.UseAuthorization();
app.UseMiddleware<ApiKeyMiddleware>();
app.MapControllers();
app.Run();
API key via a query string parameter using custom attribute
Just like our middleware, we only need to make one small change to our custom attribute in order to be able to pass an API key via the query string.
Create a folder titled Attributes and inside it place the following ApiKeyAttribute:
[AttributeUsage(validOn: AttributeTargets.Class | AttributeTargets.Method)]
public class ApiKeyAttribute : System.Attribute, IAsyncActionFilter
{
private const string _apiKey = "x-api-key";
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (!context.HttpContext.Request.Query.TryGetValue(_apiKey, out var extractedApiKey))
{
context.HttpContext.Response.StatusCode = 401;
await context.HttpContext.Response.WriteAsync("No API key was provided.");
return;
}
var configuration = context.HttpContext.RequestServices.GetRequiredService<IConfiguration>();
var apiKey = configuration.GetValue<string>(_apiKey);
if (!apiKey.Equals(extractedApiKey))
{
context.HttpContext.Response.StatusCode = 401;
await context.HttpContext.Response.WriteAsync("Invalid API key.");
return;
}
await next();
}
}
Our attribute works in the same way as before whereby we can decorate individual methods or entire classes if we wish.
[ApiKey]
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
[ApiKey]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}
Testing an API key via a query string parameter in Postman
We can test both our custom middleware and our custom attribute implementations using Postman. With the API running and no API added to the query string, let’s make a request to the default WeatherforecastController:

As expected, we are returned a 401 unauthorized status code, along with the message that we defined in our class.
Now we’ll add the API key to the query string. In Postman, adjust the request url to include a query string with an api-key-parameter, matching the key used in our classes. We’ll first see what happens when we provide an incorrect key:

The API has recognised our key but we are still unauthorized as the key is invalid. Now we will enter the correct API key:

Now we get a 200 success status code along with the data from the API.