FastAPI Authentication with JWT
Implement JWT-based authentication in FastAPI with OAuth2 password flow, secure token signing, and a reusable get_current_user dependency.
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 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 indecode. 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, andaudclaims 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_userdependency and a thinrequire_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.
Related articles
- FastAPI FastAPI CORS: A Practical Tutorial
Configure CORS in FastAPI without security holes: how the browser preflight works, which origins and headers to allow, credentials and cookies, and the most common misconfigurations to avoid.
- FastAPI FastAPI Deployment with Uvicorn and Gunicorn
Deploy FastAPI to production with Gunicorn managing Uvicorn workers. Cover process counts, timeouts, and health checks.
- FastAPI FastAPI Middleware Tutorial
Learn how FastAPI middleware works under the hood and write your own for logging, timing, and request enrichment.
- FastAPI FastAPI OpenAPI Customization: A Practical Tutorial
Tailor FastAPI's auto-generated OpenAPI schema: tags, summaries, examples, response models, custom operation IDs, security schemes, and a custom Swagger UI your team will actually use.