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.

Leave a Comment

Your email address will not be published. Required fields are marked *