🌐

Tokens vs API Keys: When to Use Which

APIs & Integration Intermediate 1 min read 200 words
Security

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

πŸ“š Related Articles