Skip to content

[Security] Missing iss, aud, exp claim validation in _validateIdTokenAttributes (CWE-863) #2475

Description

@jayantkamble10000

Summary

ResponseValidator._validateIdTokenAttributes() (src/ResponseValidator.ts, lines 195-226) does not validate the iss (issuer), aud (audience), or exp (expiration) claims of ID tokens. This violates MUST-level requirements of OIDC Core 1.0 Section 3.1.3.7.

The library has MetadataService.getIssuer() and MetadataService.getSigningKeys() methods defined but never called anywhere in src/ — the validation infrastructure exists but was never wired up. client_id is available at this._settings.client_id (used at line 131) but is never compared against the aud claim.

CWE-863 — Incorrect Authorization
CVSS 3.1: 7.4 HIGH (AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N)
Precedent: CVE-2026-45069 (Symfony OidcTokenHandler — same bug class)

Affected Code

// src/ResponseValidator.ts lines 195-226
protected _validateIdTokenAttributes(response: SigninResponse, existingToken?: string, nonce?: string): void {
    const incoming = JwtUtils.decode(response.id_token ?? "");

    if (!incoming.sub) {
        logger.throw(new Error("ID Token is missing a subject claim"));
    }

    if (nonce && incoming.nonce !== nonce) {
        logger.throw(new Error("nonce in id_token does not match nonce in client storage"));
    }

    if (existingToken) {
        const existing = JwtUtils.decode(existingToken);
        if (incoming.sub !== existing.sub) { ... }
        if (incoming.auth_time && incoming.auth_time !== existing.auth_time) { ... }
        if (incoming.azp && incoming.azp !== existing.azp) { ... }
    }

    response.profile = incoming as UserProfile;
    // NO iss check. NO aud check. NO exp check.
}

Dead Infrastructure Proof

$ grep -rn "getIssuer" src/
src/MetadataService.ts:71:    public getIssuer(): Promise<string> {
# ZERO callers — only the definition

$ grep -rn "getSigningKeys" src/
src/MetadataService.ts:127:    public async getSigningKeys(): Promise<SigningKey[] | null> {
src/MetadataService.ts:128:        const logger = this._logger.create("getSigningKeys");
# ZERO callers — only the definition + internal log

OIDC Specification Violations

Step Requirement Level Status
2 iss MUST exactly match the expected Issuer Identifier MUST MISSING
3 aud MUST contain the client_id MUST MISSING
9 Current time MUST be before exp MUST MISSING

Attack Scenarios

1. Token Confusion (Missing aud)

If an IdP serves multiple apps (App A and App B), an attacker authenticated to App B can inject their ID token into App A — aud is never compared against client_id.

2. Issuer Mix-Up (Missing iss)

RFC 9207 mix-up attacks become possible when an application interacts with multiple IdPs — iss is never validated against the configured authority.

3. Token Replay (Missing exp)

Leaked/stolen ID tokens (from browser history, logs, or storage) are accepted indefinitely — zero temporal validation.

Mitigating Factors

  1. The library enforces authorization code flow only, so ID tokens arrive over TLS backchannel — mitigates direct injection.
  2. State and PKCE are correctly implemented.
  3. Nonce is correctly validated by VALUE when present.

Suggested Fix

 protected _validateIdTokenAttributes(response: SigninResponse, existingToken?: string, nonce?: string): void {
     const incoming = JwtUtils.decode(response.id_token ?? "");

     if (!incoming.sub) {
         logger.throw(new Error("ID Token is missing a subject claim"));
     }

+    // OIDC Core 1.0 Section 3.1.3.7 Step 2: Validate issuer
+    const expectedIssuer = this._settings.metadata?.issuer ?? this._settings.authority;
+    if (incoming.iss !== expectedIssuer) {
+        logger.throw(new Error(\`iss "\${incoming.iss}" does not match expected issuer "\${expectedIssuer}"\`));
+    }
+
+    // OIDC Core 1.0 Section 3.1.3.7 Step 3: Validate audience
+    const aud = Array.isArray(incoming.aud) ? incoming.aud : [incoming.aud];
+    if (!aud.includes(this._settings.client_id)) {
+        logger.throw(new Error(\`aud does not include client_id "\${this._settings.client_id}"\`));
+    }
+
+    // OIDC Core 1.0 Section 3.1.3.7 Step 9: Validate expiration
+    const now = Math.floor(Date.now() / 1000);
+    if (incoming.exp && incoming.exp < now) {
+        logger.throw(new Error("ID Token has expired"));
+    }

     if (nonce && incoming.nonce !== nonce) {
         logger.throw(new Error("nonce in id_token does not match nonce in client storage"));
     }
     // ... rest unchanged
 }

Cross-Library Comparison

Library iss check aud check exp check
openid-client (panva) Yes Yes Yes
auth0-spa-js Yes Yes Yes
passport-openidconnect Yes Yes Yes
oidc-client-ts Missing Missing Missing

Additional Context

Note on #1113: This report is specifically about claim validation (iss, aud, exp — OIDC Core Steps 2, 3, 9), NOT about cryptographic/signature validation (Step 6). In #1113, the maintainer correctly noted that TLS substitutes for signature verification in code flow (Step 6). However, the OIDC spec treats claim validation as separate MUST-level requirements that apply regardless of transport security. TLS ensures the token wasn't tampered with in transit — it does NOT ensure the token was issued for the correct client (aud), from the expected IdP (iss), or that it hasn't expired (exp). These are orthogonal concerns.

  • I attempted to report this through private vulnerability reporting, but it is not enabled for this repository and there is no SECURITY.md.
  • I am happy to submit a PR with the fix if the maintainers would prefer.
  • I am requesting a CVE be assigned for this vulnerability.

Reporter: Jayant Kamble (jayantkamble10000@gmail.com)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions