You finish an OAuth/OIDC login, you’re holding an access token, and you need the user’s email and name. The token is right there — often a JWT you can decode in one line. So you decode it and read the claims out.

That one shortcut is the most common identity bug in OAuth systems, and it’s worth understanding why — because the code usually works, right up until it doesn’t.

Two different documents for two different readers

OAuth 2.0 and OpenID Connect answer different questions:

  • Authorization — is this client allowed to do X? → the access token.
  • Authentication — who is this user? → the ID token (and /userinfo).

An access token is a key to a room. An ID token is an ID card. Both can be in your pocket; they are not interchangeable. You don’t prove who you are by showing a hotel key — even though the key got you into the room.

Why the access token is the wrong source — even when you can read it

The tempting rebuttal: “but my provider issues a JWT access token, and it has an email claim.” Both PingFederate and Auth0 can do that. It still doesn’t make it a valid identity source:

  • It’s opaque by contract. The OAuth spec deliberately leaves the access token’s format to the authorization server. It can be a reference string today and an encrypted JWT tomorrow — and nothing broke a promise, because no promise was ever made to you. Code that parses it is built on sand.
  • Wrong audience. The access token’s aud is your resource API, not your client app. Reading it for login is reading a document addressed to someone else — the classic “confused deputy” setup.
  • Wrong purpose and lifetime. It’s tuned to authorize API calls: short, scoped, revocable on the resource server’s terms. It is not a considered statement about identity for your session.
  • No nonce, no client-audience check. The validation ceremony that makes an ID token trustworthy for your app was never run on the access token.

What actually proves identity

  • The ID token — a JWT of identity claims, minted for your client, which you validate (signature, iss, aud = your client, exp, nonce). After that, its sub, email, name are trustworthy.
  • The /userinfo endpoint — call it with the access token (Authorization: Bearer …) when you want fresh or fuller claims. Note the roles reverse: the access token is the key that lets you ask, and the response is the identity answer. That’s the access token doing its real job.

The rule, and the identifier

The access token gets you into the API. It never tells you who knocked. Identity comes from the ID token or /userinfo — never from decoding the access token.

And once you have identity: key users on sub + iss, not email. sub is the stable, opaque, per-issuer identifier. Email changes, and a released address can be handed to a different person — key on it and you’ve built an account-takeover path.

The full flow this lives inside: the OIDC authorization-code flow with PKCE.