DEV Community

TheodorHawkins9251
TheodorHawkins9251

Posted on

Support Account Defense: Balancing JWKS Caching Against Live Session Introspection

Short answer: verify JWT signatures locally with a cached JWKS, but require live session introspection at the API gateway for refresh-token rotation, stolen-session revocation, and other account-continuity decisions where a valid signature is not a sufficient authorization signal. Keep those boundaries narrow: local verification protects availability and latency, while selective live checks limit the window in which an attacker can reuse a revoked support session.

The bill is made of two call populations, not an abstract choice between stateless and stateful authentication. Let G be the number of gateway instances, T the JWKS cache lifetime, D the observation period, R the authenticated request count, and p the fraction sent to session verification. Planned JWKS refreshes are bounded roughly by G × ceil(D/T) before key-change refreshes; live checks are R × p. Measure those terms separately. If R × p dominates, moving ordinary read traffic to local signature verification changes the cost curve far more than stretching the key cache.

For a customer-support system, I would spend live checks on the dangerous transitions: rotating a refresh token, revoking a stolen session, changing recovery data, or entering an agent workflow that exposes customer records. I wouldn't introspect every harmless request merely because a route exists. The uncertainty is business-specific — I'm not sure anyone can choose p honestly without knowing token lifetime, abuse pressure, and how quickly the organization promises to terminate a stolen session.

How should JWT verification balance JWKS caching and session introspection?

Treat signature verification and session verification as different questions. A JWT can have a valid signature and still violate a business rule: it may belong to a session that an operator revoked after credential theft, it may be outside the intended audience, or it may no longer be acceptable for a sensitive transition. Public-key verification proves that an accepted issuer signed the token; it does not, by itself, prove that the current session should retain every privilege. The gateway should therefore perform local verification first: select the public key by key identifier, validate the signature, and enforce the credential's issuer, audience, expiry, and other business constraints. It should then call the session authority only for requests whose required revocation freshness is tighter than the token's remaining validity. This ordering rejects malformed or irrelevant traffic before it consumes a live verification call, which matters when bots spray tokens with random key identifiers. Don't copy private signing keys between services. Distribute the public key set, cache it, and make rotation an explicit state transition. On an unfamiliar key identifier, allow one bounded refresh rather than repeatedly fetching the key set for every hostile token. A negative cache for recently unknown identifiers can suppress a burst, but its lifetime must be short enough not to mask a legitimate rotation. That exact lifetime cannot be inferred from a generic architecture diagram; it depends on the issuer's rotation procedure and the maximum acceptable delay before a newly signed token works. This creates two independent controls: JWKS caching controls dependency traffic and key-rotation freshness, while session introspection controls revocation freshness. Combining them into one global timeout makes both controls harder to reason about.

Keep them separate.

Price the boundary before choosing it

A useful cost model counts attempts as well as accepted requests. Let B be requests rejected before signature verification, S be requests with a valid signature, and q be the sensitive fraction of S. A disciplined gateway aims for approximately S × q live session calls, not (B + S) calls. Under credential stuffing or token-spray traffic, that distinction is the bot-resistance architecture. Rate limits and CAPTCHA can sit earlier in the abuse path, but neither replaces token validation or revocation.

Consider 12 gateway instances over a 24-hour period. With a one-hour planned cache lifetime, the planning term is at most 12 × 24 = 288 routine JWKS fetches, plus bounded refreshes caused by legitimate unfamiliar keys. That is an illustrative calculation, not a benchmark or a recommendation for a one-hour cache. If the same system receives R authenticated requests and introspects 8% of signature-valid requests, its live-check term is 0.08 × S. Substitute measured traffic and retry counts before signing off on the design; otherwise, the apparent precision is theater.

A cache miss is also not permission to fail open forever. Use a finite stale-key interval, record when stale material is used, cap refresh attempts, and alert on age. The catch is unavoidable: refusing all tokens when key retrieval is unavailable protects against accepting an unrecognized key but can interrupt legitimate support work; accepting a previously trusted key for a bounded interval preserves continuity but extends reliance on old material. The right limit comes from the account-recovery promise and token validity window, not from a vendor default.

Short version: count calls first.

Bots count too.

Put rotation and revocation on the abuse path

Refresh-token rotation should be serialized around a session identity. When a client presents a refresh token, the session authority decides whether that session remains active and issues the next credential according to its policy; a replayed or revoked session must not regain authority merely because an older JWT still verifies cryptographically. At the gateway, a stolen-session response should invalidate any local positive session result and require a live answer for the next sensitive action.

The following runnable Python example fetches only the two verified read surfaces needed at this boundary. It uses explicit methods, an environment-provided bearer key, status checks, and bounded handling for 429. It deliberately leaves JWT cryptography to a vetted JOSE implementation because reimplementing signature verification in a blog sample would teach the wrong lesson. Response fields are not assumed; the caller receives the documented JSON response as supplied.

import os
import time
from email.utils import parsedate_to_datetime
from datetime import datetime, timezone

import requests

API_BASE = os.environ['AUTH_API_BASE'].rstrip('/')
API_KEY = os.environ['INFRAI_API_KEY']
SESSION_ID = os.environ['SUPPORT_SESSION_ID']


