Day 13 · ~12m

Authentication Patterns

Implement API keys, JWT basics, and protected routes.

🧑‍💻

Our API is open to anyone right now. How do I restrict access?

👩‍🏫

Authentication verifies who the caller is. The simplest pattern is an API key — a secret string the client sends with every request:

from fastapi import FastAPI, Depends, HTTPException, Header

app = FastAPI()
VALID_KEYS = {"key-alice-123": "alice", "key-bob-456": "bob"}

def require_auth(x_api_key: str = Header()):
    if x_api_key not in VALID_KEYS:
        raise HTTPException(status_code=401, detail="Invalid API key")
    return VALID_KEYS[x_api_key]

@app.get("/me")
def get_me(user: str = Depends(require_auth)):
    return {"user": user}

The client sends X-API-Key: key-alice-123 in the request header. The dependency checks it, and the endpoint receives the username.

🧑‍💻

What about JWT tokens? I've heard they're more common.

👩‍🏫

JWTs (JSON Web Tokens) are signed data packets. Instead of looking up a key in a database, you verify the signature. The token itself contains the user info:

import jwt
from datetime import datetime, timedelta

SECRET = "your-secret-key"

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

def verify_token(token: str) -> dict:
    try:
        return jwt.decode(token, SECRET, algorithms=["HS256"])
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=401, detail="Token expired")
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="Invalid token")

The server creates a token at login, the client sends it with every request, and the server verifies the signature without hitting a database.

🧑‍💻

How do I make some endpoints public and others protected?

👩‍🏫

Use the dependency selectively:

@app.get("/public")
def public_data():
    return {"message": "Anyone can see this"}

@app.get("/private")
def private_data(user: str = Depends(require_auth)):
    return {"message": f"Hello, {user}"}

@app.get("/admin")
def admin_only(user: str = Depends(require_admin)):
    return {"message": "Admin access"}

No dependency = public. Depends(require_auth) = authenticated. You can stack dependencies for role-based access.

🧑‍💻

What's the difference between authentication and authorization?

👩‍🏫

Authentication answers "who are you?" — checking the API key or JWT. Authorization answers "what can you do?" — checking if that user has permission for this specific action.

Both use the same Depends() pattern. Authentication returns the user identity. Authorization checks that identity against permissions and raises 403 Forbidden if access is denied.

Practice your skills

Sign up to write and run code in this lesson.

Already have an account? Sign in