DEV Community

Alex Spinov
Alex Spinov

Posted on

API Authentication in 2026: JWT vs OAuth2 vs API Keys (With Python Examples)

Every API you build needs authentication. But which method should you use?

I've implemented all three in production systems. Here's when to use each — with working Python code.


Quick Decision Tree

Is it a public API (no user data)?  → API Key
Does it need user-specific data?    → OAuth2
Is it internal/microservices?       → JWT
Is it server-to-server?             → API Key or JWT
Enter fullscreen mode Exit fullscreen mode

1. API Keys — The Simple Choice

Use when: Public APIs, rate limiting, usage tracking.

from fastapi import FastAPI, Header, HTTPException
import secrets

app = FastAPI()

# In production: store in database
VALID_KEYS = {
    secrets.token_hex(32): {'user': 'client_1', 'tier': 'free'},
}

@app.get('/api/data')
async def get_data(x_api_key: str = Header(...)):
    if x_api_key not in VALID_KEYS:
        raise HTTPException(401, 'Invalid API key')

    client = VALID_KEYS[x_api_key]
    return {'data': 'here', 'client': client['user']}
Enter fullscreen mode Exit fullscreen mode

Pros: Dead simple. Works everywhere. Easy to rotate.

Cons: No user identity. Keys can leak in logs/URLs. No expiration by default.

Security tip: Never put API keys in URL query params — they end up in server logs and browser history. Use headers.


2. JWT (JSON Web Tokens) — The Stateless Choice

Use when: Microservices, SPAs, mobile apps where you need user identity without hitting a database on every request.

from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import HTTPBearer
import jwt
from datetime import datetime, timedelta

app = FastAPI()
security = HTTPBearer()
SECRET = 'your-256-bit-secret'  # In production: env var

def create_token(user_id: str, role: str = 'user') -> str:
    payload = {
        'sub': user_id,
        'role': role,
        'exp': datetime.utcnow() + timedelta(hours=1),
        'iat': datetime.utcnow(),
    }
    return jwt.encode(payload, SECRET, algorithm='HS256')

def verify_token(credentials = Depends(security)):
    try:
        payload = jwt.decode(credentials.credentials, SECRET, algorithms=['HS256'])
        return payload
    except jwt.ExpiredSignatureError:
        raise HTTPException(401, 'Token expired')
    except jwt.InvalidTokenError:
        raise HTTPException(401, 'Invalid token')

@app.post('/auth/login')
async def login(username: str, password: str):
    # Verify credentials (simplified)
    if username == 'admin' and password == 'secret':
        token = create_token(username, role='admin')
        return {'access_token': token, 'token_type': 'bearer'}
    raise HTTPException(401, 'Invalid credentials')

@app.get('/api/protected')
async def protected(user = Depends(verify_token)):
    return {'message': f"Hello {user['sub']}", 'role': user['role']}
Enter fullscreen mode Exit fullscreen mode

Pros: Stateless (no session store). Contains user info. Cross-service compatible.

Cons: Can't revoke individual tokens (use short expiry + refresh tokens). Token size can be large.

Security tip: NEVER store JWTs in localStorage — use httpOnly cookies to prevent XSS theft.


3. OAuth2 — The Enterprise Choice

Use when: Third-party access, "Login with Google/GitHub", delegated permissions.

from fastapi import FastAPI, Request
import httpx

app = FastAPI()

GITHUB_CLIENT_ID = 'your_client_id'
GITHUB_CLIENT_SECRET = 'your_client_secret'

@app.get('/auth/github')
async def github_login():
    return {
        'url': f'https://github.com/login/oauth/authorize?client_id={GITHUB_CLIENT_ID}&scope=user:email'
    }

@app.get('/auth/github/callback')
async def github_callback(code: str):
    # Exchange code for access token
    async with httpx.AsyncClient() as client:
        token_response = await client.post(
            'https://github.com/login/oauth/access_token',
            json={
                'client_id': GITHUB_CLIENT_ID,
                'client_secret': GITHUB_CLIENT_SECRET,
                'code': code,
            },
            headers={'Accept': 'application/json'},
        )
        access_token = token_response.json()['access_token']

        # Get user info
        user_response = await client.get(
            'https://api.github.com/user',
            headers={'Authorization': f'Bearer {access_token}'},
        )
        user = user_response.json()

        # Create session/JWT for your app
        app_token = create_token(user['login'])
        return {'user': user['login'], 'token': app_token}
Enter fullscreen mode Exit fullscreen mode

Pros: Industry standard. Granular permissions. Users don't share passwords.

Cons: Complex flow. Requires redirect handling. Token management is tricky.


Comparison Table

Feature API Key JWT OAuth2
Complexity Low Medium High
User identity No Yes Yes
Expiration Manual Built-in Built-in
Revocation Easy Hard Medium
Best for Public APIs Internal/SPAs Third-party
Stateless Yes Yes No (needs auth server)

My Recommendations for 2026

  1. Start with API keys. If you're building an API and don't need user identity, API keys are fine. Add JWT later if needed.

  2. Use short-lived JWTs + refresh tokens. Access token: 15 minutes. Refresh token: 7 days. This limits damage from leaked tokens.

  3. Never implement OAuth2 yourself. Use a library (FastAPI has built-in support) or a service (Auth0, Clerk, Supabase Auth).

  4. Always HTTPS. All three methods send secrets in headers. Without TLS, anyone on the network can steal them.


Which auth method do you prefer for your APIs? Have you tried any of the newer approaches (like Passkeys or FIDO2)? 👇


More from me: 10 Dev Tools I Use Daily | 77 Scrapers on a Schedule | 150+ Free APIs

Top comments (0)