Caching a JWKS correctly when you verify sessions per request

Four ways JWKS caching goes wrong in production, the revocation window it creates, and a Python verifier that handles key rotation without a fetch storm.

Verifying an Infrai session token on every request should cost you nothing over the wire. GET /v1/auth/token/jwks publishes one Ed25519 public key; you cache it, verify the EdDSA signature locally, and never call the platform in your hot path. The interesting engineering is entirely in the caching, because each of the obvious implementations is wrong in a different way.

Here’s the key set as it actually comes back:

curl -sS "https://api.infrai.cc/v1/auth/token/jwks" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"
{
  "ok": true,
  "data": {
    "keys": [
      {
        "kty": "OKP",
        "crv": "Ed25519",
        "use": "sig",
        "alg": "EdDSA",
        "kid": "infrai-auth-ed25519-v1",
        "x": "KlELlwmJ87lR-5UPJ2JaagXal0Zo87THwPusrfKsKg4"
      }
    ]
  }
}

Note the kid. It ends in -v1, which tells you the platform expects to rotate one day, and a verifier that ignores kid is a verifier that breaks on that day.

The four failure modes

Fetching per request. You turned an offline check into a network round trip, added a hard dependency on one more service being up, and made your p99 someone else’s p99. It usually ships because it passes every test.

Caching forever. Fast until the key rotates, then every token fails at once and the fix requires a deploy.

Caching by URL but not by kid. Subtler: you hold the old key, a token signed with the new one arrives, verification fails, and you log “invalid session” for a token that is perfectly valid.

Refetching on every failure. Now a script throwing junk tokens at you becomes a fetch storm against the key endpoint, and you rate-limit yourself out of verifying real users.

The shape that survives all four: cache the key set, refetch only when a token presents an unknown kid, and put a cooldown on refetches.

A verifier that does that

import os
import threading
import time

import requests
from jose import jwt
from jose.exceptions import JWTError

API = "https://api.infrai.cc"
KEY = os.environ["INFRAI_API_KEY"]
ISSUER = "infrai-auth"
COOLDOWN_SECONDS = 600

_lock = threading.Lock()
_keys: dict[str, dict] = {}
_last_fetch = 0.0


def _fetch_jwks() -> None:
    """Refresh the cached key set. Guarded by a cooldown so a burst of tokens
    carrying an unknown kid cannot turn into a burst of fetches."""
    global _last_fetch
    with _lock:
        if time.monotonic() - _last_fetch < COOLDOWN_SECONDS and _keys:
            return
        resp = requests.get(
            f"{API}/v1/auth/token/jwks",
            headers={"Authorization": f"Bearer {KEY}"},
            timeout=5,
        )
        resp.raise_for_status()
        for jwk in resp.json()["data"]["keys"]:
            _keys[jwk["kid"]] = jwk
        _last_fetch = time.monotonic()


def verify_access_token(token: str) -> dict:
    """Return the token claims, or raise JWTError. No network call in the
    common case."""
    header = jwt.get_unverified_header(token)
    kid = header.get("kid")
    if kid not in _keys:
        _fetch_jwks()
    jwk = _keys.get(kid)
    if jwk is None:
        raise JWTError(f"unknown signing key {kid}")
    return jwt.decode(
        token,
        jwk,
        algorithms=["EdDSA"],
        issuer=ISSUER,
        options={"leeway": 5},
    )


if __name__ == "__main__":
    _fetch_jwks()
    print(f"cached kids: {sorted(_keys)}")

Five seconds of leeway covers ordinary clock drift between your host and the signer. Without it you’ll see sporadic failures on freshly-minted tokens that look like a bug in the platform and are actually NTP.

The revocation window you just accepted

Offline verification cannot know about a revocation. Between the moment you revoke and the moment the access token expires, that token still verifies — that’s arithmetic, not a defect, and every JWT-based system has it.

Two levers close the gap.

Short access tokens shrink the window to the token’s remaining life. And for the operations where a stale session is genuinely dangerous — changing a password, moving money, deleting a workspace — spend the round trip:

curl -sS "https://api.infrai.cc/v1/auth/session/verify/au_ses_7Uu2kQxWvR4mBn8dTcYs" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"
{
  "ok": true,
  "data": {
    "session_id": "au_ses_7Uu2kQxWvR4mBn8dTcYs",
    "user_id": "au_usr_lMmXGySJeGVM1xicqHGJaJbB",
    "started_at": "2026-09-21T02:24:30Z",
    "expires_at": "2026-09-28T02:24:30Z",
    "ip": "203.0.113.24",
    "ua": "Mozilla/5.0",
    "mfa_factor": null
  }
}

The ip and ua captured at sign-in are useful beyond verification: a session created from a different continent than the request you’re serving is worth a second factor.

CheckCost per requestSees a revocationUse for
Cached JWKS, local verify~microsecondsafter token expiryevery API request
GET /v1/auth/session/verify/{id}one round tripimmediatelydestructive or paid actions
GET /v1/auth/session/list_for_user/{id}one round tripimmediatelydevice list, admin views

Showing a user their sessions

The same read powers the “where you’re signed in” screen that users now expect:

curl -sS "https://api.infrai.cc/v1/auth/session/list_for_user/au_usr_lMmXGySJeGVM1xicqHGJaJbB" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"

items carries one entry per live session, each with its own ip, ua and timestamps. Pair it with POST /v1/auth/session/revoke/{session_id} and you’ve built device management out of two calls.

Limitations worth knowing

One algorithm, one key, no per-tenant signing keys: you can’t hand a customer their own key material, and if your compliance story requires that, you’d be better off with a product built around it — SuperTokens, self-hosted, is the usual answer. There’s also no client-side SDK here, so token storage and refresh scheduling in the browser are yours to write.

What stays on your side of the ledger is everything adjacent. The failed-verification counter you’ll want goes to POST /v1/metrics/report on the same key; the alert when it spikes goes through POST /v1/email/send. No second account, no second invoice.

These auth reads report billing_class: free in discovery, so verification isn’t metered per call at all — identity is billed per monthly active user, the way Auth0 and Clerk bill. GET /v1/account/usage shows what your account actually accrued; read it there (verified 2026-09-21) instead of trusting a published figure, and expect rates to drift downward over time.

References

Browse more auth developer guides