When it comes to building modern, high‑performance APIs with Python, FastAPI has quickly become the go‑to framework for developers who crave speed, type safety, and developer friendliness. Yet, a powerful API is only as good as its security model, and that’s where JWT (JSON Web Tokens) combined with OAuth2 shines. In this guide we’ll walk through everything you need to know to implement robust JWT‑based OAuth2 authentication in a FastAPI project—from the underlying concepts to a complete, production‑ready code example. By the end, you’ll be equipped to protect your endpoints, manage token lifecycles, and scale authentication securely.
Why Choose JWT and OAuth2 with FastAPI?
- Stateless authentication: JWTs carry all necessary claims, eliminating the need for server‑side session storage.
- Scalability: Because tokens are self‑contained, horizontal scaling and micro‑service architectures become straightforward.
- Standard compliance: OAuth2 is an industry‑standard protocol, and FastAPI provides first‑class support via
fastapi.security. - Developer experience: FastAPI’s automatic OpenAPI documentation displays security schemes out of the box, making client integration painless.
Core Concepts You Should Know
1. OAuth2 Grant Types
OAuth2 defines several grant types for different use‑cases. For most API‑first projects, the Resource Owner Password Credentials (ROPC) or Authorization Code flow with PKCE is used. In this tutorial we’ll focus on the password grant, which is ideal for internal tools and mobile apps where the client can safely handle user credentials.
2. JSON Web Tokens (JWT)
A JWT consists of three Base64‑URL‑encoded parts: header.payload.signature. The header declares the signing algorithm (e.g., HS256), the payload carries claims like sub (subject), exp (expiration), and custom fields, and the signature ensures integrity.
3. Access vs. Refresh Tokens
Access tokens are short‑lived (typically 5‑15 minutes) to limit exposure if stolen. Refresh tokens are longer‑lived and can be exchanged for new access tokens without re‑authenticating the user. Implementing both improves security and user experience.
Setting Up the Project
# Install dependencies
pip install fastapi uvicorn python-multipart python-jose[cryptography] passlib[bcrypt] sqlalchemy
We’ll use python-jose for JWT handling, passlib for password hashing, and SQLAlchemy as a lightweight ORM.
Step‑by‑Step Implementation
1. Define Settings and Security Utilities
# app/config.py
import os
from datetime import timedelta
class Settings:
SECRET_KEY: str = os.getenv("SECRET_KEY", "supersecretkey")
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 15
REFRESH_TOKEN_EXPIRE_DAYS: int = 30
settings = Settings()
# app/security.py
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from .config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
def create_refresh_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS))
to_encode.update({"exp": expire, "type": "refresh"})
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
def decode_token(token: str) -> dict:
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
return payload
except JWTError:
raise
2. Create the User Model
# app/models.py
from sqlalchemy import Column, Integer, String, Boolean
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False)
is_active = Column(Boolean, default=True)
3. Dependency for Getting the Current User
# app/dependencies.py
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from .security import decode_token
from .database import SessionLocal
from .models import User
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
def get_current_user(token: str = Depends(oauth2_scheme), db: SessionLocal = Depends(get_db)) -> User:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = decode_token(token)
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except Exception:
raise credentials_exception
user = db.query(User).filter(User.username == username).first()
if user is None:
raise credentials_exception
return user
4. Authentication Endpoints
# app/main.py
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from .models import Base, User
from .database import engine, SessionLocal
from .security import (
verify_password,
get_password_hash,
create_access_token,
create_refresh_token,
)
from .dependencies import get_current_user, get_db
app = FastAPI(title="FastAPI JWT OAuth2 Demo")
Base.metadata.create_all(bind=engine)
@app.post("/token", summary="Obtain access and refresh tokens")
def login(
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(get_db)
):
user = db.query(User).filter(User.username == form_data.username).first()
if not user or not verify_password(form_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token = create_access_token({"sub": user.username})
refresh_token = create_refresh_token({"sub": user.username})
return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}
@app.post("/refresh", summary="Refresh access token using a valid refresh token")
def refresh_token(refresh_token: str, db: Session = Depends(get_db)):
try:
payload = decode_token(refresh_token)
if payload.get("type") != "refresh":
raise HTTPException(status_code=400, detail="Invalid token type")
username = payload.get("sub")
except Exception:
raise HTTPException(status_code=401, detail="Invalid refresh token")
user = db.query(User).filter(User.username == username).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
new_access = create_access_token({"sub": user.username})
return {"access_token": new_access, "token_type": "bearer"}
@app.get("/users/me", summary="Get current authenticated user")
def read_current_user(current_user: User = Depends(get_current_user)):
return {"username": current_user.username, "is_active": current_user.is_active}
Testing the Flow with cURL
- Step 1 – Register a user (once):
curl -X POST "http://localhost:8000/users/" -H "Content-Type: application/json" -d '{"username":"alice","password":"strongpwd"}' - Step 2 – Obtain tokens:
curl -X POST "http://localhost:8000/token" -d "grant_type=password&username=alice&password=strongpwd" - Step 3 – Access a protected route:
curl -H "Authorization: Bearer <ACCESS_TOKEN>" http://localhost:8000/users/me - Step 4 – Refresh the access token:
curl -X POST "http://localhost:8000/refresh" -d "refresh_token=<REFRESH_TOKEN>"
Best Practices for Production‑Ready JWT Auth
- Use HTTPS everywhere. Tokens travel in clear text over HTTP; TLS eliminates man‑in‑the‑middle attacks.
- Rotate secret keys regularly. Store the secret in a vault (e.g., AWS Secrets Manager) and implement key‑id (kid) rotation if you need multiple keys.
- Set appropriate token lifetimes. Short‑lived access tokens (5