Authentication Deep Dive

Sessions, JWT, OAuth2, OpenID Connect, Refresh Tokens

Introduction

Authentication answers the question: "Who are you?" It is the first line of access control for every application. This lesson covers three major authentication patterns, session-based, token-based (JWT), and delegated (OAuth2), with Python code for each. You'll implement JWT token creation, validation, and refresh flows, and understand when to choose each pattern.

Session-Based Authentication

How It Works
  1. User submits username + password
  2. Server verifies credentials
  3. Server creates a session in storage (DB/Redis)
  4. Server returns a session ID cookie
  5. Client sends session ID in every request
  6. Server looks up session ID to get user data
Trade-offs
  • ✅ Instant revocation, delete session from store
  • ✅ Simple mental model
  • ❌ Requires server-side storage
  • ❌ Doesn't scale horizontally without sticky sessions or shared store
  • ❌ CSRF vulnerability if not protected
# session_auth.py, Minimal session management (no framework)
import secrets
import hashlib
from datetime import datetime, timezone, timedelta
from dataclasses import dataclass, field

@dataclass
class Session:
    user_id: int
    created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
    expires_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc) + timedelta(hours=24))

    def is_expired(self) -> bool:
        return datetime.now(timezone.utc) > self.expires_at

# In-memory session store (use Redis in production)
SESSION_STORE: dict[str, Session] = {}

def create_session(user_id: int) -> str:
    """Create a new session and return the session token."""
    token = secrets.token_urlsafe(32)   # 256-bit random token
    SESSION_STORE[token] = Session(user_id=user_id)
    return token

def get_session(token: str) -> Session | None:
    """Validate a session token and return the session."""
    session = SESSION_STORE.get(token)
    if session is None or session.is_expired():
        SESSION_STORE.pop(token, None)
        return None
    return session

def invalidate_session(token: str) -> None:
    """Logout: delete the session."""
    SESSION_STORE.pop(token, None)

# Simulation
token = create_session(user_id=42)
print(f"Session token: {token[:16]}...")

session = get_session(token)
print(f"User ID: {session.user_id}")
print(f"Expires: {session.expires_at.strftime('%H:%M %Z')}")

invalidate_session(token)
print(f"After logout: {get_session(token)}")   # None

JWT, JSON Web Tokens

JWTs are self-contained tokens that encode the user's identity and claims directly in the token. The server verifies the signature without needing to look anything up in a database.

JWT Structure
header.b64.payload.b64.signature.b64
Header: algorithm + type  | Payload: claims (sub, exp, iat, roles)  | Signature: HMAC-SHA256 or RS256

Create & Sign JWT

Install: pip install PyJWT cryptography

# jwt_create.py
import jwt
import secrets
from datetime import datetime, timezone, timedelta

# For symmetric signing (HS256), shared secret
SECRET_KEY = secrets.token_hex(32)

def create_access_token(user_id: int, roles: list[str]) -> str:
    """Create a short-lived access token."""
    now = datetime.now(timezone.utc)
    payload = {
        "sub": str(user_id),          # Subject (user ID)
        "iat": now,                    # Issued at
        "exp": now + timedelta(minutes=15),  # Expires in 15 min
        "roles": roles,
        "type": "access",
    }
    return jwt.encode(payload, SECRET_KEY, algorithm="HS256")

def create_refresh_token(user_id: int) -> str:
    """Create a long-lived refresh token."""
    now = datetime.now(timezone.utc)
    payload = {
        "sub": str(user_id),
        "iat": now,
        "exp": now + timedelta(days=30),
        "type": "refresh",
    }
    return jwt.encode(payload, SECRET_KEY, algorithm="HS256")

access_token = create_access_token(42, roles=["user", "admin"])
refresh_token = create_refresh_token(42)

print(f"Access token:  {access_token[:50]}...")
print(f"Refresh token: {refresh_token[:50]}...")
Expected Output:
Access token:  eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0M...
Refresh token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0M...

Verify JWT

# jwt_verify.py, continued
def verify_token(token: str, expected_type: str = "access") -> dict | None:
    """
    Verify a JWT. Returns payload if valid, None if invalid.
    Raises are caught internally, never let JWT errors leak to clients.
    """
    try:
        payload = jwt.decode(
            token,
            SECRET_KEY,
            algorithms=["HS256"],
            options={"require": ["exp", "iat", "sub"]},
        )
        if payload.get("type") != expected_type:
            return None
        return payload
    except jwt.ExpiredSignatureError:
        print("Token expired")
        return None
    except jwt.InvalidTokenError as e:
        print(f"Invalid token: {e}")
        return None

# Verify the access token
payload = verify_token(access_token, "access")
if payload:
    print(f"Authenticated: user_id={payload['sub']}, roles={payload['roles']}")

# Tampered token, signature verification FAILS
tampered = access_token[:-10] + "AAAAAAAAAA"
result = verify_token(tampered)
print(f"Tampered token: {result}")   # None

