In today’s micro‑service world, exposing APIs without proper protection is a recipe for disaster. JSON Web Tokens (JWT) have become the de‑facto standard for stateless authentication, and FastAPI makes integrating JWT a breeze. This guide walks you through securing your FastAPI endpoints with JWT, from project setup to token verification, while highlighting common mistakes and offering handy tricks.
What You’ll Need
- Python 3.9+ installed
- Virtual environment (venv or Conda)
- FastAPI and Uvicorn
- PyJWT or python‑jose library
- A code editor (VS Code, PyCharm, etc.)
- Basic knowledge of HTTP status codes and REST principles
Step 1: Set Up a Fresh FastAPI Project
First, create a clean directory and spin up a virtual environment. Open your terminal and run:
python -m venv env && source env/bin/activate
Then install FastAPI, Uvicorn, and a JWT library. We’ll use python‑jose[cryptography] because it supports RSA keys out of the box:
pip install fastapi uvicorn python-jose[cryptography]
Create a file called main.py and add a minimal FastAPI app to verify everything works:
from fastapi import FastAPI
app = FastAPI()
@app.get(‘/’)
def root():
return {‘message’: ‘FastAPI is up and running!’}
Run the server with:
uvicorn main:app –reload
Visit http://127.0.0.1:8000 in your browser – you should see the JSON greeting. This confirms your environment is ready for the next steps.
Step 2: Define a Secure Settings Module
Hard‑coding secrets is a rookie mistake. Create a config.py file to store your JWT secret key, algorithm, and token expiry. In production you’ll pull these values from environment variables or a secrets manager.
import os
SECRET_KEY = os.getenv(‘JWT_SECRET_KEY’, ‘supersecretkeychangeMe’)
ALGORITHM = ‘HS256’ # For symmetric encryption; switch to RS256 for RSA
ACCESS_TOKEN_EXPIRE_MINUTES = 30
Remember to add .env to .gitignore if you use a dotenv file. This keeps your secret out of the repo.
Step 3: Create Utility Functions for Token Generation and Verification
In a new file auth.py, write helper functions that encode a payload into a JWT and decode it back. Using python‑jose keeps the code concise.
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from .config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=’token’)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
to_encode.update({‘exp’: expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def verify_token(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=’Could not validate credentials’,
headers={‘WWW-Authenticate’: ‘Bearer’},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get(‘sub’)
if username is None:
raise credentials_exception
return username
except JWTError:
raise credentials_exception
Key points:
OAuth2PasswordBearerextracts theAuthorization: Bearer <token>header automatically.- We store the username (or user ID) in the
subclaim – this is the standard “subject” field.
Step 4: Build a Login Endpoint That Issues Tokens
FastAPI’s Form class lets you accept username and password as form‑encoded data, mimicking typical OAuth2 flows. In main.py, add a dummy user store and the token endpoint.
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from .auth import create_access_token
from .config import ACCESS_TOKEN_EXPIRE_MINUTES
from datetime import timedelta
app = FastAPI()
# In‑memory user “database” – replace with real DB in production
FAKE_USERS_DB = {
‘alice’: {‘username’: ‘alice’, ‘full_name’: ‘Alice Example’, ‘hashed_password’: ‘fakehashedsecret’},
‘bob’: {‘username’: ‘bob’, ‘full_name’: ‘Bob Sample’, ‘hashed_password’: ‘fakehashedpassword’},
}
def fake_hash_password(password: str):
return ‘fakehashed’ + password
@app.post(‘/token’)
def login(form_data: OAuth2PasswordRequestForm = Depends()):
user_dict = FAKE_USERS_DB.get(form_data.username)
if not user_dict or fake_hash_password(form_data.password) != user_dict[‘hashed_password’]:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=’Incorrect username or password’, headers={‘WWW-Authenticate’: ‘Bearer’})
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(data={‘sub’: user_dict[‘username’]}, expires_delta=access_token_expires)
return {‘access_token’: access_token, ‘token_type’: ‘bearer’}
Test it with curl:
curl -X POST -F ‘username=alice’ -F ‘password=secret’ http://127.0.0.1:8000/token
The response contains a JWT you can paste into the Authorization header for subsequent calls.
Step 5: Protect Your API Routes Using Dependency Injection
Now that we can verify tokens, we can lock down any endpoint simply by adding the verify_token dependency. Let’s create a protected route that returns the current user’s profile.
@app.get(‘/users/me’)
def read_current_user(username: str = Depends(verify_token)):
user = FAKE_USERS_DB.get(username)
return {‘username’: user[‘username’], ‘full_name’: user[‘full_name’]}
When you call this endpoint without a valid token, FastAPI automatically returns a 401 Unauthorized response. With a valid token, you’ll see the user data.
For larger projects, consider grouping protected routes under an APIRouter and applying the dependency at the router level:
from fastapi import APIRouter
router = APIRouter(prefix=’/protected’, dependencies=[Depends(verify_token)])
@router.get(‘/dashboard’)
def dashboard():
return {‘msg’: ‘Welcome to the secure dashboard’}
app.include_router(router)
This pattern keeps your code DRY and makes it obvious which sections are secured.
Common Mistakes to Avoid
1. Storing the secret key in source control. Always load it from environment variables or a secret manager.
2. Using a weak algorithm. HS256 is fine for many internal services, but for public APIs prefer RS256 with a proper key pair.
3. Never checking token expiration manually. Let the JWT library raise ExpiredSignatureError – it’s more reliable than custom time checks.
4. Hard‑coding user passwords. In production, store salted hashes (e.g., bcrypt) and use a proper authentication backend.
5. Returning the entire token payload. Only expose what the client needs; leaking internal claims can create security leaks.
Tips and Tricks
• Refresh tokens. Implement a separate endpoint that issues a new short‑lived access token using a long‑lived refresh token stored in an HttpOnly cookie.
• Scope claims. Add a scopes array to the JWT payload and check it in your dependencies to enforce role‑based access.
• Automatic OpenAPI docs. FastAPI will annotate the /token endpoint as OAuth2PasswordBearer, so Swagger UI lets you “Authorize” with a bearer token out of the box.
• Rate limiting. Pair JWT authentication with a middleware like slowapi to protect against brute‑force token guessing.
• Key rotation. Store your secret in a key‑vault and rotate it periodically; keep a list of previous keys to validate older tokens during the transition.
Frequently Asked Questions
Can I use JWTs with asynchronous FastAPI routes?
Absolutely. The python‑jose functions are CPU‑bound but fast enough for most use‑cases. If you anticipate heavy token generation, offload the work to a thread pool or use an async‑compatible library like authlib.
What’s the difference between access tokens and refresh tokens?
An access token is short‑lived (minutes to an hour) and is sent with every request. A refresh token lives longer (days or weeks) and is only exchanged for a new access token. Refresh tokens should be stored securely – preferably in HttpOnly, Secure cookies – to mitigate XSS attacks.
How do I invalidate a JWT before it expires?
Since JWTs are stateless, you cannot “revoke” them without additional state. Common strategies include maintaining a token blacklist in Redis or embedding a jti (JWT ID) claim and checking it against a revocation list on each request.
Conclusion
Securing FastAPI endpoints with JWT authentication is straightforward once you understand the moving parts: a secret key, token creation, and a dependency that validates incoming tokens. By following the steps above, you’ll have a robust, stateless authentication layer that scales with your API. Remember to keep secrets out of source control, choose the right signing algorithm, and consider refresh‑token flows for a production‑grade solution. Happy coding, and keep your APIs safe!
Photo by Microsoft Copilot on Unsplash





