When working with modern identity protocols like OpenID Connect (OIDC) and OAuth 2.0, your client application typically receives two distinct tokens upon login: an ID Token and an Access Token.
Because both tokens are frequently formatted as JSON Web Tokens (JWTs), many developers treat them as interchangeable. This is a severe security mistake. Using an ID Token to authorize API calls breaks tenant isolation and introduces privilege escalation vulnerabilities. Let's examine why.
1. Authentication vs. Authorization
To grasp why two different tokens exist, consider the core division of identity systems:
🪪 ID Token vs Access Token: Identity vs Permissions
Architectural Matrix- Authentication (AuthN): Proving who you are. "This user is Jane Doe with email jane@example.com."
- Authorization (AuthZ): Determining what you can do. "The bearer is permitted to write records to the Billing API."
The ID Token is strictly an authentication artifact. The Access Token is strictly an authorization artifact.
2. Side-by-Side Comparison
| Feature | ID Token | Access Token |
|---|---|---|
| Governing Standard | OpenID Connect (OIDC) | OAuth 2.0 (RFC 6749) |
| Primary Purpose | Provide user identity profile to the client | Grant permission to call a protected Resource Server API |
| Intended Recipient (Audience) | The Client Application (Frontend / Mobile App) | The Resource Server (Backend API Gateway) |
Audience Claim (aud) |
The Client ID (e.g. my-web-app-client-id) |
The API Identifier (e.g. https://api.myapp.com/v1) |
| Format Requirement | Must always be a valid, signed JWT | Opaque string or JWT (format is undefined by OAuth 2.0) |
| Where Sent in Requests | Never sent in API headers; stays in client state | Sent in Authorization: Bearer <token> header |
| Typical Lifespan | Short or medium (e.g., 1 hour) | Very short (e.g., 5 to 15 minutes) |
3. Anatomy of an ID Token
An ID Token is designed to give the frontend client application the user details needed to render the UI, personalize greeting screens, and manage client-side state without making an extra network request to a /userinfo endpoint.
{
"iss": "https://auth.company.com/",
"sub": "auth0|987654321",
"aud": "s6BhdRkqt3", // Matches the Frontend Client ID!
"exp": 1792500000,
"iat": 1792496400,
"nonce": "n-0S6_WzA2Mj",
"auth_time": 1792496395,
"name": "Jane Developer",
"given_name": "Jane",
"family_name": "Developer",
"email": "jane@example.com",
"email_verified": true,
"picture": "https://avatar.example.com/jane.png"
}
4. Anatomy of an Access Token (as JWT)
An Access Token is designed for the backend Resource Server. Notice how user profile details (such as names or pictures) are absent, while API-centric claims (like audiences, scopes, and tenant identifiers) take center stage:
{
"iss": "https://auth.company.com/",
"sub": "auth0|987654321",
"aud": "https://api.company.com/v2/", // Matches the API Gateway Identifier!
"exp": 1792497300, // 15-minute expiration
"iat": 1792496400,
"client_id": "s6BhdRkqt3",
"scope": "read:reports write:invoices delete:drafts",
"permissions": [
"reports:view",
"invoices:create"
]
}
5. The Dangerous Anti-Pattern: Sending ID Tokens to APIs
Why is it dangerous to accept an ID Token at your backend API?
🚨 The Audience Confusion Vulnerability
An ID Token's aud claim is set to the client ID of the app that requested login. Suppose your company has multiple client apps (e.g. a low-privilege customer portal and a high-privilege employee portal) all authenticating against the same corporate identity provider.
If your backend API accepts ID Tokens without enforcing that aud matches the API itself, an attacker who registers a rogue client application with your Identity Provider can obtain a valid ID Token issued to their own client ID, send it to your backend API, and gain unauthorized access because the cryptographic signature is valid!
6. Backend API Validation Checklist
When building an API gateway or backend microservice, always enforce these validation rules for incoming bearer tokens:
- Extract from Authorization Header: Ensure the token is provided via
Authorization: Bearer <token>. - Verify Cryptographic Signature: Validate against the Authorization Server's public JWKS keys.
- Verify the Audience (
aud): Reject the token ifauddoes not match your specific API identifier. - Verify the Issuer (
iss): Confirm the token was issued by your trusted authorization server domain. - Check Expiration (
exp): Ensureexp > current_timestamp. - Evaluate Scopes and Permissions: Check that the token's
scopeclaim contains the permission required for the specific HTTP method and route.