Est.

OAuth 2.0 Token Management at Scale

When OAuth tokens scale beyond tutorials, token rotation and storage become security imperatives.

Contributing Editor · · 12 min read
Cover illustration for “OAuth 2.0 Token Management at Scale”
Auth and Security · August 22, 2026 · 12 min read · 2,640 words

OAuth 2.0 tokens work fine until they don't, and the breaking point is almost always scale. Every getting-started tutorial teaches you one authorization server, one resource server, one client. Real systems have five to ten services checking the same token on a single user request, and that's not some rare edge case. It's just what a modern API stack looks like on a Tuesday.

This piece walks through what actually breaks when you scale past the tutorial: token structure, rotation, storage, validation, revocation, and scope design. No padding, no theory for theory's sake. Just the failure modes and the fixes, in the order you'll hit them.

What the 2025 standards update (RFC 9700 and OAuth 2.1) actually requires engineers to change

RFC 9700 came out in January 2025. It's the IETF's current best-practice doc for OAuth 2.0 security, and it replaces the old threat model from RFCs 6749, 6750, and 6819 with lessons learned from over a decade of things going wrong in production. OAuth 2.1 (the draft, still at draft-ietf-oauth-v2-1-15 as of April 2025) hasn't been finalized as its own RFC yet, but its rules are stable enough that major auth servers and libraries have already built to them. Waiting for the official stamp at this point is a bit like waiting for a movie to hit theaters when you already watched the leaked cut.

Five things changed that you need to check against what's actually running in your stack:

PKCE used to be for public clients only (think mobile apps, single-page apps). Now it's required for everyone, confidential clients included. The implicit grant and the Resource Owner Password Credentials grant are both gone. If you've got a legacy service still doing ROPC because "it was simpler," that's now a finding in your next audit, not a shortcut.

Refresh token rotation is required, and RFC 9700 specifically calls out sender-constraining or rotation as a MUST for public clients. Redirect URIs need exact matching now, prefix matching doesn't fly anymore. Bearer tokens can't live in URL query strings, either; they belong in the Authorization header or the POST body, full stop. Query string tokens end up in server logs, browser history, and referrer headers, which is a strange place to keep something that unlocks a user's account.

Worth noting: OAuth 2.1 has already been adopted as a foundational part of the Model Context Protocol authorization spec. That's a signal this draft isn't just a web app concern anymore, it's shaping how AI agents authenticate too.

The audit checklist here is simple: go through each of the five items, find what you're running today, and mark what needs to change. Most teams find at least two.

How access token and refresh token lifetimes should be set relative to the sensitivity of what they protect

The whole point of splitting access tokens from refresh tokens is exposure control. Access tokens are short-lived so a leaked one doesn't do much damage. Refresh tokens live longer so the user doesn't have to log in every ten minutes.

RFC 9700 gives rough tiers based on sensitivity. For financial services, healthcare, anything regulated: access tokens should run 5 to 15 minutes, refresh tokens no more than 7 to 30 days. For general-purpose APIs, you can stretch access tokens to 30 or 60 minutes without much added risk.

Here's the part that trips people up during incident response. Refresh tokens sit outside your SSO and MFA layer entirely. A user resets their password, feels good about it, and meanwhile their old refresh tokens are still sitting there, perfectly valid, waiting to be used. Password resets alone don't kill them. If your incident playbook stops at "user changed password," you've left the back door unlocked while congratulating yourself on locking the front one.

That gap, tokens outliving the credentials that were supposed to protect them, is exactly why rotation isn't optional once you're operating at any real scale. It's the next section for a reason.

Refresh token rotation at scale: race conditions, reuse detection, and provider fragmentation

Rotation is straightforward in concept: every time a refresh token gets used, the server issues a new one and kills the old one. A stolen token is good for exactly one use before someone notices.

That "someone notices" part matters. Reuse detection turns rotation into an actual security signal instead of just a bookkeeping rule. If an already-rotated token gets used a second time, that's not a fluke, that's evidence someone else has a copy. The right response is to revoke the entire token family (every token tied to that session) and fire an alert to the security team. Just rejecting the second call and moving on is like catching a burglar in your kitchen and just asking them to leave through the front door.

