Handling sessions with JWTs

Jan 15, 2025

Authentication can feel overwhelming. There are countless libraries and services promising secure solutions. Even today, most web apps use JSON Web Tokens (JWTs) for authentication. JWTs work like digital ID cards. The server creates a cryptographically encoded string that the client stores and sends back as an auth token in future requests. These tokens contain verified information, like user IDs, that the server can quickly validate without hitting the database every time.

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: Use 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 are fine for simple apps, production systems often use short-lived tokens along with refresh tokens that last longer but can be revoked. This setup reduces the need for users to re-authenticate frequently while limiting risk if a token gets compromised. Even with this added complexity, the core idea stays the same: time-bound validation to balance security and usability.