As .NET developers, we have all faced this situation: you have built a sleek, high-performing ASP.NET Core Web API, and now it is time to expose it to external services or partners. However, you only want authorized consumers to access your API.
One of the simplest ways to secure a Web API is API Key Authentication.
In this article, I will walk you through five practical patterns for implementing API Key authentication in ASP.NET Core, along with code examples and their pros and cons.
Prerequisites
Before continuing, you should have:
Basic understanding of .NET 6 or later
Familiarity with ASP.NET Core Web API
Knowledge of Middleware and Dependency Injection
Understanding of the Authorize attribute
Basic knowledge of AuthenticationHandler and HttpContext
1. API Key Authentication using Custom Middleware
Steps:
Let's create a middleware that checks for an X-API-Key header and compares it to a key stored in appsettings.json
// appsettings.json
{ "ApiKey": "your-super-secret-api-key-12345" }
public class APIKeyMiddleware
{
private readonly RequestDelegate _next;
private const string API_KEY_HEADER_NAME = "X-API-Key";
public APIKeyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, IConfiguration configuration)
{
// Skip validation for Swagger/development endpoints
if (context.Request.Path.StartsWithSegments("/swagger"))
{
await _next(context);
return;
}
if (!context.Request.Headers.TryGetValue(API_KEY_HEADER_NAME, out var extractedApiKey))
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
await context.Response.WriteAsync("API Key is missing.");
return;
}
var apiKey = configuration["ApiKey"];
if (!string.Equals(apiKey, extractedApiKey))
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
await context.Response.WriteAsync("Unauthorized: Invalid API Key.");
return;
}
// API Key is valid — continue processing
await _next(context);
}
}
Now, Register it in Program.cs
app.UseMiddleware<APIKeyMiddleware>();
Run the application and test the endpoint using below powershell script.
Invoke-WebRequest -Uri "https://localhost:7055/weatherforecast"
-Method Get
-Headers @{ "X-API-Key" = "your-super-secret-api-key-12345" }
See the output in PowerShell
![]()
Pros
Super simple
No extra dependencies
Fast to implement
Cons
2. API Key Authentication using Custom Authentication Handler
We use into ASP.NET Core’s authentication pipeline using a custom AuthenticationHandler. This gives us full access to claims, policies, and the Authorize attribute.
Steps:
Define ApiKeyOptions
public class ApiKeyOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "ApiKey";
public string Scheme => DefaultScheme;
public string AuthenticationType = DefaultScheme;
// Custom property for the header name
public string HeaderName { get; set; } = "X-API-Key";
}
Create the Handler
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyOptions>
{
private const string API_KEY_HEADER_NAME = "X-API-Key";
private const string API_KEY_CONFIG_KEY = "ApiKey"; // Key in appsettings.json
private readonly IConfiguration _configuration;
public ApiKeyAuthenticationHandler(
IOptionsMonitor<ApiKeyOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
IConfiguration configuration)
: base(options, logger, encoder, clock)
{
_configuration = configuration;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
// Skip authentication for OPTIONS (preflight)
if (Context.Request.Method == "OPTIONS")
{
return Task.FromResult(AuthenticateResult.NoResult());
}
if (!Context.Request.Headers.TryGetValue(API_KEY_HEADER_NAME, out var apiKeyHeader))
{
return Task.FromResult(AuthenticateResult.Fail("API Key missing"));
}
var apiKey = _configuration[API_KEY_CONFIG_KEY];
var requestApiKey = apiKeyHeader;
// Prevent timing attacks with constant-time comparison
if (!SecureCompare(apiKey, requestApiKey))
{
return Task.FromResult(AuthenticateResult.Fail("Invalid API Key"));
}
var identity = new ClaimsIdentity(new[] { new Claim("ApiKey", "valid-key") }, Options.Scheme);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Options.Scheme);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
private static bool SecureCompare(string? a, string? b)
{
if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b))
return false;
if (a.Length != b.Length)
return false;
var result = 0;
for (int i = 0; i < a.Length; i++)
{
result |= a[i] ^ b[i];
}
return result == 0;
}
public class ApiKeyAuthorizationRequirement : IAuthorizationRequirement
{
// This is an empty marker for custom policy evaluation
}
}
Register in Program.cs
builder.Services.AddAuthentication(ApiKeyOptions.DefaultScheme)
.AddScheme<ApiKeyOptions, ApiKeyAuthenticationHandler>(ApiKeyOptions.DefaultScheme, options =>
{
// You can configure options here if needed
});
// Optional: Add Authorization with Policy (if using IAuthorizationHandler)
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ApiKeyPolicy", policy =>
{
policy.RequireAuthenticatedUser();
policy.Requirements.Add(new ApiKeyAuthorizationRequirement());
});
});
// Learn more about configuring Swagger/OpenAPI
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
// Add API Key to Swagger
options.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Name = "X-API-Key",
Type = SecuritySchemeType.ApiKey,
Description = "Enter your API key"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "ApiKey"
}
},
Array.Empty<string>()
}
});
});
Add API Key in appsettings.json
"ApiKey": "your-super-secret-api-key-12345"
Run the application and test the API.
Invoke-WebRequest -Uri "https://localhost:7055/weatherforecast"
-Method Get
-Headers @{ "X-API-Key" = "your-super-secret-api-key-12345" }
See the output in PowerShell
![]()
Pros
Cons
3. Using DelegatingHandler for Outbound API Calls
So far, we’ve focused on incoming requests. But what about when your service calls another API?
Say you’re hitting https://api.weatherpartner.com, and they require an API key. Instead of adding headers everywhere, use a DelegatingHandler.
Steps:
Create the ApiKeyDelegatingHandler
public class ApiKeyDelegatingHandler : DelegatingHandler
{
private readonly string _apiKey;
private readonly string _headerName;
public ApiKeyDelegatingHandler(string apiKey, string headerName = "X-API-Key")
{
_apiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey));
_headerName = headerName;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
// Add the API key to the headers
request.Headers.Add(_headerName, _apiKey);
// Log (optional)
// Console.WriteLine($"Adding API Key to request: {request.RequestUri}");
// Send the request onward
return await base.SendAsync(request, cancellationToken);
}
Register HttpClient with the Handler in Program.cs
builder.Services.AddHttpClient<IWeatherService, WeatherService>(client =>
{
client.BaseAddress = new Uri("https://api.weatherpartner.com/");
})
.AddHttpMessageHandler(sp =>
{
var configuration = sp.GetRequiredService<IConfiguration>();
var apiKey = configuration["ExternalWeatherApi:ApiKey"]
?? throw new InvalidOperationException("API Key not configured.");
return new ApiKeyDelegatingHandler(apiKey, "X-API-Key");
});
Add API-Key in appsettings.json
"ExternalWeatherApi": {
"ApiKey": "your-actual-api-key-here-12345"
}
Use that function from any services like this.
public async Task<string> GetWeatherDataAsync()
{
var response = await httpClient.GetAsync("/data");
response.EnsureSuccessStatusCode();
return await httpClient.GetStringAsync("/data");
}
4. Database-Backed API Keys
If you need multiple keys, Key Expiration, Usage tracking, Then it’s better to store keys to the database.
Steps:
Define ApiKey Model
public class ApiKey
{
public int Id { get; set; }
public string Key { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public bool IsActive { get; set; } = true;
}
Add AppDbContext
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<ApiKey> ApiKeys { get; set; }
}
Seed sample key to check with
modelBuilder.Entity<ApiKey>().HasData(new ApiKey
{
Id = 1,
Key = "your-secure-api-key-here-12345", // Generate this securely
Name = "InternalService",
CreatedAt = DateTime.UtcNow,
IsActive = true
});
Add API Key Authentication Handler class
public class ApiKeyAuthHandler : AuthenticationHandler<ApiKeyAuthOptions>
{
private const string API_KEY_HEADER_NAME = "X-API-Key";
private readonly AppDbContext _dbContext; // For DB lookup
private readonly IOptionsMonitor<ApiKeySettings> _config; // For config lookup
public ApiKeyAuthHandler(
IOptionsMonitor<ApiKeyAuthOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
AppDbContext dbContext,
IOptionsMonitor<ApiKeySettings> config)
: base(options, logger, encoder, clock)
{
_dbContext = dbContext;
_config = config;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.TryGetValue(API_KEY_HEADER_NAME, out StringValues apiKeyHeader))
{
return AuthenticateResult.Fail("API Key missing");
}
var apiKey = apiKeyHeader.FirstOrDefault();
if (string.IsNullOrEmpty(apiKey))
{
return AuthenticateResult.Fail("API Key missing");
}
bool isValid = await ValidateKeyFromDatabase(apiKey) || ValidateKeyFromConfig(apiKey);
if (!isValid)
{
return AuthenticateResult.Fail("Invalid API Key");
}
var claims = new[] { new Claim(ClaimTypes.Name, "API Client") };
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
private async Task<bool> ValidateKeyFromDatabase(string key)
{
var apiEntity = await _dbContext.ApiKeys
.FirstOrDefaultAsync(x => x.Key == key && x.IsActive);
return apiEntity != null;
}
private bool ValidateKeyFromConfig(string key)
{
return _config.CurrentValue.ApiKeys.Any(k => k.Key == key);
}
}
// Options class
public class ApiKeyAuthOptions : AuthenticationSchemeOptions { }
Register Authentication in program.cs
builder.Services.AddAuthentication("ApiKey")
.AddScheme<ApiKeyAuthOptions, ApiKeyAuthHandler>("ApiKey", null);
Secure the controller
[Authorize(AuthenticationSchemes = "ApiKey")]
Test the API.
Pros
Full lifecycle management
Support for revocation, rotation, and expiration
Ready for multi-tenant systems
Cons
5. Using Third Party Library
Steps:
Install Nuget Package
dotnet add package Microsoft.AspNetCore.Authentication.ApiKey
Add ApiKey class
public class ApiKey : IApiKey
{
// Required property: Key
public string Key { get; set; } = string.Empty;
// Required property: Name
public string Name { get; set; } = string.Empty;
// Required property: OwnerName
public string OwnerName { get; set; } = string.Empty;
// Required property: Claims
public IReadOnlyCollection<Claim> Claims { get; set; } = Array.Empty<Claim>();
// Optional property: Roles
public IReadOnlyCollection<string> Roles { get; set; } = Array.Empty<string>();
}
public class DatabaseApiKeyProvider : IApiKeyProvider
{
// Simulated list - replace with DB call in production
private readonly List<ApiKey> _apiKeys = new()
{
new ApiKey { Key = "your-secure-api-key-here-12345", Name = "LocalAPIService" },
new ApiKey { Key = "your-secure-api-key-here-123456", Name = "GlobalAPIService" }
};
/// <summary>
/// Validates if the provided API key exists and is valid.
/// </summary>
public async Task<IApiKey?> IsValidAsync(string key)
{
var apiKey = _apiKeys.FirstOrDefault(k => k.Key == key);
return apiKey;
}
/// <summary>
/// Provides additional details about the API key (e.g., metadata).
/// </summary>
public async Task<IApiKey?> ProvideAsync(string key)
{
return await IsValidAsync(key); // Reuse logic
}
}
Add authentication in Program.cs
builder.Services.AddAuthentication(ApiKeyDefaults.AuthenticationScheme)
.AddApiKeyInHeader<DatabaseApiKeyProvider>(
options =>
{
options.Realm = "SUN Microsystems Nepal";
options.KeyName = "X-API-Key"; // Header name
});
builder.Services.AddAuthorization();
Add multiple keys in appsettings.json
"ApiKeySettings": {
"RequireHttps": false,
"AllowedKeys": [
{
"Key": "your-secure-api-key-here-12345",
"Name": "LocalAPIService"
},
{
"Key": "your-secure-api-key-here-123456",
"Name": "GlobalAPIService"
}
]
}
Now, protect the API Controller
[Authorize(AuthenticationSchemes = ApiKeyDefaults.AuthenticationScheme)]
Run the application, test the endpoint via PowerShell
Invoke-RestMethod -Uri "https://localhost:7055/weatherforecast" -Method Get -Headers @{"X-API-Key" = "your-secure-api-key-here-12345"} -ContentType "application/json"
This will return 200 for correct API Key
![]()
Security Consideration while protecting an API with API Keys
Never expose keys in URLs (avoid query strings unless absolutely necessary)
Always use HTTPS
Hash and salt stored keys (never store plain text)
Implement rate limiting and logging per key.
Rotate keys regularly
Avoid using API keys for user-level authentication (user OAuth2/JWT instead)