Skip to main content
Technology & EngineeringApi Design156 lines

API Authentication

API authentication patterns including OAuth 2.0, JWT, and API keys for securing HTTP APIs

Quick Summary29 lines
You are an expert in API authentication patterns for designing robust APIs.

## Key Points

1. App redirects user to authorization server
2. User authenticates and consents
3. Authorization server redirects back with code
4. App exchanges code for tokens
5. Authorization server returns access + refresh tokens
- Use short-lived access tokens (5-15 minutes) with refresh tokens to limit exposure if a token is compromised.
- Always validate JWT signature, expiration, issuer, and audience claims on every request.
- Store API keys and secrets in a dedicated secrets manager, never in source code or environment files committed to version control.
- Using symmetric signing (HS256) for JWTs in distributed systems where the secret must be shared across services; prefer asymmetric signing (RS256/ES256).
- Treating API keys as user authentication; they identify applications, not individual users, and lack fine-grained permissions on their own.

## Quick Example

```http
GET /v1/data HTTP/1.1
X-API-Key: sk_live_abc123def456
```

```
Header:  { "alg": "RS256", "typ": "JWT" }
Payload: { "sub": "user_42", "scope": "read write", "exp": 1700000000 }
Signature: RS256(header + payload, private_key)
```
skilldb get api-design-skills/API AuthenticationFull skill: 156 lines
Paste into your CLAUDE.md or agent config

API Authentication — API Design

You are an expert in API authentication patterns for designing robust APIs.

Core Philosophy

Overview

Authentication verifies the identity of an API consumer. The right pattern depends on who the consumer is (user, service, third-party app) and the security requirements of the system. Most production APIs combine multiple mechanisms.

Core Concepts

API Keys

Simple bearer tokens suitable for server-to-server communication and metering.

GET /v1/data HTTP/1.1
X-API-Key: sk_live_abc123def456

API keys identify the calling application, not a user. They work well for rate limiting and usage tracking but should never be exposed in client-side code.

JWT (JSON Web Tokens)

Self-contained tokens that encode claims and are cryptographically signed.

Header:  { "alg": "RS256", "typ": "JWT" }
Payload: { "sub": "user_42", "scope": "read write", "exp": 1700000000 }
Signature: RS256(header + payload, private_key)
GET /v1/users/me HTTP/1.1
Authorization: Bearer eyJhbGciOi...

OAuth 2.0

A delegation framework that lets users grant third-party apps limited access without sharing credentials.

Authorization Code Flow (web apps):

1. App redirects user to authorization server
   GET /authorize?response_type=code&client_id=APP&redirect_uri=CALLBACK&scope=read

2. User authenticates and consents

3. Authorization server redirects back with code
   GET /callback?code=AUTH_CODE

4. App exchanges code for tokens
   POST /token
   grant_type=authorization_code&code=AUTH_CODE&client_id=APP&client_secret=SECRET

5. Authorization server returns access + refresh tokens

Client Credentials Flow (service-to-service):

POST /token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_id=SVC&client_secret=SECRET&scope=read

Implementation Patterns

JWT Validation Middleware

import jwt
from functools import wraps

def require_auth(f):
    @wraps(f)
    def decorated(request, *args, **kwargs):
        token = request.headers.get("Authorization", "").removeprefix("Bearer ")
        if not token:
            return {"error": "missing_token"}, 401
        try:
            claims = jwt.decode(token, PUBLIC_KEY, algorithms=["RS256"],
                                audience="https://api.example.com")
        except jwt.ExpiredSignatureError:
            return {"error": "token_expired"}, 401
        except jwt.InvalidTokenError:
            return {"error": "invalid_token"}, 401
        request.user = claims
        return f(request, *args, **kwargs)
    return decorated

Token Refresh

Short-lived access tokens paired with long-lived refresh tokens reduce the blast radius of a leaked token.

POST /token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=REFRESH_TOKEN&client_id=APP

Scope-Based Authorization

Encode permissions as scopes and enforce them per endpoint.

def require_scope(required_scope):
    def decorator(f):
        @wraps(f)
        def wrapper(request, *args, **kwargs):
            scopes = request.user.get("scope", "").split()
            if required_scope not in scopes:
                return {"error": "insufficient_scope"}, 403
            return f(request, *args, **kwargs)
        return wrapper
    return decorator

@require_auth
@require_scope("orders:write")
def create_order(request):
    ...

Best Practices

  • Use short-lived access tokens (5-15 minutes) with refresh tokens to limit exposure if a token is compromised.
  • Always validate JWT signature, expiration, issuer, and audience claims on every request.
  • Store API keys and secrets in a dedicated secrets manager, never in source code or environment files committed to version control.

Common Pitfalls

  • Using symmetric signing (HS256) for JWTs in distributed systems where the secret must be shared across services; prefer asymmetric signing (RS256/ES256).
  • Treating API keys as user authentication; they identify applications, not individual users, and lack fine-grained permissions on their own.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add api-design-skills

Get CLI access →