Lesson 13 · FastAPI deep dive
FastAPI Tutorial #13: JWT Authentication End to End
Video thirteen of the eighteen-part series: stateless, portable tokens instead of an in-memory map. Encoding and decoding JWTs, expiry, signature tampering,…
- CourseFastAPI deep dive
- Lesson13 of 18
- Video16 min
- FormatJupyter notebook · 10 code cells
What you'll learn
- The Problem - an In-Memory Token Map Doesn't Scale
- jwt.encode - Building a Signed Token
- The Three Parts of a JWT
- jwt.decode - Verifying and Reading a Token
- A Wrong Secret Correctly Fails
- An Expired Token Correctly Fails
- createaccesstoken - a Reusable Helper
- getcurrentuser - Now Backed by JWT Instead of a Map
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbFastAPI Deep-Dive, Video 13: JWT Authentication#
- Video thirteen of the eighteen-part series: stateless, portable tokens instead of an in-memory map.
- Encoding and decoding JWTs, expiry, signature tampering, and protecting routes with a real token.
- Let's get into it.
Part 1: The Problem - an In-Memory Token Map Doesn't Scale#
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
SERVER_A_TOKENS = {'abc123': 'alex'}
SERVER_B_TOKENS = {}
print('abc123' in SERVER_A_TOKENS)
print('abc123' in SERVER_B_TOKENS)
Part 2: jwt.encode - Building a Signed Token#
import jwt
from datetime import datetime, timedelta, timezone
SECRET_KEY = 'a-real-secret-would-be-much-longer-than-this'
ALGORITHM = 'HS256'
payload = {'sub': 'alex', 'exp': datetime.now(timezone.utc) + timedelta(minutes=15)}
token = jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
print(type(token))
print(token[:20])
Part 3: The Three Parts of a JWT#
parts = token.split('.')
print(len(parts))
import base64, json
header_bytes = parts[0] + '=' * (-len(parts[0]) % 4)
print(json.loads(base64.urlsafe_b64decode(header_bytes)))
Part 4: jwt.decode - Verifying and Reading a Token#
decoded = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
print(decoded['sub'])
print('exp' in decoded)
Part 5: A Wrong Secret Correctly Fails#
try:
jwt.decode(token, 'the-wrong-secret', algorithms=[ALGORITHM])
except jwt.InvalidSignatureError as e:
print(f'rejected: {e}')
Part 6: An Expired Token Correctly Fails#
expired_payload = {'sub': 'alex', 'exp': datetime.now(timezone.utc) - timedelta(minutes=1)}
expired_token = jwt.encode(expired_payload, SECRET_KEY, algorithm=ALGORITHM)
try:
jwt.decode(expired_token, SECRET_KEY, algorithms=[ALGORITHM])
except jwt.ExpiredSignatureError as e:
print(f'rejected: {e}')
Part 7: create_access_token - a Reusable Helper#
def create_access_token(data: dict, expires_minutes: int = 15):
to_encode = data.copy()
expire = datetime.now(timezone.utc) + timedelta(minutes=expires_minutes)
to_encode['exp'] = expire
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
fresh_token = create_access_token({'sub': 'alex'})
print(jwt.decode(fresh_token, SECRET_KEY, algorithms=[ALGORITHM])['sub'])
Part 8: get_current_user - Now Backed by JWT Instead of a Map#
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='token')
def get_current_user(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
except jwt.PyJWTError:
raise HTTPException(status_code=401, detail='could not validate credentials')
return {'username': payload['sub']}
print(get_current_user(fresh_token))
Part 9: A Login Endpoint Issuing Real JWTs#
from fastapi.security import OAuth2PasswordRequestForm
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
USERS_DB = {'alex': {'username': 'alex', 'hashed_password': pwd_context.hash('hunter22')}}
@app.post('/token')
def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = USERS_DB.get(form_data.username)
if not user or not pwd_context.verify(form_data.password, user['hashed_password']):
raise HTTPException(status_code=401, detail='incorrect username or password')
access_token = create_access_token({'sub': user['username']})
return {'access_token': access_token, 'token_type': 'bearer'}
@app.get('/users/me')
def read_me(current_user: dict = Depends(get_current_user)):
return current_user
client = TestClient(app)
login_response = client.post('/token', data={'username': 'alex', 'password': 'hunter22'})
issued = login_response.json()['access_token']
print(client.get('/users/me', headers={'Authorization': f'Bearer {issued}'}).json())
Part 10: A Real Pattern - a Small, Complete JWT-Protected App#
jwt_app = FastAPI()
JWT_SECRET = 'YOUR_SECRET_HERE'
jwt_users = {'sam': {'username': 'sam', 'hashed_password': pwd_context.hash('correct-horse')}}
def jwt_create_token(username: str, minutes: int = 15):
expire = datetime.now(timezone.utc) + timedelta(minutes=minutes)
return jwt.encode({'sub': username, 'exp': expire}, JWT_SECRET, algorithm=ALGORITHM)
def jwt_get_current_user(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=[ALGORITHM])
except jwt.PyJWTError:
raise HTTPException(status_code=401, detail='could not validate credentials')
return {'username': payload['sub']}
@jwt_app.post('/login')
def jwt_login(form_data: OAuth2PasswordRequestForm = Depends()):
user = jwt_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='incorrect username or password')
return {'access_token': jwt_create_token(user['username']), 'token_type': 'bearer'}
@jwt_app.get('/profile')
def jwt_profile(current_user: dict = Depends(jwt_get_current_user)):
return current_user
jwt_client = TestClient(jwt_app)
print(jwt_client.get('/profile').status_code)
login = jwt_client.post('/login', data={'username': 'sam', 'password': 'correct-horse'})
token = login.json()['access_token']
print(jwt_client.get('/profile', headers={'Authorization': f'Bearer {token}'}).json())
Wrap-Up: What You Learned#
- An in-memory token map doesn't survive across multiple separate server instances.
- jwt.encode signs a payload with a secret key, producing a self-contained, portable token.
- A JWT has three dot-separated parts: a header, a payload, and a signature.
- jwt.decode re-verifies the signature using the secret and returns the payload if it matches.
- Decoding with the wrong secret raises InvalidSignatureError, stopping forged tokens.
- The exp claim is checked automatically; an expired token fails even with the right secret.
- A shared create_access_token helper centralizes the expiry logic in one place.
- get_current_user backed by JWT just decodes the token, no shared map needed at all.
- A login endpoint's password verification barely changes; only the issued token itself does.
- That wraps up JWT authentication. Next up: Databases with SQLAlchemy.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