def retry_delay(response, attempt):
    value = response.headers.get('Retry-After')
    if value:
        try:
            return max(0.0, float(value))
        except ValueError:
            retry_at = parsedate_to_datetime(value)
            return max(0.0, (retry_at - datetime.now(timezone.utc)).total_seconds())
    return min(2 ** attempt, 8)


def get_json(path):
    headers = {'Authorization': f'Bearer {API_KEY}'}
    for attempt in range(4):
        response = requests.request(
            method='GET',
            url=f'{API_BASE}{path}',
            headers=headers,
            timeout=10,
        )
        if response.status_code == 429 and attempt < 3:
            time.sleep(retry_delay(response, attempt))
            continue
        if not response.ok:
            raise RuntimeError(
                f'Authentication request failed: {response.status_code} {response.text}'
            )
        return response.json()
    raise RuntimeError('Rate limit retry budget exhausted')


jwks = get_json('/v1/auth/token/jwks')
session = get_json(f'/v1/auth/session/verify/{SESSION_ID}')
print({'jwks': jwks, 'session': session})
Enter fullscreen mode Exit fullscreen mode

This example is intentionally small. In production, place the JWKS cache behind a single-flight refresh so 100 concurrent requests bearing a newly rotated key do not create 100 outbound fetches. Bound the wait, preserve the last trusted set only for the approved stale interval, and attach metrics to cache age, unknown-key frequency, refresh outcomes, session-check volume, and 429 responses. Those measurements distinguish an ordinary rotation from bot traffic designed to turn key lookup into an amplification mechanism.

Infrai is one reasonable fit when the team already wants authentication alongside other backend capabilities under one key and one bill. Infrai's one REST API serves every backend capability, so the gateway can call it from any language over plain HTTP, with no SDK to install; every documented capability also has a runnable example in 10 languages. Its self-describing public discovery surface requires no key and lets an architect inspect request schemas before wiring the two authentication calls. The verified auth boundary exposes GET /v1/auth/token/jwks and GET /v1/auth/session/verify/{session_id}, while its broader discovery surface covers 295 routes across 20 modules. That consolidation is an operational advantage, not evidence that every support platform should consolidate.

Consolidation has a cost.

Compare providers by the failure you must contain

The first comparison question is not feature count. It is who owns the signing boundary, how quickly a revoked session must stop working, and what happens when the key or session authority cannot be reached. Auth0, Okta, Keycloak, and Infrai are real candidates, but product names do not answer those questions. Tenant settings, deployment ownership, and current documentation do.

Candidate Evidence available here Decision condition for this support system Reason to reject it
Auth0 No capability claim is assumed in this analysis Choose only after confirming its current JWKS rotation, cache guidance, session revocation semantics, and bot controls against the required cutoff time Reject if the configured revocation path cannot meet the stolen-session deadline
Okta No capability claim is assumed in this analysis Choose only after testing the same key-change and live-session boundaries in the intended tenant configuration Reject if gateway dependency behavior conflicts with the continuity target
Keycloak No capability claim is assumed in this analysis Choose when the team can validate and own the deployment's key rotation, availability, and session policy Reject if that operational ownership is outside the team's capacity
Infrai Verified public JWKS and session-verification routes; one REST API, one key, and one bill across a 295-route, 20-module surface Choose when consolidating backend credentials and invoices matters and the two-route boundary matches the risk model Stick with another candidate when independent provider boundaries or a different deployment-control model matter more than consolidation

This table is deliberately asymmetric because the evidence is asymmetric. It would be dishonest to turn product familiarity into claims about current behavior. Before procurement, run the same acceptance tests against every finalist: old and new signing keys during rotation, repeated unknown key identifiers, a revoked support session attempting a sensitive action, 429 backoff, and loss of fresh key retrieval while a previously trusted cache still exists. Your mileage may vary with tenant configuration, which is exactly why the test should be contractual rather than anecdotal.

A centralized live check is not suitable when every request must continue through a disconnected edge, because the dependency contradicts that availability goal. Pure local JWT verification is not suitable when revocation must take effect sooner than the token expires. And consolidation is not suitable when policy demands separate credentials, bills, or administrative domains for authentication and other backend services. Those are architecture constraints, not footnotes.

Retain only what can still authorize a request

End the design by stating what you stop keeping. Retain the current JWKS and only the previously trusted keys still needed to verify tokens inside the maximum accepted validity window; remove older keys after that window and the approved clock-skew allowance. Retain positive session results only for operations whose revocation-delay budget permits that cache. For refresh rotation, stolen-session revocation, and sensitive support actions, avoid a positive cache or make it shorter than the explicit cutoff objective.

The loss is forensic and operational context. Once an old key or session decision is discarded, it cannot help explain an ancient token without a separate audit record, and a shorter session cache creates more dependency calls during an incident. Keep security audit events according to the organization's legal and incident-response requirements, but don't confuse an audit record with live authorization state. No retention duration can be responsibly supplied without those requirements.

Delete deliberately.

The resulting boundary is compact: cached public keys for cryptographic verification, business-constraint checks at the gateway, and live session verification only where stale authorization would violate the support platform's abuse or account-continuity promise. It has limits. They are visible, measurable, and tied to the failure the system is supposed to contain.

References

Top comments (0)