17. Supporting both JWT & API Key

  1. 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

  1. 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)]
  1. 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
}
  1. 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...
}
  1. Secure Communication:
// Enforce HTTPS in production
if (!app.Environment.IsDevelopment())
{
    app.UseHttpsRedirection();
    app.UseHsts();  // HTTP Strict Transport Security
}
  1. 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
  1. 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);