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
- The library enforces authorization code flow only, so ID tokens arrive over TLS backchannel — mitigates direct injection.
- State and PKCE are correctly implemented.
- 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)
Summary
ResponseValidator._validateIdTokenAttributes()(src/ResponseValidator.ts, lines 195-226) does not validate theiss(issuer),aud(audience), orexp(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()andMetadataService.getSigningKeys()methods defined but never called anywhere insrc/— the validation infrastructure exists but was never wired up.client_idis available atthis._settings.client_id(used at line 131) but is never compared against theaudclaim.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
Dead Infrastructure Proof
OIDC Specification Violations
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 —
audis never compared againstclient_id.2. Issuer Mix-Up (Missing
iss)RFC 9207 mix-up attacks become possible when an application interacts with multiple IdPs —
issis 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
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
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.Reporter: Jayant Kamble (jayantkamble10000@gmail.com)