# Algorithm confusion attack, always specify algorithms explicitly
try:
    jwt.decode(access_token, SECRET_KEY, algorithms=["none"])
except Exception as e:
    print(f"Algorithm=none rejected: {type(e).__name__}")
Expected Output:
Authenticated: user_id=42, roles=['user', 'admin']
Invalid token: Signature verification failed
Tampered token: None
Algorithm=none rejected: InvalidAlgorithmError

Token Refresh Pattern

# token_refresh.py, Token refresh + revocation via blocklist
import time

# In production: use Redis SET with TTL for the blocklist
REFRESH_TOKEN_BLOCKLIST: set[str] = set()

def refresh_access_token(refresh_token: str) -> str | None:
    """Exchange a valid refresh token for a new access token."""
    # Check blocklist first (revoked tokens)
    if refresh_token in REFRESH_TOKEN_BLOCKLIST:
        print("Refresh token revoked")
        return None

    payload = verify_token(refresh_token, expected_type="refresh")
    if payload is None:
        return None

    user_id = int(payload["sub"])
    # In production: look up user roles from database
    return create_access_token(user_id, roles=["user"])

def logout(refresh_token: str) -> None:
    """Revoke a refresh token."""
    REFRESH_TOKEN_BLOCKLIST.add(refresh_token)

# Simulate refresh flow
new_access = refresh_access_token(refresh_token)
print(f"New access token: {new_access[:40] if new_access else None}...")

# Logout, revoke refresh token
logout(refresh_token)
result = refresh_access_token(refresh_token)
print(f"After logout: {result}")   # None
Expected Output:
Authenticated: user_id=42, roles=['user', 'admin']
New access token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...
Refresh token revoked
After logout: None

OAuth2 & OpenID Connect

Grant TypeUse CaseInvolves Human?
Authorization Code + PKCEWeb/mobile apps accessing APIs on behalf of users✅ Yes (browser redirect)
Client CredentialsMachine-to-machine (service accounts, cron jobs)❌ No
Device CodeSmart TVs, CLI tools with limited input✅ Out-of-band
Refresh TokenObtaining new access tokens silently❌ No

Install: pip install requests

# oauth2_client_credentials.py, Machine-to-machine auth
import requests

def get_oauth2_token(
    token_url: str,
    client_id: str,
    client_secret: str,
    scope: str = "",
) -> dict:
    """
    OAuth2 Client Credentials flow.
    No user interaction required, used for service accounts.
    """
    response = requests.post(
        token_url,
        data={
            "grant_type": "client_credentials",
            "client_id": client_id,
            "client_secret": client_secret,
            "scope": scope,
        },
        headers={"Content-Type": "application/x-www-form-urlencoded"},
        timeout=10,
    )
    response.raise_for_status()
    return response.json()

def call_api_with_token(api_url: str, access_token: str) -> dict:
    """Call a protected API using the Bearer token."""
    response = requests.get(
        api_url,
        headers={"Authorization": f"Bearer {access_token}"},
        timeout=10,
    )
    response.raise_for_status()
    return response.json()

# Example usage (requires a real OAuth2 server)
# token_data = get_oauth2_token(
#     token_url="https://auth.example.com/oauth/token",
#     client_id="my-service",
#     client_secret="secret-from-secrets-manager",
#     scope="read:data write:data",
# )
# print(f"Access token type: {token_data['token_type']}")
# print(f"Expires in: {token_data['expires_in']} seconds")
# result = call_api_with_token("https://api.example.com/data", token_data["access_token"])

print("OAuth2 Client Credentials pattern: POST grant_type=client_credentials")
print("Token stored in memory (not disk), re-fetched when expired")
print()
print("OpenID Connect adds:")
print("  • id_token (JWT with user identity)")
print("  • /userinfo endpoint for profile data")
print("  • Discovery endpoint (.well-known/openid-configuration)")
Expected Output:
OAuth2 Client Credentials pattern: POST grant_type=client_credentials
Token stored in memory (not disk), re-fetched when expired

OpenID Connect adds:
  • id_token (JWT with user identity)
  • /userinfo endpoint for profile data
  • Discovery endpoint (.well-known/openid-configuration)

Key Takeaways

  • Sessions require server storage, easy revocation, but requires shared session store for horizontal scaling
  • JWTs are stateless, no server lookup required, but revocation is complex (use short expiry + refresh tokens)
  • Always specify algorithms explicitly, algorithms=["HS256"] prevents algorithm confusion attacks
  • Access tokens should be short-lived, 15 minutes is typical; use refresh tokens for continuity
  • OAuth2 is for delegation, "let this app act on your behalf"; not a direct authentication protocol
  • OpenID Connect adds identity to OAuth2, the id_token proves who the user is
What's Next?

Lesson 11 answers the follow-up question: once you know who someone is, what are they allowed to do?

  • RBAC, Role-Based Access Control with Python decorators
  • ABAC, Attribute-Based Access Control for fine-grained policies
  • OPA, Open Policy Agent as a policy engine