ID Token Decoder
Decode OpenID Connect ID tokens to extract user identity information. ID tokens provide authenticated user details to client applications.
ID Token vs Access Token
| Feature | ID Token | Access Token |
|---|---|---|
| Purpose | User identity | API authorization |
| Audience | Client application | Resource server (API) |
| Content | User claims (name, email) | Scopes and permissions |
| Format | Always JWT | JWT or opaque |
| Sender | Always to client | Client to API |
Standard ID Token Claims
| Claim | Required | Description |
|---|---|---|
| iss | Yes | Issuer (OAuth server URL) |
| sub | Yes | Subject (unique user ID) |
| aud | Yes | Audience (your client ID) |
| exp | Yes | Expiration time |
| iat | Yes | Issued at time |
| auth_time | Optional | When user authenticated |
| nonce | Conditional | Replay attack prevention |
| acr | Optional | Authentication context class |
| amr | Optional | Authentication methods used |
ID Token Decoder
``javascript
function decodeIDToken(token) {
const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('ID tokens must be JWTs');
}
const decode = (s) => JSON.parse(atob(s.replace(/-/g, '+').replace(/_/g, '/')));
const header = decode(parts[0]);
const payload = decode(parts[1]);
// Standard OIDC claims
const standardClaims = {
issuer: payload.iss,
subject: payload.sub,
audience: payload.aud,
expiresAt: new Date(payload.exp * 1000),
issuedAt: new Date(payload.iat * 1000),
authTime: payload.auth_time ? new Date(payload.auth_time * 1000) : null,
nonce: payload.nonce
};
// User profile claims
const profileClaims = {
name: payload.name,
email: payload.email,
emailVerified: payload.email_verified,
picture: payload.picture,
locale: payload.locale,
phone: payload.phone_number
};
// Provider-specific claims
const customClaims = {};
const knownClaims = ['iss', 'sub', 'aud', 'exp', 'iat', 'auth_time', 'nonce',
'name', 'email', 'email_verified', 'picture', 'locale', 'phone_number'];
for (const [key, value] of Object.entries(payload)) {
if (!knownClaims.includes(key)) {
customClaims[key] = value;
}
}
return {
header,
standardClaims,
profileClaims,
customClaims,
raw: payload
};
}
`
OIDC Profile Scopes
| Scope | Claims Included |
|---|---|
| openid | sub (required for ID token) |
| profile | name, family_name, given_name, picture |
| email, email_verified | |
| address | address (formatted, street, city, etc.) |
| phone | phone_number, phone_number_verified |
The Attacks a Verifier Must Stop
alg: none. A token declaring no algorithm with an empty signature. A library that
trusts the header's alg will accept it as valid. Always specify the expected algorithm
when verifying rather than reading it from the token.
Algorithm confusion. A token signed with HMAC using the server's *public* RSA key as the
secret. If the verifier picks the algorithm from the header, an RS256 verifier can be tricked
into running HS256 with a key the attacker already has.
Weak secrets. HS256 with a short or dictionary secret is brute-forceable offline from a
single captured token. Use at least 256 bits of real entropy.
No expiry check. exp is a claim, not an enforcement. A library that decodes without
verifying, or code that reads claims from a decoded-but-unverified token, accepts expired and
forged tokens alike.
`javascript
// Specify the algorithm; never trust the header's
jwt.verify(token, secret, { algorithms: ['HS256'] });
`
Anyone Can Read the Payload
A JWT is signed, not encrypted. The payload is base64url — readable by anyone holding the
token, including the browser it is stored in. Never put anything in it you would not print
on a postcard: no passwords, no PII beyond an identifier, no internal keys.
Revocation Is the Hard Part
A signed token is valid until it expires, and there is no way to withdraw one. Options:
| Approach | Cost |
|---|---|
| Short expiry (5–15 min) plus refresh tokens | Standard; adds a refresh endpoint |
| A denylist of revoked JTIs | Reintroduces the state JWTs were meant to avoid |
| Rotate the signing key | Revokes every token at once |
| Version claim checked against the user record | A database read per request |
If you need immediate revocation for every session, a server-side session is a simpler and
more honest choice than a JWT.Where to Store One
| Location | XSS-safe | CSRF-safe |
|---|---|---|
localStorage | No | Yes |
| sessionStorage` | No | Yes |
| Cookie (HttpOnly, Secure, SameSite) | Yes | Yes, with SameSite |
| In-memory | Yes | Yes |
An HttpOnly cookie is the safest default. Any token reachable from JavaScript is reachable by
any script that gets injected.