Security & Authentication

API Authentication Patterns: Bearer Tokens, API Keys, and Session Cookies

Comprehensive comparison of API authentication mechanisms — when to use bearer tokens vs API keys vs session cookies, and how each interacts with HTTP status codes.

Authentication vs Authorization

These terms are often confused. In HTTP, the distinction is reflected in status codes:

  • Authentication — proving who you are. Failure returns 401 Unauthorized (which should really be called "unauthenticated"). The response includes a WWW-Authenticate header that tells the client how to authenticate.
  • Authorization — proving you're allowed to do something. Failure returns 403 Forbidden. You've proven who you are, but you lack permission.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api.example.com"

HTTP/1.1 403 Forbidden
Content-Type: application/json

{"error": "insufficient_scope", "required_scope": "admin"}

Pattern 1: Bearer Tokens

Bearer tokens are credentials that grant access to whoever holds them. The holder ("bearer") doesn't need to prove they generated the token — possession is sufficient. JWTs are the most common bearer token format.

How It Works

Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyXzEyMyJ9...

The client includes the token in every request. The server validates the token (usually by verifying a signature or looking it up in a database) and identifies the user.

Token Validation

Two approaches:

  • Self-contained (JWT): The server verifies the token's signature using a public key. No database lookup needed. Fast, but tokens cannot be revoked until expiry.
  • Reference token (opaque): The server looks up the token in a database or cache. Slower (one DB hit per request), but tokens can be revoked instantly.
# Django: bearer token authentication
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
import jwt

class JWTAuthentication(BaseAuthentication):
    def authenticate(self, request):
        auth_header = request.headers.get('Authorization', '')
        if not auth_header.startswith('Bearer '):
            return None  # No credentials — let other authenticators try
        token = auth_header[7:]
        try:
            payload = jwt.decode(
                token,
                settings.JWT_PUBLIC_KEY,
                algorithms=['RS256'],
            )
            user = User.objects.get(id=payload['sub'])
            return (user, token)
        except (jwt.InvalidTokenError, User.DoesNotExist):
            raise AuthenticationFailed('Invalid token')

Token Refresh Flow

Access tokens should be short-lived (5-60 minutes). A separate long-lived refresh token is used to obtain new access tokens without re-authenticating:

POST /auth/token/refresh
Content-Type: application/json

{"refresh_token": "long-lived-opaque-token"}

HTTP/1.1 200 OK
{"access_token": "new-short-lived-jwt", "expires_in": 3600}

Pattern 2: API Keys

API keys are long random strings issued to developers for programmatic access. They're simpler than OAuth but less granular.

Header vs Query Parameter

# Preferred: Authorization header (not logged by default)
curl -H 'X-API-Key: sk_live_abc123...' https://api.example.com/data

# Alternative: Bearer format
curl -H 'Authorization: ApiKey sk_live_abc123...' https://api.example.com/data

# Avoid: Query parameter (logged in access logs, server logs, referrer headers)
curl 'https://api.example.com/data?api_key=sk_live_abc123...'

Never put API keys in URLs. URLs are logged everywhere — access logs, browser history, CDN logs, referrer headers. A key in a URL is a leaked key.

Key Design Best Practices

import secrets
import hashlib

def create_api_key(user):
    # Generate key with a prefix for easy identification
    key = 'sk_live_' + secrets.token_urlsafe(32)

    # Store only the hash — never the plaintext key
    key_hash = hashlib.sha256(key.encode()).hexdigest()
    ApiKey.objects.create(
        user=user,
        key_hash=key_hash,
        prefix=key[:12],  # For identification without full exposure
    )

    # Return the plaintext key to the user once — never again
    return key

Show the full key only once at creation. Display only the prefix thereafter (e.g., sk_live_abc1...). This means if a key is compromised, you can identify which key it was without the database being a risk.

Key Rotation

Support multiple active keys per user to enable zero-downtime rotation:

  • User creates a new key
  • User updates their application to use the new key
  • User deletes the old key

Send email alerts when a key hasn't been used for 90 days (may be forgotten somewhere risky) and when a key is first used from a new IP range.

Pattern 3: Session Cookies

Session cookies are the traditional web authentication mechanism. A session ID stored in a cookie is looked up in a server-side session store on each request.

The Setup

POST /auth/login
Content-Type: application/json
{"username": "alice", "password": "..."}

HTTP/1.1 200 OK
Set-Cookie: session_id=abc123xyz; HttpOnly; Secure; SameSite=Lax; Path=/

The browser automatically sends the cookie with every subsequent request to the same origin. The server looks up abc123xyz in Redis or the database to find the associated user.

CSRF Protection

Because cookies are sent automatically with cross-origin requests (for some SameSite configurations), session cookies are vulnerable to Cross-Site Request Forgery (CSRF). A malicious page can trigger state-changing requests to your API if the user is logged in.

Defenses:

  • SameSite=Strict — cookie not sent on any cross-origin request (breaks OAuth callbacks)
  • SameSite=Lax (default) — cookie sent on top-level navigations, not on sub-resource loads
  • CSRF tokens — include a secret token in forms, verify it server-side
# Django: CSRF is enabled by default for session-authenticated views
# For API endpoints using session auth, use Django REST Framework's session auth:
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',  # Enforces CSRF
    ]
}

Choosing the Right Pattern

Client TypeRecommended PatternWhy
Server-to-server (microservices)Bearer token (JWT or opaque)No user context needed; token can carry service identity
Web SPABearer token (short-lived JWT) + refresh token in HttpOnly cookieProtects against XSS; refresh token survives page reloads
Mobile appBearer token via OAuth + PKCESystem browser protects credentials; OS keychain stores tokens
Third-party developerAPI keySimple to issue, rotate, and revoke; scoped permissions
Traditional web appSession cookieBrowser handles automatically; server-side revocation is instant
IoT / embedded deviceAPI keyCannot run OAuth flows; key stored in secure storage

For mixed clients (a product used by both browsers and third-party integrations), support both patterns: session cookies for browser users, API keys or OAuth for developers. Keep the authentication layer pluggable so you can add methods without restructuring your permission logic.

Protokol Terkait

Istilah Glosarium Terkait

Lebih lanjut di Security & Authentication