C.W.K.
Stream
Lesson 02 of 06 · published

The Middleware — Where Every Request Gets Inspected

~15 min · middleware, fastapi, starlette

Level 0Greenhorn
0 XP0/53 lessons0/14 achievements
0/100 XP to next level100 XP to go0% complete

Middleware is the entry point for every request. Putting auth here — not in per-route decorators — means you cannot forget it on a new route. New endpoints inherit the gate by default; making them public requires an explicit opt-in.

The check order matters

Public-first (cheap, no DB hits), then the killswitch (PIN disabled), then bypass (cheap), then blacklist (one DB hit, returns immediately on hit), then session check (slowest), then login redirect. A 403 for a known-bad IP costs less than a session lookup.

Getting the real client IP

Behind a reverse proxy, request.client.host shows the proxy's IP, not the actual client. You need the X-Forwarded-For header — but only trust it from a known proxy.

Code

FastAPI / Starlette PIN auth middleware·python
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import RedirectResponse, Response

PUBLIC_PREFIXES = ("/login", "/static", "/health")
LOCAL_BYPASS    = {"127.0.0.1", "::1"}      # extend with trusted LAN IPs

class PinAuthMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        path = request.url.path
        ip   = client_ip(request)

        # 1. Public routes — always allowed
        if any(path.startswith(p) for p in PUBLIC_PREFIXES):
            return await call_next(request)

        # 2. PIN disabled globally? Bypass everything (use carefully!)
        if not config_pin_enabled():
            return await call_next(request)

        # 3. Loopback / explicitly-trusted local IPs bypass auth
        if ip in LOCAL_BYPASS:
            return await call_next(request)

        # 4. Hard block: blacklisted IPs get 403, no attempt recorded
        if is_blacklisted(ip):
            return Response("Forbidden", status_code=403)

        # 5. Valid session cookie? Attach user, pass through
        token = request.cookies.get("session")
        if token and session_valid(token, ip):
            request.state.authed = True
            return await call_next(request)

        # 6. Unauthenticated — send to login (HTML) or 401 (API)
        if request.headers.get("accept", "").startswith("text/html"):
            return RedirectResponse("/login", status_code=302)
        return Response("Unauthorized", status_code=401)
Real client IP behind a trusted proxy only·python
TRUSTED_PROXIES = {"127.0.0.1", "::1"}

def client_ip(request) -> str:
    direct = request.client.host
    if direct in TRUSTED_PROXIES:
        xff = request.headers.get("x-forwarded-for", "")
        if xff:
            return xff.split(",")[0].strip()
    return direct

External links

Exercise

Drop the PinAuthMiddleware (without yet implementing the helpers) into a tiny FastAPI app that has one public route /health and one protected route /. Verify: GET /health returns 200, GET / returns 302 to /login. The control flow should pass without auth helpers (we wire those next).

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.