Chapter 5: Tokens in Depth — What's Actually in That JWT

This is Part 5 of a chapter-by-chapter walkthrough of my book OpenID: Modern Identity for Developers and Architects. In the previous chapter we watched tokens move across the wire in the major OIDC flows. Chapter 5 cracks them open.

Most identity bugs are not flow bugs. They are token-handling bugs. A missing aud check, a skipped signature verification, a refresh token stored in local storage, an ID token shoved into an Authorization header. Chapter 5 is the chapter you want open on your other screen the next time you're writing token-validation code.


5.1 — ID Tokens and the Claims You Must Never Skip

The ID Token is the artifact OpenID Connect adds on top of OAuth 2.0. It is a JWT, signed by the OpenID Provider, that carries a standard set of claims about the authenticated user. It is consumed by the RP itself — not forwarded to APIs, not used as a bearer token for resource servers. Its job is to answer "who is this person?" securely.

Five required claims do the heavy lifting:

  • iss — the issuer. Must match the OP URL you expect.
  • sub — the stable subject identifier for the user at this OP.
  • aud — the audience. Must match your client_id, or the token isn't meant for you.
  • exp — expiry. Reject if past.
  • iat — issued-at. Useful for replay detection.

On top of these, you get the standard identity claims: name, email, email_verified, picture, locale, and friends. Chapter 5 walks through which ones you can rely on across providers and which you need to treat with suspicion (hint: email without email_verified: true is not verified).

Important: An ID Token where you skip the aud check is an ID Token you don't actually own. Any valid token from the same issuer, for any client, will pass the rest of your validation. This is the token-substitution attack — simple, devastating, and embarrassingly common.

5.2 — Access Tokens: Opaque vs. JWT (and Why Your Provider Chose)

Access Tokens authorize access to resources; they are not identity assertions. Two formats dominate in practice:

Opaque access tokens are random strings with no internal structure. The resource server validates them by calling the OP's introspection endpoint. Upside: instant revocation. Downside: a network hop per validation.

JWT access tokens are self-contained — the resource server validates the signature locally and reads claims directly. Upside: zero-hop validation, great for microservices. Downside: revocation is hard, because the token is valid until it expires.

Which your provider issues is usually not a configuration knob; it's an architectural choice. Knowing which you've got and its implications for revocation is more important than having a preference.

5.3 — Refresh Tokens and Rotation

Access tokens should be short-lived — 15 minutes to an hour — because that is the window an attacker has if they steal one. Refresh tokens exist so users don't get re-prompted every hour. They live for days or weeks, they never leave your backend, and they are traded silently at the token endpoint for a fresh access token.

The modern best practice is refresh token rotation: each refresh returns a new refresh token and invalidates the old one. The best providers also implement reuse detection — if the same refresh token is presented twice, they assume theft and kill the entire token chain. Chapter 14 goes deeper on the operational side.

Key idea: Refresh tokens are the highest-value secret in your system. A stolen access token is a 15-minute problem. A stolen refresh token is a months-long problem. Protect them accordingly.

5.4 — JWT Structure: JWS and JWE

A JWT is three base64url-encoded parts separated by dots: header, payload, signature. The header declares the signing algorithm (alg) and usually a key ID (kid). The payload is the claim set. The signature covers both.

Most tokens you meet are JWS (signed, not encrypted). When the payload itself is sensitive and must be hidden in transit, JWE wraps the payload in an encryption envelope. In OpenID Connect the usual pattern is JWS; JWE shows up in high-assurance contexts like FAPI (Chapter 15). Algorithm choice matters: prefer RS256 or ES256, and always reject alg: none — that one has a storied history of breaking real systems.

5.5 — Signature Validation, End to End

Validating a JWT is more than calling jwt.verify(). A complete validation is a checklist, and Chapter 5 spells it out in full. The short form:

  1. Parse the three parts; reject malformed input.
  2. Check the alg is on your allowlist. Reject none and anything unexpected.
  3. Fetch the OP's JWKS (from .well-known/openid-configurationjwks_uri), cached.
  4. Match the token's kid to a key in the JWKS; refresh the JWKS if there's no match.
  5. Verify the signature.
  6. Verify iss, aud, exp. Consider iat for clock-skew tolerance.
  7. Only now do you trust the claims.

Every one of these steps exists because someone, somewhere, has been compromised by skipping it.

5.6 — Token Binding: DPoP and mTLS

Plain bearer tokens have a fundamental weakness: whoever holds them can use them. Token binding fixes that by tying the token to something the legitimate client holds that the attacker doesn't.

DPoP (RFC 9449) has the client generate an ephemeral key pair and sign a small proof JWT for each request, including a hash of the token and the request's method and URL. The resource server verifies that the same key that authorized the token is signing this request. Stolen token without the key is useless.

mTLS binds the token to a TLS client certificate via a cnf claim. The resource server checks that the certificate presented in the current TLS handshake matches the one recorded in the token. Same principle, different cryptographic substrate.

Both produce sender-constrained tokens. FAPI requires one of them for high-value flows (Chapter 15), and they are gradually becoming the default for any system where a bearer token being intercepted would ruin your day.


What Chapter 5 Sets Up

After Chapter 5, you should be able to explain the difference between an ID Token and an Access Token in one breath, name the validation steps for a JWT in order, reason about opaque vs. JWT access tokens without bias, protect refresh tokens like the crown jewels they are, and know when DPoP or mTLS belong in the picture.


Next up — Chapter 6: Discovery and Metadata. We look at the connective tissue of modern OIDC: the .well-known/openid-configuration endpoint, provider metadata, the JWKS, and Dynamic Client Registration. These are the mechanisms that let RPs and OPs find each other without hand-crafted configuration — and the reason modern identity can scale to millions of clients.

Want the full picture? Grab OpenID: Modern Identity for Developers and Architects here for the full claim reference, complete validation walkthroughs, and the rest of the 22-chapter journey through modern identity.
2026-03-11

Sho Shimoda

I share and organize what I’ve learned and experienced.