In the previous article, we covered the basics of authentication in FastAPI, including the use of sessions, API keys, and basic HTTP authentication. Now, let’s take it a step further and talk about JWT (JSON Web Token) — one of the most popular ways to secure modern APIs.
What is JWT?
JSON Web Tokens (JWTs) are like digital ID cards. They are an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. JWTs are often used for authentication and information exchange in web development.
When you log in, the server assigns you an ID (the token), which you must display on every request to prove your identity.
Unlike traditional sessions, JWTs don’t need the server to “remember” you — everything it needs is inside the token itself.
How JWT Works
JWT authentication follows these steps:
User logs in with credentials (username/password)
Server validates credentials and generates a JWT token
The token is returned to the client
Client stores the token (usually in local storage or cookies)
For subsequent requests, the client sends the token in the Authorisation header
The server validates the token and processes the request if valid
JWT Structure
A JSON web token has three parts, separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqYW5lZG9lIiwiZXhwIjoxNzYwMDQ2MTgwfQ.xWdrV_-y3Fnsii_IdIzDXaUjb05FL2-CPal5iwJ_UlU
Header(green) – tells which algorithm was used to sign the token.
Payload(red) – contains your data (like username or role).
Signature(blue) – ensures the token hasn’t been changed.
Example:
{
"sub": "janedoe",
"role": "user",
"exp": 1716242622
}
Why JWTs are so popular
Stateless
Unlike traditional session-based authentication, JWTs are completely stateless.
There’s no need to store session information in memory or a database — the token itself contains everything the server needs to know about the user.
This means:
No need to “remember” users on the backend
Easier horizontal scaling (add more servers without worrying about shared sessions)
Reduced load on your database or cache
Scalable
Because they don’t rely on centralised session storage, JWTs are ideal for distributed systems or microservice architectures. Each service can independently verify the same token using a shared secret or public key — no coordination required.
That’s why large-scale platforms like Google, GitHub, and Auth0 rely on JWT-based authentication for millions of users daily.
Fast
Authenticating with JWTs is blazing fast.
The server only needs to verify the token’s signature ****and ****decode its payload ****— no database lookup or session validation required.
This stateless design significantly improves performance, especially under high traffic.
Cross-platform
JWTs are based on open standards (RFC 7519), making them universally compatible across languages, frameworks, and platforms.
Whether your frontend is built in React, Flutter, or Vue, and your backend runs on FastAPI, Node.js, or Go — JWTs work seamlessly for all.
But….
While JWTs offer major advantages, they come with trade-offs you must understand to use them securely and efficiently:
Hard to Invalidate Immediately
Once issued, JWTs remain valid until they expire — unless you implement a token blacklist or revocation mechanism.
If a user logs out or a token is compromised, there’s no easy way to instantly revoke it without additional logic (such as maintaining a deny list or rotating signing keys).
Can Get Large
JWTs contain encoded data (header, payload, signature) in Base64 format.
The more claims you add — user info, roles, permissions — the larger the token becomes.
This can slightly increase request size, especially for mobile or bandwidth-limited clients.
Must Be Stored Securely
JWTs often contain sensitive information and access rights.
If stored improperly (like in localStorage)
, they can be stolen via XSS attacks.
Safer alternatives include:
HTTP-only cookies (cannot be accessed by JavaScript)
Secure client storage mechanisms
Always combine this with HTTPS and proper token expiration.
Implementing JWT Authentication in FastAPI
Setting up JWT in FastAPI
pip install fastapi uvicorn[standard] python-jose[cryptography] passlib[bcrypt] python-multipart
Basic Project Structure
app/
├── main.py
├── auth/
│ ├── jwt_handler.py
│ └── routes.py
└── models/
└── user.py
Step 1: Create and Verify Tokens
# app/auth/jwt_handler.py
from datetime import datetime, timedelta
from jose import jwt, JWTError
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
def create_access_token(data: dict, expires_minutes: int = 30):
data_copy = data.copy()
expire = datetime.utcnow() + timedelta(minutes=expires_minutes)
data_copy.update({"exp": expire})
return jwt.encode(data_copy, SECRET_KEY, algorithm=ALGORITHM)
def verify_token(token: str):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except JWTError:
return None
Step 2: Create Auth Routes
# app/auth/routes.py
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from passlib.context import CryptContext
from .jwt_handler import create_access_token, verify_token
router = APIRouter(prefix="/auth", tags=["Auth"])
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Mock database
fake_users = {
"janedoe": {
"username": "janedoe",
"hashed_password": pwd_context.hash("password123")
}
}
@router.post("/login")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = fake_users.get(form_data.username)
if not user or not pwd_context.verify(form_data.password, user["hashed_password"]):
raise HTTPException(status_code=401, detail="Invalid username or password")
token = create_access_token({"sub": user["username"]})
return {"access_token": token, "token_type": "bearer"}
@router.get("/verify")
def verify(token: str):
data = verify_token(token)
if not data:
raise HTTPException(status_code=401, detail="Invalid or expired token")
return {"user": data["sub"]}
Step 3: Protect Your Routes
# app/main.py
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from app.auth.jwt_handler import verify_token
from app.auth.routes import router as auth_router
app = FastAPI(title="JWT Auth Demo")
app.include_router(auth_router)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
def get_current_user(token: str = Depends(oauth2_scheme)):
data = verify_token(token)
if not data:
raise HTTPException(status_code=401, detail="Invalid or expired token")
return data
@app.get("/")
def home():
return {"message": "Welcome to the public route"}
@app.get("/dashboard")
def dashboard(current_user: dict = Depends(get_current_user)):
return {"message": f"Welcome {current_user['sub']}! This is a protected route."}
Step 4: Test your JWT System
uvicorn app.main:app --reload
Then test your API with Swagger:
Go to
localhost:8000
Post
auth/login
→ get the tokenCopy the token and send it in the headers:
Authorization: Bearer <your_token_here>
4. Go to /dashboard → should return your username.
Best Practices for API Security
Use HTTPS: Always use HTTPS to encrypt data in transit