ID token vs access token vs refresh token

OpenID Connect flows hand a client up to three different tokens, and most JWT confusion comes from treating them interchangeably. They answer different questions for different audiences:

ID tokenAccess tokenRefresh token
Answers"Who signed in?""What may this caller do?""May I get new tokens?"
Consumed byThe client appThe API (resource server)The authorization server only
FormatAlways a JWTJWT (RFC 9068) or opaqueUsually opaque
Typical lifetimeMinutesMinutes to an hourDays to months, often rotated

ID token — proof of authentication

The ID token exists for the client: it proves a user authenticated, and carries identity claims like sub, email, and auth_time. Its aud is the client's own ID (with azp disambiguating when there are several), and the client must check nonce to bind it to the login request it started.

Access token — authorization for an API

The access token is for the API. The client should treat it as an opaque string to attach to requests — even when it happens to be a JWT. When it is a JWT, RFC 9068 profiles it: scope lists granted permissions, client_id names the caller, and the API must verify the signature, iss, its own aud, and exp on every request.

Refresh token — long-lived credential

The refresh token is presented only to the authorization server, to obtain fresh ID/access tokens without re-prompting the user. It's the most dangerous of the three if stolen, which is why modern guidance rotates it on every use and revokes the family on reuse detection. It should never be sent to an API and never needs to be a JWT.

The classic mistakes

  • Calling an API with the ID token. Its audience is the client, so a correct API rejects it — and an incorrect API that accepts it has an audience-check bug.
  • Reading claims from the access token in the client. Its format is a contract with the API, not with you; providers change it without notice.
  • Skipping nonce validation on the ID token, which reopens replay attacks the claim exists to stop.

Not sure which token you're holding? Paste it into the TokenPrism debugger — the annotated claims (aud, scope, nonce, azp) identify it immediately, or diff two tokens to see how your provider's ID and access tokens differ.