There's also the refresh storm problem, and this one is scale-specific. Picture five workers behind a load balancer, each noticing the access token expired at roughly the same moment. All five try to refresh simultaneously. Most providers issue a fresh refresh token on every refresh call, so the first worker to land wins, and the other four just torched a valid token and are now holding one that's already dead. The fix is either a distributed lock (Redis or ZooKeeper, held for the duration of the refresh call) or a centralized credential service that serializes refresh calls and hands the new token out to everyone waiting.

Providers make it worse, too, each in their own special way. RFC 6749 section 6 describes the refresh grant in about four sentences. Everything else, lifetime, rotation behavior, what happens on reuse, per-account limits, is up to the provider. HubSpot tokens expire in 30 minutes. Salesforce tokens last longer but an admin can kill them whenever. Some providers don't even bother returning expiresin. Zendesk is rolling out default expiration for OAuth tokens on global clients starting February 2, 2026, with local clients following by April 1, 2027, so a policy that's stable today might not be stable next year. Treat provider token behavior as something you observe and probe for, not something you assume. Check for expiresin, handle it being missing, and actually read the provider changelogs once in a while.

Token storage architecture: what changes when millions of tokens need to be secured, queried, and expired

Storage is where a lot of scale problems are born, quietly, long before anyone notices. Bad token storage practices are a leading cause of token theft in web apps, according to a 2024 vendor report.

Different token types need different homes. Access tokens are short-lived and read constantly, so an in-memory cache per service, or a shared Redis cache, works fine; you often don't need a persistent store at all if the lifetime is short enough. Refresh tokens need to survive a service restart, so they go in a database, encrypted at rest, with a separate key from whatever you use elsewhere. Never in local storage, never in a cookie without httpOnly, Secure, and SameSite all set. Token families, the ones you're tracking for reuse detection, need a store that's consistently readable the moment you write to it. An eventually-consistent cache is useless here: if a reuse check misses a revocation that happened a second ago, the whole point of reuse detection evaporates.

On the encryption side: store refresh tokens as hashed or encrypted blobs, never as plaintext. If your database gets breached, the attacker should walk away with garbage, not usable tokens. Keep encryption key rotation separate from token rotation, too, they're related but they're not the same operation and conflating them causes headaches.

One more thing people get wrong: relying on TTL alone for expiry. A token with a TTL will happily stay valid until it naturally times out, even after you've decided to revoke it. Your storage layer needs to support explicit invalidation, by token ID or by revoking a whole user or session family, not just "wait it out."

If you're running multi-tenant SaaS, token queries need to be scoped by tenant, too. Otherwise a shared index becomes a quiet little cross-tenant leak waiting to happen.

JWT local validation vs. token introspection: choosing the right validation architecture for the load profile

This is a real tradeoff, not a "just pick the better one" situation. Local JWT validation is fast but blind to revocation. Introspection is authoritative but slower and adds a dependency on the auth server being reachable.

Local validation works like this: the resource server fetches the JWKS endpoint, caches the public keys, and checks the signature and claims entirely in-process. No network call per request. That makes it something like 10 to 20 times faster than introspection, according to benchmark data, and at high volume the marginal cost is basically zero. The catch: a revoked token stays valid locally until it naturally expires. For a 60-minute access token, that's a revocation window most security teams would call unacceptable. The fix is simply keeping sensitive-API access tokens short, 5 to 15 minutes, so the window is small even if it's not zero.

Introspection, defined in RFC 7662, works the opposite way. The resource server calls the authorization server on every request and asks "is this still active?" That adds 15 to 30 milliseconds of latency per call, which sounds small until it's showing up in your p99 at high volume. You need it for opaque tokens (no claims to check locally) and for anything where real-time revocation is a compliance requirement, not a nice-to-have.

Most distributed systems land on a hybrid: local JWT validation for the hot path, with introspection results cached briefly for services that need some revocation awareness without a round trip on every single call. Save full, uncached introspection for the operations where it actually matters: payment initiation, credential changes, admin actions.

A couple of hygiene notes. Cache the whole JWKS key set, not individual keys, and if validation fails against a key ID you recognize, re-fetch once before rejecting the token; that quietly handles authorization server key rotation without an outage. Watch out for clock skew, too: unsynchronized clocks across nodes will reject perfectly valid tokens right at the expiry boundary. The standard fix is a small, explicit grace window applied consistently across every validating service, and the real fix underneath that is just running NTP properly.

Sender-constrained tokens: when mTLS and DPoP are worth the implementation cost

