PyFu

JWT for Authentication and Authorization in FastAPI

Python Web Development Frameworks

In FastAPI, JWT can be integrated into the authentication and authorization pipeline by issuing a token after a successful login, and then validating this token on protected routes.

We will use pyjwt for signing and decoding tokens, and passlib for password hashing.

Code Example

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from passlib.context import CryptContext
from jose import JWTError, jwt
from datetime import datetime, timedelta

app = FastAPI()

# Secret key and algorithm
SECRET_KEY = "super-secret-key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60

# Password hashing
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# OAuth2 scheme
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

# Dummy user storage
fake_users_db = {
    "askar": {
        "username": "askar",
        "hashed_password": pwd_context.hash("pyfu123")
    }
}

# Verify password
def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)

# Authenticate user
def authenticate_user(username: str, password: str):
    user = fake_users_db.get(username)
    if not user or not verify_password(password, user['hashed_password']):
        return None
    return user

# Create JWT token
def create_access_token(data: dict, expires_delta: timedelta = None):
    to_encode = data.copy()
    expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

# Login route to issue JWT
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(status_code=401, detail="Invalid credentials")
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user["username"]}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}

# Dependency to get current user
async def get_current_user(token: str = Depends(oauth2_scheme)):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise HTTPException(status_code=401, detail="Invalid token")
    except JWTError:
        raise HTTPException(status_code=401, detail="Invalid token")
    user = fake_users_db.get(username)
    if user is None:
        raise HTTPException(status_code=401, detail="User not found")
    return user

# Protected route
@app.get("/protected")
async def read_protected(current_user: dict = Depends(get_current_user)):
    return {"message": f"Hello {current_user['username']}, you are authenticated."}

Why JWT auth matters from an offensive security perspective

A JWT is trusted because its signature verifies, so every weakness in how that signature is produced or checked is an authentication bypass. This tutorial is a compact catalogue of the things that go wrong, and each one maps to a dedicated attack page.

The whole scheme hangs on SECRET_KEY. Here it is "super-secret-key", hardcoded in the source. Recover that value (from the repo, a config leak, or by brute-forcing it offline against any captured token) and you can mint a token with any sub you like and the server will accept it. That is the most common JWT failure in the wild. See Authentication Bypass via JWT Hardcoded Secret and Cracking Weak JWT Signing Keys.

The line jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) is doing more security work than it looks. Pinning algorithms is what stops two classic forgeries:

  • The none algorithm. If a library accepts alg: none, an attacker strips the signature entirely and the token is trusted unsigned. Verification must never allow it. See Forging JWTs with the none Algorithm.
  • Algorithm confusion (RS256 to HS256). If an app verifies with a key type the attacker can influence, a token signed with HS256 can be validated against the public RS256 key as if it were the HMAC secret. The fix is to pin the exact expected algorithm, which this example does. See JWT Algorithm Confusion (RS256 to HS256).

Two more things to internalize. First, once the signature checks out, the claims are authoritative: the app trusts payload["sub"] completely. So if authorization data (a role or is_admin claim) is carried in the token, recovering the key is not just impersonation, it is privilege escalation in the same move. Second, validation has to actually verify the right things, the signature, the expiry, the issuer/audience, in the right order; skipping or loosening any of them is its own bypass class, covered in Authentication Bypass via Broken JWT Validation and JWT Header Injection via jku, jwk, and kid.

For the format itself and how tokens are structured, start at Introduction to JSON Web Tokens in Python.

Mitigation

Load the signing key from a secret manager or environment, never hardcode it, and make it long and random so offline cracking is hopeless. Pin the exact expected algorithm on decode so neither alg: none nor an RS256-to-HS256 confusion is reachable, and require the library to verify the signature, expiry, and the issuer and audience before you trust a single claim. Carry authorization data server-side or re-check it rather than trusting a role claim blindly, since a key compromise then yields impersonation but not automatic privilege escalation.

import os
from jose import jwt

SECRET_KEY = os.environ["JWT_SECRET"]        # never hardcoded

payload = jwt.decode(
    token,
    SECRET_KEY,
    algorithms=["HS256"],                    # pinned: no none, no confusion
    issuer="pyfu-auth",
    audience="pyfu-api",
    options={"require": ["exp", "iss", "aud"]},
)