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
ID TOKEN (OIDC) Purpose: Authentication Answers: "Who is the user?" (User Identity Card) Intended Audience (aud): Client Application (SPA / App) Contains: sub, email, name, picture, auth_time 🚫 NEVER pass to Backend APIs to authorize actions! ACCESS TOKEN (OAuth 2) Purpose: Authorization Answers: "What can the user do?" (API Bearer Key) Intended Audience (aud): Resource Server / Backend API Contains: scope, permissions, roles, sub ✓ Transmitted via Authorization: Bearer <token>
Figure 1: ID tokens prove identity to the frontend client application; access tokens delegate granular authorization scopes to backend resource servers.

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.

Decoded ID Token Payload
{
  "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:

Decoded Access Token Payload
{
  "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:

  1. Extract from Authorization Header: Ensure the token is provided via Authorization: Bearer <token>.
  2. Verify Cryptographic Signature: Validate against the Authorization Server's public JWKS keys.
  3. Verify the Audience (aud): Reject the token if aud does not match your specific API identifier.
  4. Verify the Issuer (iss): Confirm the token was issued by your trusted authorization server domain.
  5. Check Expiration (exp): Ensure exp > current_timestamp.
  6. Evaluate Scopes and Permissions: Check that the token's scope claim contains the permission required for the specific HTTP method and route.