17. Supporting both JWT & API Key
- Supporting both JWT & API Key
About this chapter
Support multiple authentication methods in a single API by configuring both JWT and API Key schemes, then using policies to control which endpoints accept which methods.
- Multiple authentication schemes: Registering JWT and API Key handlers together
- Authorization policies: Creating policies that accept specific schemes
- Scheme-specific endpoints: Requiring different auth methods on different endpoints
- Policy-based authorization: Using custom policies for flexible access control
- Checking authentication method: Determining which scheme was used at runtime
- Security best practices: Least privilege, defense in depth, rate limiting
Learning outcomes:
- Register and configure multiple authentication schemes
- Create authorization policies for different authentication methods
- Protect endpoints with specific authentication requirements
- Determine which authentication method was used in a handler
- Implement least privilege access control
- Apply defense-in-depth strategies for security
17.1 Supporting Both JWT and API Key
// Program.cs - Register both schemes
builder.Services.AddAuthentication(options =>
{
// Default to JWT for most endpoints
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = builder.Configuration["Auth0:Authority"];
options.Audience = builder.Configuration["Auth0:ApiIdentifier"];
})
.AddScheme<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(
ApiKeyConstants.SchemeName,
options => { }); // Options can be configured here
// Configure authorization policies
builder.Services.AddAuthorization(options =>
{
// Policy requiring API Key authentication
options.AddPolicy("ApiKeyPolicy", policy =>
policy.RequireAuthenticatedUser()
.AddAuthenticationSchemes(ApiKeyConstants.SchemeName));
// Policy requiring JWT authentication
options.AddPolicy("JwtPolicy", policy =>
policy.RequireAuthenticatedUser()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme));
// Policy accepting either authentication method
options.AddPolicy("JwtOrApiKey", policy =>
policy.RequireAuthenticatedUser()
.AddAuthenticationSchemes(
JwtBearerDefaults.AuthenticationScheme,
ApiKeyConstants.SchemeName));
// Role-based policy
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Admin")
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme));
// Claims-based policy
options.AddPolicy("PremiumUser", policy =>
policy.RequireClaim("subscription_level", "premium", "enterprise"));
});
17.2 Policy-Based Authorization
// Different endpoints, different schemes
// Registrations: JWT only (user must authenticate with Auth0)
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[Route("api/[controller]")]
[ApiController]
public class RegistrationsController : ControllerBase
{
// Create API key - requires JWT
[HttpPost]
public async Task<ActionResult> RegisterKey() { }
}
// Commands/Platforms: API Key only (uses generated keys)
[Authorize(AuthenticationSchemes = ApiKeyConstants.SchemeName)]
[Route("api/[controller]")]
[ApiController]
public class CommandsController : ControllerBase
{
[HttpPost]
public async Task<ActionResult> CreateCommand() { }
}
// Flexible: Accept either
[Authorize(Policy = "JwtOrApiKey")]
[Route("api/[controller]")]
[ApiController]
public class ReportsController : ControllerBase
{
[HttpGet]
public async Task<ActionResult> GetReport()
{
// Check which auth method was used
var authMethod = User.FindFirst("auth_method")?.Value;
if (authMethod == "api_key")
{
// Different behavior for API key users
}
else
{
// Different behavior for JWT users
}
}
}
17.3 Different Schemes for Different Endpoints
[Route("api/[controller]")]
[ApiController]
public class CommandsController : ControllerBase
{
// Public endpoint - no auth
[AllowAnonymous]
[HttpGet("public")]
public async Task<ActionResult> GetPublicCommands() { }
// API Key required
[Authorize(AuthenticationSchemes = ApiKeyConstants.SchemeName)]
[HttpGet]
public async Task<ActionResult> GetAllCommands() { }
// API Key required
[Authorize(Policy = "ApiKeyPolicy")]
[HttpPost]
public async Task<ActionResult> CreateCommand() { }
// Admin only (JWT with Admin role)
[Authorize(Policy = "AdminOnly")]
[HttpDelete("admin/purge")]
public async Task<ActionResult> PurgeAllCommands() { }
// Either auth method works
[Authorize(Policy = "JwtOrApiKey")]
[HttpGet("stats")]
public async Task<ActionResult> GetStatistics() { }
}
Testing Different Auth Methods:
### Using API Key
GET {{baseUrl}}/api/commands
x-api-key: {{apiKey}}
### Using JWT
GET {{baseUrl}}/api/commands/stats
Authorization: Bearer {{jwtToken}}
### Neither (should fail)
GET {{baseUrl}}/api/commands
# Expected: 401 Unauthorized
17.4 Security Best Practices
- Principle of Least Privilege:
// ❌ Don't grant broad access
[Authorize] // Any authenticated user can do anything
// ✅ Be specific
[Authorize(Policy = "AdminOnly")]
[Authorize(Roles = "Administrator")]
[Authorize(AuthenticationSchemes = ApiKeyConstants.SchemeName)]
- Defense in Depth:
[Authorize(Policy = "ApiKeyPolicy")]
[HttpDelete("{id}")]
public async Task<ActionResult> DeleteCommand(int id)
{
var command = await _repo.GetCommandByIdAsync(id);
// Additional check: users can only delete their own commands
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (command.CreatedBy != userId)
{
return Forbid(); // 403 - authenticated but not authorized
}
// Proceed with delete
}
- Rate Limiting Authentication Attempts:
// In ApiKeyAuthenticationHandler
private static readonly Dictionary<string, (int attempts, DateTime resetTime)>
_failedAttempts = new();
private const int MaxAttempts = 5;
private static readonly TimeSpan LockoutDuration = TimeSpan.FromMinutes(15);
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var providedKey = Request.Headers[ApiKeyHeaderName].FirstOrDefault();
if (string.IsNullOrEmpty(providedKey))
return AuthenticateResult.NoResult();
var keyIndex = providedKey[..36];
// Check if locked out
if (_failedAttempts.TryGetValue(keyIndex, out var attempts))
{
if (attempts.attempts >= MaxAttempts &&
DateTime.UtcNow < attempts.resetTime)
{
_logger.LogWarning("API key locked out: {KeyIndex}", keyIndex);
return AuthenticateResult.Fail("Too many failed attempts. Try again later.");
}
// Reset if lockout expired
if (DateTime.UtcNow >= attempts.resetTime)
{
_failedAttempts.Remove(keyIndex);
}
}
var userId = await ValidateKeyAsync(providedKey);
if (string.IsNullOrEmpty(userId))
{
// Track failed attempt
if (_failedAttempts.ContainsKey(keyIndex))
{
_failedAttempts[keyIndex] = (
_failedAttempts[keyIndex].attempts + 1,
DateTime.UtcNow.Add(LockoutDuration)
);
}
else
{
_failedAttempts[keyIndex] = (1, DateTime.UtcNow.Add(LockoutDuration));
}
return AuthenticateResult.Fail("Invalid API key");
}
// Success - clear failed attempts
_failedAttempts.Remove(keyIndex);
// Create ticket...
}
- Secure Communication:
// Enforce HTTPS in production
if (!app.Environment.IsDevelopment())
{
app.UseHttpsRedirection();
app.UseHsts(); // HTTP Strict Transport Security
}
- Token Expiration:
// JWT configuration
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5), // Allow small clock differences
};
// Consider short-lived access tokens + refresh tokens
// Access token: 15 minutes
// Refresh token: 30 days
- Audit Logging:
// Track all authentication events
_logger.LogInformation(
"Authentication succeeded. User: {UserId}, Method: {Method}, IP: {IP}",
userId,
authMethod,
httpContext.Connection.RemoteIpAddress);
// Track failed attempts
_logger.LogWarning(
"Authentication failed. KeyIndex: {KeyIndex}, IP: {IP}",
keyIndex,
httpContext.Connection.RemoteIpAddress);