Mathew K Analytics

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,…

⬇ Download notebookOpen in Colab ↗

📓 Full notebook

Download .ipynb

FastAPI 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)
True
False

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])
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[2], line 1
----> 1 import jwt
      2 from datetime import datetime, timedelta, timezone
      3 SECRET_KEY = 'a-real-secret-would-be-much-longer-than-this'

ModuleNotFoundError: No module named 'jwt'

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)))
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[3], line 1
----> 1 parts = token.split('.')
      2 print(len(parts))
      3 import base64, json

NameError: name 'token' is not defined

Part 4: jwt.decode - Verifying and Reading a Token#

decoded = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
print(decoded['sub'])
print('exp' in decoded)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[4], line 1
----> 1 decoded = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
      2 print(decoded['sub'])
      3 print('exp' in decoded)

NameError: name 'jwt' is not defined

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}')
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[5], line 2
      1 try:
----> 2     jwt.decode(token, 'the-wrong-secret', algorithms=[ALGORITHM])
      3 except jwt.InvalidSignatureError as e:

NameError: name 'jwt' is not defined

During handling of the above exception, another exception occurred:

NameError                                 Traceback (most recent call last)
Cell In[5], line 3
      1 try:
      2     jwt.decode(token, 'the-wrong-secret', algorithms=[ALGORITHM])
----> 3 except jwt.InvalidSignatureError as e:
      4     print(f'rejected: {e}')

NameError: name 'jwt' is not defined

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}')
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[6], line 1
----> 1 expired_payload = {'sub': 'alex', 'exp': datetime.now(timezone.utc) - timedelta(minutes=1)}
      2 expired_token = jwt.encode(expired_payload, SECRET_KEY, algorithm=ALGORITHM)
      3 try:

NameError: name 'datetime' is not defined

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'])
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[7], line 6
      4     to_encode['exp'] = expire
      5     return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
----> 6 fresh_token = create_access_token({'sub': 'alex'})
      7 print(jwt.decode(fresh_token, SECRET_KEY, algorithms=[ALGORITHM])['sub'])

Cell In[7], line 3, in create_access_token(data, expires_minutes)
      1 def create_access_token(data: dict, expires_minutes: int = 15):
      2     to_encode = data.copy()
----> 3     expire = datetime.now(timezone.utc) + timedelta(minutes=expires_minutes)
      4     to_encode['exp'] = expire
      5     return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

NameError: name 'datetime' is not defined

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))
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[8], line 10
      8         raise HTTPException(status_code=401, detail='could not validate credentials')
      9     return {'username': payload['sub']}
---> 10 print(get_current_user(fresh_token))

NameError: name 'fresh_token' is not defined

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())
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[9], line 2
      1 from fastapi.security import OAuth2PasswordRequestForm
----> 2 from passlib.context import CryptContext
      3 pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
      4 USERS_DB = {'alex': {'username': 'alex', 'hashed_password': pwd_context.hash('hunter22')}}

ModuleNotFoundError: No module named 'passlib'

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())
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[10], line 3
      1 jwt_app = FastAPI()
      2 JWT_SECRET = 'another-real-secret-kept-only-on-the-server'
----> 3 jwt_users = {'sam': {'username': 'sam', 'hashed_password': pwd_context.hash('correct-horse')}}
      4 def jwt_create_token(username: str, minutes: int = 15):
      5     expire = datetime.now(timezone.utc) + timedelta(minutes=minutes)

NameError: name 'pwd_context' is not defined

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.