FastAPI JWT Tokens for Secure Login
In the previous topic, the "token" was just the username string — which is insecure in a real app. JWT (JSON Web Token) is the proper solution. A JWT is a signed token that carries user information inside it. The server can verify it without looking up a database on every request.
What a JWT Looks Like
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 .eyJzdWIiOiJtZWVyYSIsImV4cCI6MTcwMDAwMDAwMH0 .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c Three parts separated by dots: Part 1: Header (algorithm used) Part 2: Payload (user data — visible but signed) Part 3: Signature (proves it was not tampered with)
Payload decoded (not encrypted — just encoded):
{
"sub": "meera", ← subject (user identifier)
"exp": 1700000000 ← expiry timestamp
}
Install python-jose
pip install "python-jose[cryptography]"
JWT Configuration
from datetime import datetime, timedelta from jose import JWTError, jwt SECRET_KEY = "your-very-long-random-secret-key-keep-it-private" ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 30
The SECRET_KEY signs every token. Anyone with this key can forge tokens. Store it in an environment variable — never in your source code.
Creating a JWT Token
def create_access_token(data: dict) -> str:
payload = data.copy()
expires = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
payload["exp"] = expires
token = jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
return token
create_access_token({"sub": "meera"})
→ returns a signed JWT string
valid for 30 minutes
Verifying a JWT Token
def decode_token(token: str) -> str:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username = payload.get("sub")
if username is None:
raise HTTPException(status_code=401, detail="Invalid token")
return username
except JWTError:
raise HTTPException(status_code=401, detail="Invalid or expired token")
The Full Login + Protected Route Flow
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.post("/token")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(status_code=401, detail="Wrong credentials")
token = create_access_token({"sub": user.username})
return {"access_token": token, "token_type": "bearer"}
@app.get("/me")
def get_me(token: str = Depends(oauth2_scheme)):
username = decode_token(token)
return {"username": username}
JWT Flow Diagram
1. Client: POST /token {username, password}
│
2. Server: verifies credentials
creates JWT: {"sub":"meera","exp":...}
signs with SECRET_KEY
returns token string
│
3. Client: stores token
4. Client: GET /me
Authorization: Bearer eyJ...
│
5. Server: extracts token
decodes with SECRET_KEY
verifies signature + expiry
reads "sub" → "meera"
returns user data
Why JWT is Stateless
Session-based auth: Server stores session in database/memory. Every request → DB lookup. Does not scale horizontally easily. JWT auth: Token carries all info. Server only needs SECRET_KEY to verify. No DB lookup per request. Any server instance can verify any token.
Token Expiry and Refresh Tokens
Access tokens expire quickly (15–60 minutes) for security. A refresh token (longer-lived, stored securely) lets the client get a new access token without logging in again:
Access token: expires in 30 minutes ← used for API calls Refresh token: expires in 7 days ← used to get new access tokens
Key Points
- A JWT carries signed user data inside the token itself — no database lookup needed per request.
- The token has three parts: header, payload, and signature.
- Sign tokens with a secret key stored in an environment variable.
- Always set an expiry (
exp) on tokens to limit the damage if one is stolen. - Use
jose.jwt.decode()to verify and read token contents on each request.
