ID Token

A JWT issued by an OpenID Connect provider that asserts who the authenticated user is, signed by the issuer. Distinct from an OAuth access token (which says what the bearer can do).

Structure: header.payload.signature

A JWT is three base64url-encoded parts joined by .. Each part is independently decodable; only the signature requires the issuer’s public key to verify.

eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9 . eyJpc3MiOiJodHRwczovL2FjY291bnRzLi4uIn0 . SflKxw...
   header (alg, kid)                       payload (claims)                          signature

Decoded Payload (Google example)

{
  "iss": "https://accounts.google.com",
  "sub": "110169484474386276334",
  "aud": "YOUR_CLIENT_ID",
  "exp": 1353604926,
  "iat": 1353601026,
  "nonce": "random_value_from_request",
  "email": "user@gmail.com",
  "email_verified": true,
  "name": "John Doe",
  "picture": "https://lh3.googleusercontent.com/photo.jpg"
}

Critical Claims

ClaimMeaningWhy it matters
issIssuer URLMust match the discovery issuer. Defends against tokens from a wrong provider.
subSubject — stable user identifier within the issuerUse this as your user ID. Never use email.
audAudience — your client_idIf it doesn’t match yours, the token was issued for a different app. Reject.
expExpiration (Unix seconds)Reject if past.
iatIssued atReject if implausibly old or in the future (clock skew tolerance ~5 min).
nonceReplay-protection value you sentMust match the value your client sent in the auth request. See Social Login Security.
azpAuthorized partyPresent when token is for one client but the user authorized many; verify if present.

Validation Algorithm

flowchart TD
    A[Receive id_token] --> B[Parse header → get kid, alg]
    B --> C[Fetch JWKS from issuer's jwks_uri<br/>cache by kid]
    C --> D[Find key matching kid]
    D --> E[Verify signature using key + alg]
    E -->|fail| X[Reject]
    E -->|pass| F[Check iss == expected issuer]
    F -->|fail| X
    F -->|pass| G[Check aud == our client_id]
    G -->|fail| X
    G -->|pass| H[Check exp > now]
    H -->|fail| X
    H -->|pass| I[Check nonce == sent nonce]
    I -->|fail| X
    I -->|pass| J[Accept user identity]

Reference Validation (pseudocode)

import jwt  # pyjwt
from jwt import PyJWKClient
 
jwks = PyJWKClient(discovery["jwks_uri"])
signing_key = jwks.get_signing_key_from_jwt(id_token).key
 
claims = jwt.decode(
    id_token,
    signing_key,
    algorithms=["RS256"],          # never accept "none"; pin algorithm
    audience=CLIENT_ID,
    issuer=discovery["issuer"],
    options={"require": ["exp", "iat", "iss", "aud", "sub"]},
)
 
if claims.get("nonce") != stored_nonce_for_session:
    raise InvalidToken("nonce mismatch")

Why sub and Not email for the User ID

  • Emails change (rebranding, address change, marriage).
  • An email at one provider can be reassigned to a different person.
  • email may be absent if the user denies the email scope.
  • sub is per-issuer-stable and unique forever.

The composite primary key for a federated user is (iss, sub), not email.

ID Token vs Access Token vs Refresh Token

ID TokenAccess TokenRefresh Token
Tells youwho the user iswhat bearer can dohow to get a new access token
Formatalways JWTopaque or JWTopaque
Audienceyour clientthe resource serverthe auth server
Sent tonobody (consumed by you)resource server APIsthe token endpoint
Lifetimeminutes (short — it’s a snapshot)minutes to ~1hdays to months

Common mistake: sending the ID Token to APIs as an authorization header. Don’t. APIs accept access tokens. The ID Token is for your client to consume.

Pitfalls

  • Accepting alg: none. Old JWT libraries did this by default. Pin acceptable algorithms.
  • Skipping signature verification because “we trust HTTPS” — HTTPS proves you talked to the auth server now; the token may be stale, replayed, or forged.
  • Caching keys forever without honoring kid rotation. Refetch JWKS on kid miss.
  • Trusting email_verified=false as if it were verified.

See Also