Bearer tokens have one structural flaw: whoever holds the token can use it, no questions asked, until it expires. Binding the token to the client that requested it breaks that property, which is the whole appeal.

There are two mechanisms actually used in production. mTLS binds the token to the client's certificate, and the resource server checks that the certificate presented matches. It's strong, but it needs PKI infrastructure and TLS termination that actually passes the client cert up to the application layer, which is more plumbing than most teams want to build. DPoP is lighter: the client generates a key pair, signs a proof-of-possession JWT with the private key on each request, and the authorization server ties the token to the public key. It runs over plain HTTPS, no PKI needed.

Google's setup is a useful reference point here: they support DPoP binding for refresh tokens, but the access tokens issued for Google APIs remain standard Bearer tokens. Even a company running auth infrastructure at that scale doesn't bind everything end to end, they bind what matters most.

DPoP and PKCE aren't doing the same job, even though people sometimes assume one replaces the other. PKCE protects the authorization code during the redirect. DPoP protects the refresh token that comes out the other end, from replay if it's ever stolen. Layer them.

So when's it worth the build? Machine-to-machine flows are the clearest case, service accounts, background workers, AI agents, anywhere there's no human around to re-authenticate if something goes wrong. Refresh tokens are the second case, given how long they live and the fact that rotation alone doesn't stop a single-use theft before rotation even runs. Any high-value operation where a stolen token would cost more than the engineering time to prevent it belongs on this list too.

Being honest about the cost: DPoP means managing keys client-side and signing every request, which is overhead. mTLS means managing a certificate lifecycle. Neither one is a drop-in for clients you've already built; budget the time.

Revocation that actually propagates: the gap between issuing a revoke call and all services honoring it

Calling a revocation endpoint feels like flipping a switch. It isn't. It tells the authorization server the token is dead; it says nothing to the ten resource servers that cached it an hour ago or the one that's mid-request with it right now.

There's a rough hierarchy of how fast revocation actually spreads. Short-lived access tokens combined with rotation give you revocation by default, the token just expires before the abuse window matters, and most of the time that's enough. A shared blocklist (Redis, with a TTL matching the token's own lifetime) is the next step up: resource servers check it on each request or on cache miss, adding some latency but propagating within seconds. Beyond that is CAEP, the Continuous Access Evaluation Profile, an IETF standard where the authorization server actively pushes events like "session modified" or "credential changed" to any resource server that's registered to receive them.

CAEP is where things get genuinely fast: suspend an account and every active session can be terminated within seconds instead of waiting around for a token to time out. It plays nicely with local JWT validation too, the resource server keeps validating locally but also holds a CAEP listener that can override the cached decision the moment something changes. Adoption is still early. Microsoft Entra and Okta have it running in production; most custom-built authorization servers don't emit CAEP events yet, so don't assume it's available unless you've checked.

For refresh token families specifically, one compromised token should take down the whole family, every token issued from that same original grant, not just the one that got flagged. That requires tracking families in storage as a first-class thing, not bolting it on after the fact.

Back to the incident response point from earlier: because refresh tokens outlive password resets, your revocation process needs to explicitly find and kill every grant tied to an account. Resetting the password and calling it a day just isn't enough.

Scope and claims design that stays manageable as the number of services grows

Scope creep is a real thing, and it happens slowly enough that nobody notices until the token payload looks like a phone book. Every new service wants its own scope, every scope needs a claim, and two years in you've got 40 scopes and nobody remembers what half of them gate.

The fix that actually holds up is keeping scopes tied to actions, not services. payments:write describes something meaningful about what a token can do. payment-service:access just describes an org chart, and org charts change more often than permissions should. When a new microservice shows up, it should be checking for the capability it needs, not asking for a brand-new scope minted just for it.

Claims deserve the same discipline. Cramming every attribute a service might someday want into the JWT payload bloats the token and leaks information to anyone who can decode it (which, for a JWT, is anyone). Keep the token itself lean: identity, a handful of core claims, expiry. Push anything service-specific into a lookup the resource server does on its own, scoped to what it actually needs to know.

None of this is glamorous work. Still, scope sprawl is the kind of problem that's cheap to prevent early and expensive to unwind later, and by the time it hurts, you'll have forty services depending on the mess you didn't clean up.

Sources

  1. developers.google.com
  2. datatracker.ietf.org
  3. curity.io
  4. oauth.net
  5. johal.in

More in Auth and Security