The ID Token
The ID Token is OIDC’s contribution to OAuth2. It’s a JWT issued alongside the access token, and it has exactly one job: tell the client who the user is.
Not for calling APIs. Just for the client to read.
What it looks like
An ID Token is a JWT, so it has three dot-separated parts:
eyJhbGciOiJSUzI1Ni...eyJzdWIiOiI0Mj...siglmFsdfqkn...
└── header ────┘└── payload ─────┘└── signature ─┘
Decoded payload, typical example:
{
"iss": "https://auth.example.com/realms/my-app",
"sub": "42f1f9c2-04cd-4b56-9c0a-eaaa12345678",
"aud": "my-frontend",
"exp": 1736380000,
"iat": 1736376400,
"auth_time": 1736376400,
"nonce": "n-0S6_WzA2Mj",
"name": "Alice Smith",
"email": "alice@example.com",
"email_verified": true
}
Each field is a claim. Some claims are standardized; others are added by the provider or the client.
Mandatory claims
OIDC requires these claims, and clients must validate them:
| Claim | Meaning |
|---|---|
iss | Issuer. Who issued this token. Must match the expected provider. |
sub | Subject. The user’s stable identifier. Treat as the primary key. |
aud | Audience. Which client this token is for. Must be your client_id. |
exp | Expiration. Unix timestamp. After this, reject. |
iat | Issued at. When it was minted. |
auth_time | When the user actually logged in (vs. when this token was issued). |
nonce | The value your client sent in the authorization request. Replay protection. |
Optional standard claims
Pulled in by the profile, email, address, and phone scopes:
name,given_name,family_name,nickname,preferred_usernameemail,email_verifiedpicture,locale,zoneinfoaddress,phone_number
How a client validates an ID Token
This is what every halfway-competent OIDC library does, every single time:
- Parse the JWT. Confirm three segments.
- Verify the signature against the provider’s public key from
jwks_uri. - Check
issmatches the expected issuer. - Check
audcontains the client’s ownclient_id. - Check
expis in the future (with a small clock-skew tolerance). - Check
noncematches what the client sent in the authorize request. - If present, check
c_hash/at_hashagainst the code and access token.
Skip any of these and you’ve shipped a security bug. Use a vetted library. Don’t roll your own.
ID Token vs Access Token: the rule
This is the single most-broken rule in OIDC, so it deserves a callout:
ID Tokens are not bearer tokens for APIs
The ID Token is for the client to inspect. The Access Token is for the API. Sending the ID Token in Authorization: Bearer headers to your API is wrong. If your APIs accept ID Tokens, an attacker can replay any ID Token issued to any client for any audience.
If your API “just wants to know who the user is,” issue a real access token that carries sub and validate that.
Where the user identity actually lives
sub is the stable identifier. Treat it as the user’s primary key in your own database. Do not use email or preferred_username as the key. Those can change.
If the user changes their email at the IdP, email in tomorrow’s ID Tokens will change. sub will not.
In FerrisKey
FerrisKey issues OIDC ID Tokens as JWTs signed with the realm’s signing key. You can decode any FerrisKey ID Token at jwt.io for a quick visual.