Handling Sessions with JWTs

Jan 15, 2025

Stateless JWTs simplify authentication but risk valid tokens post-logout. We can fix this by embedding expiration timestamps and validate server-side.

Authentication can feel overwhelming - there are countless libraries and services promising secure solutions. But for simple web apps, JSON Web Tokens (JWTs) gets the job done. JWTs work like digital ID cards: the server creates an encoded string (using cryptographic algorithms) that clients store and send back as a Bearer token in subsequent requests. These tokens contain verified information (like user IDs) that server can quickly validate without constant database checks.

The Stateless Challenge

JWTs are stateless by design - meaning the server doesn’t track their status after creation. Imagine a scenario where a user logs out on their device, but their JWT remains valid until expiration. Without additional safeguards, this creates a security gap where “logged out” users could theoretically keep accessing services.

Solution - Using Timestamps in JWT

One way to get around is by adding time limits directly into the tokens. When generating a JWT, include both the user ID and an expiration timestamp. Now the server does two checks during authentication:

  1. Is the signature valid?
  2. Is the token’s expiration time still in the future?

This simple strategy mimics session management without complex server-side tracking.

import jwt
from datetime import datetime, timedelta, timezone
from time import sleep

SECRET_KEY = "SUPERSECRETKEY"
ALGORITHM = "HS256"

def create_access_token(username: str, expires_in: int = 5):
    now = datetime.now(tz=timezone.utc)
    payload = {
        "sub": username,
        "iat": now,
        "exp": now + timedelta(seconds=expires_in)
    }
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

def validate_token(token: str, leeway: int = 0):
    try:
        return jwt.decode(
            token,
            SECRET_KEY,
            algorithms=[ALGORITHM],
            leeway=leeway
        )
    except jwt.ExpiredSignatureError:
        return "Token expired"
    except Exception as e:
        return f"Invalid token: {e}"

print("=== Valid Token Test ===")
valid_token = create_short_lived_token("johndoe", expires_in=10)
print("Validation result:", validate_token(valid_token))

print("\n=== Expired Token Test ===")
expired_token = create_short_lived_token("johndoe", expires_in=2)
sleep(3)  # Wait for token to expire
print("Without leeway:", validate_token(expired_token))
print("With 5s leeway:", validate_token(expired_token, leeway=5))

While expiration timestamps work for simple apps, production systems often pair short-lived tokens with refresh tokens (long-lived but revocable). This hybrid approach reduces frequent re-authentication while limiting exposure if a token is compromised. Even these advanced systems still rely on the core concept: time-bound validation to balance security and usability.