ID Token vs JWT: Decode, Validate & Read Claims
Explore the structure of ID tokens and JWTs in OpenID Connect, learn to decode and validate them, and extract user claims for secure authentication.
Focus: id token
ID Tokens and JWT Structure in OpenID Connect: Decoding, Validating, and Reading Claims
What is an ID Token?
An ID token is a JSON Web Token (JWT) that an OpenID Connect (OIDC) provider returns to a client after a successful authentication. It contains self‑contained information about the user (claims) and is signed by the provider so that the client can verify its integrity without contacting the server for every request.
Key points:
- ID token = JWT – it follows the same three‑part structure: header.payload.signature.
- Claims inside the payload describe who the user is and when the token was issued.
- The token is short‑lived (usually a few minutes) and cannot be reused after expiration.
JWT Structure Overview
A JWT is composed of three Base64Url‑encoded sections separated by dots (.):
- Header – describes the token type (
JWT) and the signing algorithm (e.g.,RS256). - Payload – a set of claims (statements about the token). In OIDC the payload always contains the
subclaim and may includeiss,aud,exp,iat,nonce, and user‑specific attributes. - Signature – the result of signing the
header.payloadwith the provider’s private key (or a symmetric secret). The client verifies this signature with the provider’s public key.
Example (decoded)
Header (base64url): eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9
Payload (base64url): eyJ1c2VySWQiOiIxMjM0NSIsImV4cCI6MTcyMzQ1NjU2MSwiaWF0IjoxNjcyMzQ1NjYxLCJzdWIiOiIyMDEyMzQ1Njc4In0=
Signature: rXtX... (truncated)When decoded, the payload becomes a JSON object:
{
"sub": "1234567890",
"iat": 1622547600,
"exp": 1622551200,
"nonce": "abcdef123456"
}Decoding a JWT
The JWT can be decoded manually or with a small helper function. Below is a Python snippet that safely decodes a token without verifying the signature (useful for inspection only).
import base64
import json
def decode_jwt(token: str) -> dict:
"""Return the payload as a dict (no signature verification)."""
# Split into three parts
header_b64, payload_b64, _ = token.split('.')
# Add padding for base64 decoding
def _b64decode(part: str) -> bytes:
return base64.urlsafe_b64decode(part + '=' * (-len(part) % 4))
payload_json = _b64decode(payload_b64).decode('utf-8')
return json.loads(payload_json)
# Example usage
jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NSIsImV4cCI6MTcyMzQ1NjU2MSwiaWF0IjoxNjcyMzQ1NjYxLCJzdWIiOiIyMDEyMzQ1Njc4In0="
payload = decode_jwt(jwt)
print(payload)Output
{'sub': '1234567890', 'iat': 1622547600, 'exp': 1622551200, 'nonce': 'abcdef123456'}Note: Decoding alone does not guarantee the token is trustworthy. Validation (signature, expiry, audience, issuer, nonce) must be performed before using any claim.
Validating an ID Token
1. Verify the Signature
- Use the provider’s public key (for asymmetric algorithms like RS256) or the secret (for HS256).
- Libraries such as
pyjwtorpython-josehandle this automatically.
2. Check Standard Claims
| Claim | Purpose | Typical Validation |
|---|---|---|
iss |
Issuer identifier | Must match the expected OIDC provider URL. |
aud |
Audience | Must be the client ID (or a list containing it). |
exp |
Expiration time | Token must not be expired (current_time < exp). |
iat |
Issued‑at time | Optional, but useful for detecting clock skew. |
nonce |
Proof of possession | Must match the nonce sent in the authentication request. |
3. Clock Skew Handling
Because servers may have slightly different clocks, allow a small window (e.g., ±30 seconds) when comparing exp and iat.
4. Example Validation (Python)
import jwt # pyjwt
import requests
import time
# 1. Fetch the provider's JWKS (JSON Web Key Set) – contains public keys
jwks_url = "https://example-idp.com/.well-known/jwks.json"
jwks = requests.get(jwks_url).json()
def get_signing_key(header):
"""Extract the correct key from JWKS based on header."""
kid = header['kid']
for key in jwks['keys']:
if key['kid'] == kid:
return jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(key))
raise ValueError("Unable to find signing key")
# 2. Decode and verify
def validate_id_token(token: str, client_id: str, nonce: str) -> dict:
# Decode header to get the key id (kid)
header = jwt.get_unverified_header(token)
key = get_signing_key(header)
# Verify signature, audience, issuer, expiration, and nonce
claims = jwt.decode(
token,
key,
algorithms=[header['alg']],
audience=client_id,
issuer="https://example-idp.com/",
options={"require": ["exp", "iat", "nonce"]},
claims_options={"nonce": {"essential": True}}
)
# Additional nonce check (the token payload already contains it)
if claims.get("nonce") != nonce:
raise ValueError("Invalid nonce")
return claims
# Usage
client_id = "my-client-123"
nonce = "xyz789"
id_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
payload = validate_id_token(id_token, client_id, nonce)
print(payload)The function:
- Retrieves the public key from the JWKS endpoint.
- Validates the signature algorithm matches the header.
- Ensures required claims are present.
- Checks that the nonce matches the one sent during login.
Reading Claims
Once the token is validated, you can read any claim directly from the payload. Common OIDC claims include:
sub– subject: unique identifier for the user within the provider.email– user’s email address (if the provider discloses it).name– full name.given_name– first name.family_name– last name.profile– URL to the user’s profile picture.role– optional custom claim for authorization.
Example: Extracting User Info
def extract_user_info(claims: dict) -> dict:
"""Return a simplified user info dict."""
return {
"user_id": claims.get("sub"),
"email": claims.get("email"),
"name": claims.get("name"),
"given_name": claims.get("given_name"),
"family_name": claims.get("family_name"),
}
# Assuming `payload` is the dict returned from validate_id_token
user_info = extract_user_info(payload)
print(user_info)Typical output:
{
"user_id": "1234567890",
"email": "alice@example.com",
"name": "Alice Example",
"given_name": "Alice",
"family_name": "Example"
}You can now use user_info to create a session, fetch additional profile data, or perform authorization checks.
Real‑World Example: From Login to Secure Access
-
User clicks “Login with Google”. The OIDC provider redirects the user to the authorization endpoint with parameters: -
response_type=code-client_id=my-client-123-redirect_uri=https://myapp.com/callback-scope=openid email profile-state=random123-nonce=xyz789 -
Provider authenticates the user and redirects back to
https://myapp.com/callback?code=abc123&state=xyz789. -
Backend exchanges the authorization code for tokens (access token, ID token, optionally refresh token) at the token endpoint.
-
Backend validates the ID token using the steps shown earlier (signature,
iss,aud,exp,nonce). If validation fails, the login is aborted. -
After validation, the backend extracts the user’s claims, creates a local session, and can safely trust the user’s identity for subsequent API calls.
-
For API authorization, the backend may read the
scopeclaim (if present) or map theroleclaim to internal permissions.
This flow demonstrates why correctly decoding and validating the ID token is crucial: it guarantees that the user is who they claim to be, without exposing the user to forged or replayed tokens.
Summary
- An ID token is a signed JWT that carries user information and authentication status.
- It consists of three Base64Url parts: header, payload, and signature.
- Decoding gives you the raw claims; validation (signature, expiry, audience, issuer, nonce) proves the token’s authenticity.
- Use a reputable library (e.g.,
pyjwt) to handle cryptographic verification and claim checks. - After validation, read the claims to identify the user, populate a session, or enforce authorization.
- Real‑world implementations follow the OIDC flow: redirect → code exchange → token receipt → token validation → claim extraction → secure access.
By mastering ID token handling, junior developers can build secure, user‑centric applications that leverage OpenID Connect without reinventing authentication logic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.