FastAPI has quickly become the go‑to framework for building high‑performance APIs in Python, and one of the most common questions newcomers ask is how to secure those APIs with JWT and OAuth2. In this guide we’ll walk through everything you need to know to implement robust JWT‑based OAuth2 authentication in a FastAPI project—step by step, with clear code examples, best‑practice tips, and SEO‑friendly explanations that will help your article rank for keywords like “Python FastAPI JWT OAuth2 authentication”. Whether you’re building a microservice, a mobile backend, or a full‑stack web app, mastering this authentication flow will keep your endpoints safe and your users happy.
Why Choose JWT and OAuth2 with FastAPI?
Before diving into code, it’s worth understanding the benefits that make JSON Web Tokens (JWT) and the OAuth2 protocol a natural fit for FastAPI:
- Statelessness: JWTs carry all the necessary claims, so you don’t need server‑side session storage.
- Scalability: Because tokens are self‑contained, horizontal scaling across multiple instances is straightforward.
- Performance: FastAPI’s async nature pairs well with lightweight token verification, keeping response times low.
- Industry Standard: OAuth2 is the de‑facto protocol for delegated access, and many third‑party services (Google, GitHub, etc.) already speak it.
Core Concepts You Need to Know
1. Access Token vs. Refresh Token
An access token is a short‑lived JWT that grants the bearer permission to call protected endpoints. A refresh token is a longer‑lived credential used to obtain new access tokens without re‑authenticating the user.
2. OAuth2 Grant Types
FastAPI primarily supports the password grant (resource owner password credentials) for internal APIs and the authorization_code flow for third‑party integrations. In this tutorial we’ll focus on the password grant because it’s the simplest to demonstrate.
3. Security Scopes
FastAPI lets you define scopes (e.g., read:items, write:items) that are embedded in the JWT payload. Endpoints can then enforce scope checks automatically.
Setting Up the Project
First, create a fresh virtual environment and install the required packages:
python -m venv venv
source venv/bin/activate # On Windows use `venv\Scripts\activate`
pip install fastapi uvicorn python-multipart python-jose[cryptography] passlib[bcrypt]
We’ll use python-jose for JWT encoding/decoding and passlib for password hashing.
Creating the User Model and Password Utilities
In a real application you’d store users in a database, but for clarity we’ll keep an in‑memory dictionary.
from passlib.context import CryptContext
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)
# Mock user DB
fake_users_db = {
"alice": {
"username": "alice",
"full_name": "Alice Wonderland",
"email": "alice@example.com",
"hashed_password": get_password_hash("secret123"),
"disabled": False,
"scopes": ["read:items", "write:items"]
}
}
Configuring JWT Settings
Define the secret key, algorithm, and token expiration times. Keep the secret out of source control in production (e.g., use environment variables).
import os
from datetime import datetime, timedelta
SECRET_KEY = os.getenv("SECRET_KEY", "supersecretkeychange_me")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
REFRESH_TOKEN_EXPIRE_DAYS = 7
Generating Access and Refresh Tokens
The following helper creates both token types. Notice how we embed scopes and sub (subject) claims.
from jose import JWTError, jwt
def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
to_encode.update({"exp": expire, "type": "access"})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def create_refresh_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS))
to_encode.update({"exp": expire, "type": "refresh"})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
OAuth2PasswordBearer Dependency
FastAPI provides OAuth2PasswordBearer to extract the token from the Authorization: Bearer header. We’ll also create a function to decode and validate the JWT.
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def decode_token(token: str):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
token_type = payload.get("type")
if token_type != "access":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token type",
)
username: str = payload.get("sub")
if username is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
token_scopes = payload.get("scopes", [])
return {"username": username, "scopes": token_scopes}
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
)
Creating the Login Endpoint
The /token route authenticates the user, issues both tokens, and returns them in a JSON response.
app = FastAPI()
@app.post("/token", response_model=dict)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
user_dict = fake_users_db.get(form_data.username)
if not user_dict:
raise HTTPException(status_code=400, detail="Incorrect username or password")
if not verify_password(form_data.password, user_dict["hashed_password"]):
raise HTTPException(status_code=400, detail="Incorrect username or password")
user_data = {
"sub": user_dict["username"],
"scopes": user_dict["scopes"]
}
access_token = create_access_token(user_data)
refresh_token = create_refresh_token(user_data)
return {
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "bearer"
}
Protecting Routes with Scopes
Now we can protect any endpoint by requiring a valid JWT and optionally checking for specific scopes.
from fastapi.security import SecurityScopes
def get_current_user(security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme)):
token_data = decode_token(token)
user = fake_users_db.get(token_data["username"])
if user is None or user["disabled"]:
raise HTTPException(status_code=400, detail="Inactive user")
# Scope validation
for required_scope in security_scopes.scopes:
if required_scope not in token_data["scopes"]:
raise HTTPException(
status_code=403,
detail=f"Not enough permissions. Required scope: {required_scope}"
)
return user
@app.get("/items/", response_model=list)
async def read_items(current_user: dict = Depends(get_current_user)):
return [{"item_id": "foo", "owner": current_user["username"]}]
Refreshing an Expired Access Token
When the access token expires, the client can request a new one using the refresh token. This endpoint validates the refresh token’s type and issues a fresh access token.
@app.post("/refresh", response_model=dict)
async def refresh_access_token(refresh_token: str):
try:
payload = jwt.decode(refresh_token, SECRET_KEY, algorithms=[ALGORITHM])
if payload.get("type") != "refresh":
raise HTTPException(status_code=401, detail="Invalid refresh token")
username = payload.get("sub")
user = fake_users_db.get(username)
if not user:
raise HTTPException(status_code=401, detail="User not found")
new_access_token = create_access_token({"sub": username, "scopes": user["scopes"]})
return {"access_token": new_access_token, "token_type": "bearer"}
except JWTError:
raise HTTPException(status_code=401, detail="Could not validate refresh token")
Best Practices for Production‑Ready Security
- Use HTTPS: Never transmit tokens over plain HTTP.
- Rotate Secrets: Change
SECRET_KEYregularly and store it securely (e.g., AWS Secrets Manager). - Short‑Lived Access Tokens: Keep the access token lifespan under 15 minutes to limit exposure.
- Secure Cookie Storage: If you’re serving a browser client, consider
Leave a Reply