Asp.Net Core Security Api Key Created: 24 Jan 2026 Updated: 24 Jan 2026

Securing ASP.NET Core Minimal APIs with Custom API Key Authentication

In modern microservices architectures, not every request is initiated by a human user. Often, services need to communicate with each other securely without the overhead of an Identity Provider or complex OAuth2 flows. This is where API Key Authentication shines.

In this article, we will build a robust, attribute-based API Key authentication system using Middleware, Custom Attributes, and Minimal API Extensions.

The Architecture

Our security layer consists of three main components:

  1. The Middleware: The engine that intercepts requests and validates headers.
  2. The Attribute: A marker used to identify which endpoints require protection.
  3. The Extension: A developer-friendly way to apply security to Minimal API groups.

Step 1: The Marker Attribute

First, we define a simple attribute. We will use this to decorate our endpoints or groups.

namespace SecurityApp.API.Middleware;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class RequireApiKeyAttribute : Attribute
{
}

Step 2: The Core Logic (Authentication Middleware)

The ApiKeyAuthenticationMiddleware is responsible for checking if the endpoint metadata contains our RequireApiKeyAttribute. If it does, it validates the request header against a list of valid keys stored in the configuration.

public class ApiKeyAuthenticationMiddleware(RequestDelegate next, IConfiguration configuration)
{
private const string DefaultHeaderName = "X-API-Key";

public async Task InvokeAsync(HttpContext context)
{
// 1. Check if the metadata of the current endpoint requires an API Key
var endpoint = context.GetEndpoint();
var requiresApiKey = endpoint?.Metadata.GetMetadata<RequireApiKeyAttribute>() != null;

if (!requiresApiKey)
{
await next(context);
return;
}

// 2. Extract the Header Name from configuration or use the default
var headerName = configuration["ApiKey:HeaderName"] ?? DefaultHeaderName;

// 3. Check if the header exists
if (!context.Request.Headers.TryGetValue(headerName, out var extractedApiKey))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsJsonAsync(new { error = "API Key is missing" });
return;
}

// 4. Validate the extracted key against the allowed keys list
var validApiKeys = configuration.GetSection("ApiKey:ValidApiKeys").Get<string[]>() ?? [];

if (!validApiKeys.Contains(extractedApiKey.ToString()))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsJsonAsync(new { error = "Invalid API Key" });
return;
}

// 5. Authorized! Proceed to the next middleware
await next(context);
}
}

Step 3: Fluent Extensions for Minimal APIs

To make the API more readable and maintainable, we create an extension method for IEndpointConventionBuilder. This allows us to use .RequireApiKey() syntax.

using SecurityApp.API.Middleware;

namespace SecurityApp.API.Extensions;

public static class ApiKeyExtensions
{
/// <summary>
/// Protects a Minimal API endpoint or group with an API Key
/// </summary>
public static TBuilder RequireApiKey<TBuilder>(this TBuilder builder)
where TBuilder : IEndpointConventionBuilder
{
return builder.WithMetadata(new RequireApiKeyAttribute());
}
}

Step 4: Implementation in Minimal APIs

Now, let's see how to apply this security in a real-world scenario with public, protected, and admin-level endpoints.

using SecurityApp.API.Extensions;
using SecurityApp.API.Middleware;

namespace SecurityApp.API.Endpoints;

public static class MinimalApiExamples
{
public static void MapProductEndpoints(this WebApplication app)
{
var products = app.MapGroup("/api/minimal-products")
.WithTags("Minimal API Products");

// Public endpoint - No API Key required
products.MapGet("/public", () => Results.Ok(new
{
message = "Public Endpoint - Access Granted",
products = new[] { new { id = 1, name = "Standard Item" } }
}));

// Protected endpoint - Using Attribute syntax
products.MapGet("/protected", [RequireApiKey]() => Results.Ok(new
{
message = "Secure Endpoint - Valid API Key Provided"
}));

// Protected POST endpoint
products.MapPost("/protected", [RequireApiKey](CreateProductDto dto) =>
Results.Ok(new { message = "Product Created", name = dto.Name }));

// Group-level protection
var protectedGroup = app.MapGroup("/api/minimal-secure")
.RequireApiKey() // Entire group is now protected
.WithTags("Protected Minimal API");

protectedGroup.MapGet("/users", () => Results.Ok(new { users = "Sensitive Data" }));
}
}

public record CreateProductDto(string Name, decimal Price);

Step 5: Configuration (appsettings.json)

Finally, define your security settings in your configuration file.

{
"ApiKey": {
"HeaderName": "X-Custom-Auth-Key",
"ValidApiKeys": [
"7e97476e-5883-4a1d-8f92-5649c2980661",
"e23f9b23-0182-429f-8d2a-712837192837"
]
}
}

Summary Table: Security Layers

FeatureMethodBenefit
GranularityEndpoint AttributeControl security on a per-method basis.
ScalabilityGroup ExtensionSecure dozens of endpoints with one line.
FlexibilityConfiguration-basedChange keys or header names without recompiling.
StandardsMiddlewareCentralized logic following the ASP.NET Core pipeline.

Important Security Note

In a production environment, never store raw API Keys in appsettings.json. Use Environment Variables, Azure Key Vault, or AWS Secrets Manager to store your valid keys securely.

Share this lesson: