Secure Minimal APIs with API Key Authentication

If you’re building a .NET 8 application and want robust security, you might assume it requires a lot of complex authentication setup. But here’s the good news: you can implement API key authentication with just a few lines of code. With this straightforward approach, you can secure your services against unauthorized access quickly and easily. And that may be sufficient for your use case unless you actually need user authentication on your backend service.

Curious about how simple it can be to add API key authentication in ASP.NET Core? Then let’s take a closer look.

Understanding API Security and the Role of API Keys

In today’s connected world, APIs are everywhere, driving everything from mobile apps to cloud services. But with the explosion of API use, securing endpoints is more critical than ever. An API without proper authentication can easily expose sensitive data and put your system at risk.

API key authentication is a straightforward approach for adding access control to your APIs. Clients provide a unique key to access the API, ensuring that only authorized users can interact with it. While API keys aren’t as feature-rich as OAuth or JWT, they’re an ideal solution for many use cases where simplicity and basic security are priorities.

So let’s dive into how you can add API key authentication to an ASP.NET Core Minimal Api project in .NET 8. By the end, you’ll have a secure API that’s ready to deploy!


Step 1: Setup the ASP.NET Core Project

First, let’s create a new ASP.NET Core Web API project to work with:

dotnet new webapi -n ApiKeyAuthSample

Open the project in your favorite IDE, then you will find the following service endpoint code in your Program.cs file:

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();

internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

This code sets up a basic endpoint for fetching weather data (this is the default code that is created for a Minimal Api project). Run the existing code and validate that you can GET a list of WeatherForecast objects. Next, we’ll secure this endpoint using API key authentication.


Step 2: Adding API Key Authentication Middleware

To authenticate requests with API keys, we’ll create middleware that performs the following checks:

  1. Looks for an API key in the request header.
  2. Validates the API key.
  3. Returns an error if the key is missing or invalid.

Create the Middleware

Start by adding a new folder called Middleware, and within it, create a class named ApiKeyMiddleware:

namespace ApiKeyAuthSample;

public class ApiKeyMiddleware
{
    private readonly RequestDelegate _next;
    private const string ApiKeyHeaderName = "X-API-KEY";

    public ApiKeyMiddleware(RequestDelegate next)
    {
        _next = next;
    }

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

        var configuration = context.RequestServices.GetRequiredService<IConfiguration>();
        var apiKey = configuration.GetValue<string>("ApiKey") ?? string.Empty;

        if (!apiKey.Equals(extractedApiKey))
        {
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            await context.Response.WriteAsync("Unauthorized client.");
            return;
        }

        await _next(context);
    }
}

This middleware checks for an API key in the X-API-KEY header and compares it to the key stored in your configuration. If the key is missing or incorrect, the middleware returns a 401 Unauthorized response.


Step 3: Register the Middleware

Now, add the middleware to the request pipeline in Program.cs:

using ApiKeyAuthSample;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// 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.UseMiddleware<ApiKeyMiddleware>();

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();

app.Run();

With this configuration, all requests will pass through the API key authentication middleware before reaching the Minimal API endpoints.


Step 4: Store the API Key

For simplicity, we’ll store the API key in appsettings.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ApiKey": "my-super-secret-api-key"
}

In a production environment, store API keys in a secure location like Azure Key Vault, AWS Secrets Manager, or environment variables. These API keys can also be created for your known consumers and stored in a SQL database or document DB, looked up at runtime, and validated in your middleware.


Step 5: Testing API Key Authentication

With everything set up, let’s test our API. First, try accessing the endpoint without the API key:

curl -X GET "https://localhost:7045/WeatherForecast"

You should see a 401 Unauthorized response with the message “API Key was not provided.”

Now, try the request again, this time including the API key:

curl -X GET "https://localhost:7045/WeatherForecast" -H "X-API-KEY: my-super-secret-api-key"

If the API key is valid, you’ll receive the weather forecast data in the response.


Step 6: Role-Based API Key Authorization

If you need more granular control, like allowing different levels of access for different API keys, you can expand the middleware to support role-based authorization.

Update the Middleware

Here’s how to update the middleware to include role checks:

namespace ApiKeyAuthSample;

public class ApiKeyWithRoleMiddleware
{
    private readonly RequestDelegate _next;
    private const string ApiKeyHeaderName = "X-API-KEY";

    public ApiKeyWithRoleMiddleware(RequestDelegate next)
    {
        _next = next;
    }

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

        var configuration = context.RequestServices.GetRequiredService<IConfiguration>();
        var apiKeys = configuration.GetSection("ApiKeys").Get<Dictionary<string, string>>();

        if (apiKeys is null || !apiKeys.TryGetValue(extractedApiKey, out var role))
        {
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            await context.Response.WriteAsync("Unauthorized client.");
            return;
        }

        context.Items["UserRole"] = role;
        await _next(context);
    }
}

Update appsettings.json to store multiple keys with roles:

{
  "ApiKeys": {
    "admin-api-key": "Admin",
    "user-api-key": "User"
  }
}

Using the Role in an Endpoint

Finally, you can use the stored user role in your endpoint implementation:

app.MapGet("/role", (HttpContext context) => GetRole(context))
.WithName("GetRole")
.WithOpenApi();

app.Run();

static IResult GetRole(HttpContext context)
{
    var userRole = context.Items["UserRole"] as string;

    if (userRole == "Admin")
    {
        return TypedResults.Ok("Welcome, Admin!");
    }

    if (userRole == "User")
    {
        return TypedResults.Ok("Welcome, User!");
    }

    return TypedResults.Forbid();
}

Now, try the request again, this time including the API key:

curl -X GET "https://localhost:7045/role" -H "X-API-KEY: admin-api-key"

Which will result in an OK response code with the message: “Welcome, Admin!”


Conclusion

In just a few steps, we added API key authentication to our ASP.NET Core WebApi application, securing our endpoints from unauthorized frontend access. We even extended it to support role-based authorization, adding an extra layer of control. API key authentication is a straightforward, lightweight approach to secure your APIs for simpler scenarios. And with .NET 8, setting this up is as easy as ever. Now your Web APIs can be locked down and ready for use!

Leave a comment