Loading...
Loading...
OAuth2 flows, OpenID Connect, JWT claims, and mutual TLS for service-to-service auth
Wall 3 of the stack says the gateway authenticates identity, but passwords cannot travel to every service that needs proof. You want login with Google without giving your app the user Google password, and you want services to call each other without sharing one secret everywhere. Think of a valet key as the single analogy here: the valet key starts the car but never opens the glovebox or the trunk, and you take it back when dinner ends. OAuth2, which is a standard for delegated authorization, meaning letting one app access resources on your behalf without your password, works the same way through tokens, which are short strings that carry scoped permission and expire.
The naive fix is password sharing or one shared secret across services, which works until the first leak, and then every service the secret touched must rotate at once with no way to scope the damage. Delegation fixes it by issuing limited tokens per app, per user, per scope, so a stolen token opens only what it was cut for and dies on its own schedule.
User -> App: "login"
App -> Browser: redirect to accounts.google.com/oauth/authorize?client_id=...&scope=email
User -> Google: consent
Google -> Browser: redirect to app/callback?code=abc
App -> Google: POST /token {code=abc, client_secret=...} -> {access_token, id_token}
App -> Google: GET /userinfo with access_token -> profileThe browser and mobile standard: the app receives a short-lived code and exchanges it for tokens. PKCE, which is a per-request secret the app hashes into the first redirect and reveals at the exchange, prevents a stolen code from being useful. The older implicit flow, which returned tokens directly in the redirect, is never used today because tokens leak through browser history.
Service-to-service calls post their own id and secret with a client-credentials grant, which is a token request identifying the service rather than a user, and receive a bearer token, which is a token anyone holding it can use. No user, no consent screen, just scoped machine permission.
Televisions with no keyboard use a device code the user approves on a phone, while single-page apps use a refresh token, which is a longer-lived token exchanged for fresh short access tokens without asking the user again. Store the refresh token in an httpOnly Secure cookie, which is a cookie JavaScript cannot read, never in readable storage.
OIDC, which is OpenID Connect, an identity layer on OAuth2, adds an id token, which is a signed statement of who the user is carrying claims such as subject, email, and audience. The access token authorizes, meaning it opens resources, while the ID token authenticates, meaning it proves identity. Always check the audience equals your own client id, or any valid token for another app walks into yours.
// id_token payload (JWT claims)
{
"iss": "https://accounts.google.com",
"sub": "1101694844743",
"aud": "your-client-id",
"exp": 1700000000,
"email": "ada@example.com"
}Outside the browser, bearer tokens leak through logs and traces because anyone holding the string holds the permission. mTLS, which is mutual TLS where both sides present certificates and verify each other against a certificate authority, authenticates with short-lived client certificates instead. A service mesh, which is infrastructure such as Istio or Linkerd that injects a sidecar proxy beside every service to handle identity and rotation, automates issuance and rotation so certificates live for hours and rotate without restarts.
client --(Client Cert + Server Cert verify)--> server CA signs short-lived certs (hours); sidecar rotates without restart
A single-page app storing a 30-day access token in localStorage, which is browser storage any script on the page can read, turns one cross-site scripting payload, which is attacker JavaScript running in your users browser, into a month of API access with no password needed and no revocation possible. Short lifetimes plus rotation shrink that catastrophe into a 5-minute window, which is why every lifetime below is deliberately boring: the math is stolen token value equals permissions multiplied by minutes until expiry.
lifetimes that survive review:
access token (JWT): 5-15 min, audience + issuer + expiry verified
refresh token: 7-30 days, rotated on EVERY use, reuse = theft signal
ID token: 5-15 min, audience must equal YOUR client_id
auth code: single use, 60-120s expiry, PKCE S256 required
S256, which is the SHA-256 challenge method binding the code to the requester
JWKS cache: 10-60 min; honor kid rotation without restart
storage that survives XSS review:
SPA, which is a single-page app running entirely in the browser:
refresh in httpOnly Secure SameSite cookie, access in memory only
never localStorage for long-lived tokens — XSS reads it triviallyEach refresh returns a new refresh token and invalidates the old one, forming a chain. One team tried skipping rotation costs with 24-hour access tokens instead of 5-minute ones: a stolen token stayed valid 288 times longer, while the refresh traffic saved measured under half a percent of requests. If an old token reappears, someone cloned the chain, so invalidate the whole token family and force re-login. This single check converts silent theft into a loud, actionable signal.
Check the signature against the key set by key id, then issuer, audience, expiry, not-before, and algorithm, rejecting the none algorithm and pinning to RS256 or ES256, which are RSA and elliptic-curve signature schemes. Skipping the audience check lets any valid token for another app into yours, a real bug class that has caused real breaches, not a hypothetical.
Permissions change when a user is demoted from admin, but their 15-minute JWT still says admin and downstream services trust it without asking anyone. JWTs are snapshots that go stale until expiry, while opaque tokens are pointers the server resolves fresh on each request. Stateless scale against instant truth is the entire debate: snapshots save a lookup per request and cost a staleness window, pointers cost a lookup per request, cacheable for 30 to 60 seconds, and buy immediate revocation by deleting the server-side session.
| Concern | JWT wins | Opaque wins |
|---|---|---|
| Scale | Verify locally via the key set with no per-request database hit | Introspection call per request, cacheable for 30 to 60 seconds |
| Revocation | None until expiry, needs a denylist to fake it | Instant, by deleting the server-side session |
| Size | Headers bloat as claims grow, adding bytes to every request | Tiny fixed-size random string regardless of permissions |
Identity solved: strangers prove who they are without handing over passwords, and services prove themselves with certificates that rotate hourly. But every verified user arrives carrying bytes: a hundred million avatars and uploads, written once, read for years, executable if you store them wrong. Where hostile bytes sleep safely is its own storage question, and it is next.