One common method of securing APIs is through API key authentication. In this blog post, we'll explore how to implement API key authentication in ASP.NET to ensure that only authorized clients can access your API endpoints and cover both Controllers and Minimal APIs.

Our Back to School Sale is now Live on Dometrain. Keep reading the blog to find out how to save up to 30% on any course.

What is API Key Authentication?

API key authentication is a simple way to secure your APIs by requiring clients to provide a unique key when making requests. The server validates this key before processing the request, ensuring that only clients with valid keys can access the API. There are multiple ways you can achieve this, but let's explain a simple workflow for it:

  1. When a client registers to use your API, you generate a unique API key for them. This key is usually a long, random string that is hard to guess or brute-force.

  2. The API key is securely shared with the client. It's crucial that this key is transmitted over secure channels (like HTTPS) and stored securely on the client side.

  3. Every time the client makes a request to your API, they include the API key in the request headers, query parameters, or body.

  4. The server intercepts the request and extracts the API key. It then validates the key against a store of valid keys (like a database or configuration file).

  5. If the API key is valid, the server processes the request and returns the appropriate response. If not, it returns an error response, typically a 401 Unauthorized status code.

Why Use API Key Authentication?

It is super simple to use. Clients only need to include the API key with their requests, making it straightforward for developers to implement.
Unlike other authentication methods, API keys don't rely on cookies or server-side sessions, which is beneficial for stateless server architectures. Another benefit is it works well across different platforms and programming languages, as it doesn't require specific libraries or protocols AND you can enforce rate limits and quotas based on the API key, ensuring fair usage while you can revoke or rotate keys without affecting other clients.

Examples

First, let's create a new .NET 8 project and work on our WeatherApp template.

Start by adding the following to the appsettings.json:

{
  "Authentication": {
    "ApiKey": "traindome420"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

Create a folder named Authentication to add all the stuff we are going to need, and add a class named AuthConstants:

public static class AuthConstants
{
    public const string ApiKeySectionName = "Authentication:ApiKey";
    public const string ApiKeyHeaderName = "X-Api-Key";
}

In this class what we want to declare our constants that are needed for the retrieval of the key from the appsettings.json and the name of the header we are going to use to parse the api key from any given request.

Sidenote here for the X-Api-Key header. Historically, custom HTTP headers were prefixed with X- to indicate that they are extensions and not part of the official HTTP specification. This allows developers to create custom headers without conflicting with any new standardized header that might be rolled out. For your applications, it makes sense to name this header closer to your use case. If, for example, the application is named Abc, then ABC-Api-Key is totally fine. Okay, enough history; back to the example.

Now we need to create a middleware that we are going to use to parse the key from each request and validate that it is correct. Create a class named ApiKeyAuthMiddleware.

public class ApiKeyAuthMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IConfiguration _configuration;

    public ApiKeyAuthMiddleware(RequestDelegate next, IConfiguration configuration)
    {
        _next = next;
        _configuration = configuration;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        if (!context.Request.Headers.TryGetValue(AuthConstants.ApiKeyHeaderName, out var requestApiKey))
        {
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            await context.Response.WriteAsync("API Key is missing");
            return;
        }

        var apiKey = _configuration.GetValue<string>(AuthConstants.ApiKeySectionName);

        if (!apiKey.Equals(requestApiKey))
        {
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            await context.Response.WriteAsync("Invalid API Key");
            return;
        }

        await _next(context);
    }
}

To use this middleware we need to add it in our program.cs. Careful where you add the middleware because order matters.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseMiddleware<ApiKeyAuthMiddleware>();

app.UseAuthorization();

app.MapControllers();

app.Run();

Now, let's take another approach using filters. Let's create a new class called ApiKeyAuthFilter.

public class ApiKeyAuthFilter : IAuthorizationFilter
{
    private readonly IConfiguration _configuration;

    public ApiKeyAuthFilter(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        if (!context.HttpContext.Request.Headers.TryGetValue(AuthConstants.ApiKeyHeaderName,
            out var requestApiKey))
        {
            context.Result = new UnauthorizedObjectResult("API Key missing");
            return;
        }

        var apiKey = _configuration.GetValue<string>(AuthConstants.ApiKeySectionName);

        if (!apiKey.Equals(requestApiKey))
        {
            context.Result = new UnauthorizedObjectResult("Invalid API Key");
            return;
        }
    }
}

This time, we inherit from IAuthorizationFilter and implement the OnAuthorization method. We no longer await the next RequestDelegate, and we will retrieve the HttpContext from the AuthorizationFilterContext. If you need to run asynchronous operations, inherit from IAsyncAuthorizationFilter instead.

Now, there are two ways we can add our filter into the pipeline. First is by adding it into the filters of the controllers:

builder.Services.AddControllers(x => x.Filters.Add<ApiKeyAuthFilter>());

This way, the filter is added to all of our controllers. If we don't want to add this filter to all of our controllers, what we should do is add the filter to our DI:

builder.Services.AddScoped<ApiKeyAuthFilter>();

Now, it is up to us to apply it to a controller:

[ServiceFilter(typeof(ApiKeyAuthFilter))]
[ApiController]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries =
    [
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    ];

    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecast> Get()
    {
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

And apply this to all the methods inside the controller, OR we can apply this to only a method of a controller:

[ApiController]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries =
    [
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    ];

    [ServiceFilter(typeof(ApiKeyAuthFilter))]
    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecast> Get()
    {
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

Perfect, we added our authentication to traditional controllers, but what about Minimal APIs? If we go with the Middleware approach, the same thing will work with our Minimal APIs. If I want to create a filter like we did with controllers, what I have to do is create a new class called ApiKeyEndpointFilter:

public class ApiKeyEndpointFilter : IEndpointFilter
{
    private readonly IConfiguration _configuration;

    public ApiKeyEndpointFilter(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public async ValueTask<object> InvokeAsync(EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        if (!context.HttpContext.Request.Headers.TryGetValue(AuthConstants.ApiKeyHeaderName,
            out var requestApiKey))
        {
            return TypedResults.Unauthorized();
        }

        var apiKey = _configuration.GetValue<string>(AuthConstants.ApiKeySectionName);

        if (!apiKey.Equals(requestApiKey))
        {
            return TypedResults.Unauthorized();
        }

        return await next(context);
    }
}

Now, to apply this, we need to add the filter to our endpoint:

app.MapGet("weathermini", () =>
{
    return Results.Ok();
}).AddEndpointFilter<ApiKeyEndpointFilter>();

If we want to add this filter to a group, we can also achieve this by doing the following:

var group = app.MapGroup("Weather").AddEndpointFilter<ApiKeyEndpointFilter>();

group.MapGet("weathermini", () =>
{
    return Results.Ok();
}).AddEndpointFilter<ApiKeyEndpointFilter>();

Last but certainly not least, if we want to achieve the same authentication using swagger, I need to do something like this:

builder.Services.AddSwaggerGen(c =>
{
    c.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme
    {
        Description = "The API Key to access the API",
        Type = SecuritySchemeType.ApiKey,
        Name = "x-api-key",
        In = ParameterLocation.Header,
        Scheme = "ApiKeyScheme"
    });

    var scheme = new OpenApiSecurityScheme
    {
        Reference = new OpenApiReference
        {
            Type = ReferenceType.SecurityScheme,
            Id = "ApiKey"
        },
        In = ParameterLocation.Header
    };

    var requirement = new OpenApiSecurityRequirement
    {
        { scheme, new List<string>() }
    };

    c.AddSecurityRequirement(requirement);
});

This will now make the Authorize button appear in Swagger, and by using the correct API key, we can authenticate and call our endpoints.

Conclusion

API Key Authentication is a simple yet effective way to secure your APIs. It works best in scenarios where you need to authenticate applications rather than individual users. While it has its limitations, when implemented correctly and combined with best practices, it provides a solid layer of security for your API endpoints. Are you securing your APIs? Have you used this method in the past? Hope you enjoyed it and as always, keep coding.

Next steps

If you're interested in learning more about Authentication or any other topic, check out my learning platform, Dometrain, where 35,000 students are learning how to excel in their careers as software engineers.

Until the end of September you can use discount code BTS30 to get 30% off any course, BTS15 to get 15% off any (already discounted) bundle and BTS20 to get 20% off your first year of Dometrain Pro.

cover-promo copy.jpg