Srikanth Bandaru

Your auth token is a cache

You’ve banned a user, so why do her requests still work?

Contents6 sections
  1. Ban a user and count her requests
  2. Every scheme caches the same decision
  3. Which way does each scheme fail?
  4. Allowlists fail closed, denylists fail open
  5. Push to the holders you can list
  6. Choose per endpoint

A login token is a saved copy of the server’s decision to let one person in. That makes it a cache, and taking access away is cache invalidation. So the questions are who holds copies, how stale they may get, and which way they fail when the source is unreachable.

Ban a user and count her requests

Say you have to ban Mallory (security writing’s usual name for an attacker), who is scraping your API at 1 request a second. Her client holds a JWT (JSON Web Token), a signed token saying who she is and when it expires. It has 11 minutes left. Every API server checks a denylist of revoked token IDs (the token’s jti field). The list is written in us-east and copied to eu-west, where Mallory’s server runs.

Ban Mallory

Model: assumed network numbers, 1 request/s

Guess first, then press Ban Mallory.

How many of Mallory’s requests get through after the ban?

  1. us-east denylistban written at 0 s
  2. eu-west region replicareceives it after the lag
  3. eu-west server memorythe copy each request checks

Healthy link

Mallory’s requests after the ban appear here, one tick per second.

0 requests accepted after the ban

No ban yet. Mallory’s token has 11 minutes left.

The 3 s replication lag, the 30 s refresh interval and the 1 request per second are the model’s assumptions, not measurements.

Every server checked the denylist, but each one checked its own copy. A new entry first has to cross to eu-west, then wait for that server’s next refresh. Mallory gets in the whole time.

Now cut the link between regions. The check times out and fails open, which means it lets the request through. Nobody wants a denylist outage to become a login outage. So Mallory gets every request until her token expires, as if there were no denylist.

Every scheme caches the same decision

A JWT carries the decision inside each request. Its time to live (TTL), how long the copy counts as fresh, is its expiry minus its issue time. A session ID is only a key, and the decision lives in a session store. Revoking access is a write to the origin, the source of truth.

A 2016 blog post by joepie91 and a 2021 post by Thomas Ptacek both made this point. Each common fix is a cache pattern with a known way to fail.

Scheme Cache pattern Fails when
Session lookup on every request No cache Every request pays a store read, and a network split locks everyone out
Short-lived token, renewed often Short TTL, then ask the origin again You need revocation faster than the TTL
Token plus a per-user “valid after” time Version check You need to kill 1 leaked token and keep the others
Token plus jti denylist Negative cache (a cache of “no” answers) Entries are evicted early, or lag lets a banned user through
Long token plus pushed revocations Long TTL plus purge (copies deleted on demand) A holder misses an event

A cached decision also has 2 limits. The TTL limits when it counts, and the cache key limits where. For a token, the where is its audience, the services it was issued for (the aud field).

I learned the second limit from a response cache, not a token. In March 2026 I was working on a platform that serves several countries’ customer sites from one app. After a review finding on my own pull request, I changed how it picks the country for each request. It now trusts only values the server sets. I also added a cache-keying rule, so a response cached for one country is never served to another.

Which way does each scheme fail?

Here is the ban against 4 schemes, with fail-open checks. “Other users” means everyone else on eu-west.

Scheme Mallory, healthy link Other users, link cut
Token only 660 Served
Token + denylist 23 Served
Session lookup 0 All rejected
Long token + push 3 Served

A token with nothing to check lets her use all of it. Push delivers the ban in the model’s 3 seconds, so 3 requests get through. Session lookup wins on a healthy network but reads the store, in us-east, on every request.

Cut the link and every scheme except session lookup lets Mallory use her whole token. Session lookup stops her, but it also rejects every other eu-west user, since a session ID carries nothing to fall back on.

Only a scheme with a local copy of the claims can choose how it fails. Ptacek calls working through a network split “the primary benefit of statelessness,” and a revocation check gives it up.

Allowlists fail closed, denylists fail open

A session store is an allowlist, so a request passes only if its session is found. A denylist is the reverse, and a delay hurts each one differently. A session that hasn’t replicated yet gives a real user a 401 (“not signed in”), which is a support ticket. A ban that hasn’t replicated yet gives Mallory a 200 (success), which is an incident.

Eviction, when a full cache drops old entries, splits the same way. An evicted session logs someone out. An evicted denylist entry brings a revoked token back. So keep each entry until its token expires.

Push to the holders you can list

Microsoft’s identity service, Entra ID, can push revocation events to its own services, and then its tokens last up to 28 hours. Because it can list every holder, it uses a long TTL plus purge. So push revocations to the holders you can list, and keep TTLs short for the rest.

Choose per endpoint

You don’t need one scheme for the whole system. Most reads can live with 30 seconds of staleness, so a token plus a denylist is fine. Privileged writes and payments can’t, and they’re rare enough to afford a session lookup. Mallory’s scraper still gets its 23 reads, but her first privileged write hits the store, which already says no.