Skip to content
C Codeloom
FastAPI

FastAPI Authentication with JWT

Implement JWT-based authentication in FastAPI with OAuth2 password flow, secure token signing, and a reusable get_current_user dependency.

·4 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • How OAuth2 password flow works in FastAPI
  • Issuing and verifying JWTs
  • Hashing passwords with bcrypt
  • Protecting routes with a dependency
  • Token expiration and refresh tradeoffs

Prerequisites

  • Comfortable with FastAPI routes and dependencies

What and Why

Most APIs need a way to identify the caller. JWTs are a compact, signed token format that lets you check identity without a database round-trip on every request. FastAPI ships with helpers for OAuth2 flows that pair perfectly with JWTs.

Mental Model

JWTs encode claims like sub and exp and sign them with a secret. The server hands one out at login. The client sends it back on every request. The server verifies the signature and expiration. If valid, the request is authenticated.

Login                Subsequent requests
-----                ------------------
POST /token          GET /me
username, password   Authorization: Bearer <jwt>
     |                       |
     v                       v
verify password         verify signature
issue JWT               read sub (user id)
     |                       |
     v                       v
return token            load user, run handler
JWT auth flow

Hands-on Example

Install dependencies.

pip install fastapi uvicorn "python-jose[cryptography]" "passlib[bcrypt]"

Hashing and token helpers.

# security.py
from datetime import datetime, timedelta, timezone
from jose import jwt, JWTError
from passlib.context import CryptContext

SECRET_KEY = "replace-with-env-var"
ALGORITHM = "HS256"
ACCESS_TOKEN_TTL = timedelta(minutes=30)

pwd = CryptContext(schemes=["bcrypt"], deprecated="auto")

def hash_password(raw: str) -> str:
    return pwd.hash(raw)

def verify_password(raw: str, hashed: str) -> bool:
    return pwd.verify(raw, hashed)

def create_access_token(subject: str) -> str:
    expire = datetime.now(timezone.utc) + ACCESS_TOKEN_TTL
    payload = {"sub": subject, "exp": expire}
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

def decode_token(token: str) -> dict:
    return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])

The app, with a login endpoint and a reusable dependency.

# main.py
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from jose import JWTError
from security import (hash_password, verify_password,
                      create_access_token, decode_token)

app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

# Pretend user table
FAKE_USERS = {
    "ada": {"username": "ada", "password_hash": hash_password("secret123")},
}

class User(BaseModel):
    username: str

@app.post("/token")
def login(form: OAuth2PasswordRequestForm = Depends()):
    user = FAKE_USERS.get(form.username)
    if not user or not verify_password(form.password, user["password_hash"]):
        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Bad credentials")
    return {"access_token": create_access_token(user["username"]),
            "token_type": "bearer"}

def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
    creds_error = HTTPException(status.HTTP_401_UNAUTHORIZED,
                                "Invalid token",
                                headers={"WWW-Authenticate": "Bearer"})
    try:
        payload = decode_token(token)
        username = payload.get("sub")
        if not username:
            raise creds_error
    except JWTError:
        raise creds_error
    user = FAKE_USERS.get(username)
    if not user:
        raise creds_error
    return User(username=username)

@app.get("/me")
def me(user: User = Depends(get_current_user)):
    return user

Hit it from curl.

TOKEN=$(curl -s -X POST -F username=ada -F password=secret123 \
  http://localhost:8000/token | jq -r .access_token)

curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/me

Common Pitfalls

  • Storing the JWT signing key in source. Always load it from an env var or secrets manager.
  • Using a long-lived token without refresh. If it leaks, attackers have access until expiration.
  • Skipping password hashing on legacy paths. Always hash with bcrypt or argon2, never store plaintext.
  • Letting the algorithm be none. Pin the algorithm in decode. Never accept the value from the token header.
  • Putting sensitive PII in the JWT payload. It is base64, not encrypted.

Practical Tips

  • Use short access tokens, around 15 to 30 minutes, plus a refresh token stored as an HTTP-only cookie.
  • Add iat, iss, and aud claims and verify them on decode.
  • Rotate secrets and support key ids in the JWT header so you can phase old keys out.
  • Build a single get_current_user dependency and a thin require_role("admin") wrapper for permissions.
  • Test the unhappy paths first. Expired, malformed, wrong signature, missing header.

Wrap-up

JWTs make stateless auth simple once you set up signing, verification, and a clean dependency. Hash passwords correctly, keep the secret out of code, and pair short access tokens with refresh tokens for a balanced security and UX. FastAPI’s dependency system makes the wiring straightforward and easy to test.