Tokens vs API Keys: When to Use Which
Understanding the differences between JWT tokens and API keys, and when to use each approach.
Overview
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Authentication Flow β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β βββββββββββ βββββββββββββββββββ β
β β β β Authentication β β
β β Users βββββββββΆβ Server β β
β β β β β β
β ββββββ¬βββββ ββββββββββ¬βββββββββ β
β β β β
β β Credentials β Token β
β βΌ βΌ β
β βββββββββββββββββββββββββββββββββββββββ β
β β Client App β β
β ββββββββββββββββ¬βββββββββββββββββββββββ β
β β β
β βββββββββββ΄ββββββββββ β
β β β β
β βΌ βΌ β
β JWT Token API Key β
β (Who is user?) (What app is this?) β
β β β β
β βΌ βΌ β
β βββββββββββ βββββββββββββββββββ β
β βOrders β β Notifications β β
β βAPI β β API β β
β βββββββββββ βββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Comparison Table
| Feature | API Keys | Tokens (JWT) |
|---|---|---|
| Purpose | Application identification | User authentication |
| Lifespan | Long-lived, static | Short-lived, dynamic |
| Permissions | Fixed set | User-specific, variable |
| User Context | No user information | Contains user data |
| Security | Less secure if compromised. Regular rotation helps. | More secure, limited lifespan |
API Keys
Characteristics
- Long-lived: Typically donβt expire
- Static: Same key for all requests
- Application-level: Identifies the application, not the user
- Simple: Easy to implement and use
Use Cases
- Server-to-server communication
- Public APIs with rate limiting
- Third-party integrations
- Background services
- Webhooks
Implementation Example
// API Key Authentication Handler
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
private const string ApiKeyHeaderName = "X-API-Key";
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKeyValue))
{
return AuthenticateResult.Fail("API Key header not found");
}
var apiKey = await _apiKeyService.ValidateAsync(apiKeyValue);
if (apiKey == null)
{
return AuthenticateResult.Fail("Invalid API Key");
}
var claims = new[]
{
new Claim(ClaimTypes.Name, apiKey.ApplicationName),
new Claim("ApplicationId", apiKey.ApplicationId)
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
// API Key Model
public class ApiKey
{
public string Key { get; set; }
public string ApplicationName { get; set; }
public string ApplicationId { get; set; }
public List<string> AllowedScopes { get; set; }
public DateTime? ExpiresAt { get; set; }
public bool IsActive { get; set; }
}
// Usage in Controller
[ApiKeyAuthorize(Scopes = "orders:read")]
[HttpGet("orders")]
public IActionResult GetOrders()
{
// API key has orders:read scope
}
Security Best Practices for API Keys
// 1. Store securely (never in code)
var apiKey = Environment.GetEnvironmentVariable("EXTERNAL_API_KEY");
// 2. Use different keys per environment
public class ApiKeyConfiguration
{
public string Development { get; set; }
public string Staging { get; set; }
public string Production { get; set; }
}
// 3. Implement key rotation
public class ApiKeyRotationService
{
public async Task RotateKeyAsync(string applicationId)
{
var newKey = GenerateSecureKey();
var oldKey = await GetCurrentKey(applicationId);
// Grace period - both keys valid
await ActivateNewKey(applicationId, newKey);
await ScheduleOldKeyDeactivation(oldKey, TimeSpan.FromHours(24));
}
private string GenerateSecureKey()
{
var bytes = new byte[32];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(bytes);
return Convert.ToBase64String(bytes);
}
}
// 4. Rate limit by API key
services.AddRateLimiter(options =>
{
options.AddPolicy("ApiKeyPolicy", context =>
{
var apiKey = context.Request.Headers["X-API-Key"].ToString();
return RateLimitPartition.GetFixedWindowLimiter(
apiKey,
_ => new FixedWindowRateLimiterOptions
{
PermitLimit = 1000,
Window = TimeSpan.FromHours(1)
});
});
});
JWT Tokens
Characteristics
- Short-lived: Typically 15-60 minutes
- Dynamic: Generated per authentication
- User-level: Contains user identity and claims
- Self-contained: All info in the token (stateless)
Use Cases
- User authentication
- Single Sign-On (SSO)
- Mobile applications
- SPAs (Single Page Applications)
- Microservices communication
Implementation Example
// JWT Token Service
public class JwtTokenService
{
private readonly JwtSettings _settings;
public string GenerateAccessToken(User user)
{
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim("role", user.Role),
new Claim("permissions", string.Join(",", user.Permissions))
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_settings.SecretKey));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _settings.Issuer,
audience: _settings.Audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(15), // Short-lived!
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
public string GenerateRefreshToken()
{
var randomBytes = new byte[64];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomBytes);
return Convert.ToBase64String(randomBytes);
}
}
// Token Response
public class AuthResponse
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public int ExpiresIn { get; set; } // seconds
public string TokenType { get; set; } = "Bearer";
}
// Refresh Token Flow
[HttpPost("refresh")]
public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenRequest request)
{
var principal = GetPrincipalFromExpiredToken(request.AccessToken);
var userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var storedRefreshToken = await _tokenStore.GetRefreshTokenAsync(userId);
if (storedRefreshToken != request.RefreshToken || storedRefreshToken.IsExpired)
{
return Unauthorized();
}
var newAccessToken = _tokenService.GenerateAccessToken(user);
var newRefreshToken = _tokenService.GenerateRefreshToken();
await _tokenStore.UpdateRefreshTokenAsync(userId, newRefreshToken);
return Ok(new AuthResponse
{
AccessToken = newAccessToken,
RefreshToken = newRefreshToken,
ExpiresIn = 900 // 15 minutes
});
}
JWT Structure
Header.Payload.Signature
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Header (Algorithm & Token Type) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β { β
β "alg": "HS256", β
β "typ": "JWT" β
β } β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Payload (Claims) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β { β
β "sub": "user123", β
β "email": "user@example.com", β
β "role": "admin", β
β "iat": 1704067200, β
β "exp": 1704068100 β
β } β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Signature β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β HMACSHA256( β
β base64UrlEncode(header) + "." + base64UrlEncode(payload), β
β secret β
β ) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Decision Guide
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β When to Use What? β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Use API Keys when: β
β ββ Server-to-server communication β
β ββ No user context needed β
β ββ Simple rate limiting by application β
β ββ Third-party service integration β
β ββ Webhook authentication β
β β
β Use JWT Tokens when: β
β ββ User authentication required β
β ββ Need user-specific permissions β
β ββ Mobile or SPA applications β
β ββ Stateless authentication needed β
β ββ SSO across multiple services β
β β
β Use BOTH when: β
β ββ API Key identifies the application β
β ββ JWT identifies the user within that application β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Combined Approach Example
// Require both API Key (app) and JWT (user)
[ApiKeyAuthorize]
[Authorize]
[HttpPost("orders")]
public async Task<IActionResult> CreateOrder([FromBody] OrderRequest request)
{
// API Key verified the application
var applicationId = User.FindFirst("ApplicationId")?.Value;
// JWT verified the user
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
_logger.LogInformation(
"Order created by user {UserId} via application {AppId}",
userId, applicationId);
// Process order...
}
Sources
Arhitectura/tokens vs api keys.jpeg