secnull
systems nominal feed · live 23:01 UTC
8
audits
open cve
archivedispatchesdispatch 112
issue 112 · Apr 22, 2026
— dispatch · iam audit

The silent privilege-creep in three popular OIDC libraries — and why your staging cluster already leaked.

We audited passport-openidconnect, openid-client, and auth0-spa-js against the full OAuth 2.1 threat model. Two shipped mitigations we couldn't verify. One quietly disabled at_hash validation in v4.2. Here's the tape.

↓ signed manifest
passport-openidconnect C
We audited passport-openidconnect, openid-client, and auth0-spa-js against the full OAuth 2.1 threat model. Two shipped mitigations we couldn't verify. One quietly disabled at_hash validation in v4.2. Here's the tape.
64 / 100·3 high·6 med·npm
auth C
62/100
crypto B
78/100
supply C
48/100
docs D
71/100

§1 · Context

OpenID Connect turned sixteen last year. The spec is finally boring — which is a compliment — and yet the libraries most teams reach for keep finding new ways to get it wrong. This is not an indictment of the spec. It is a report on the state of its JavaScript implementations, measured against a threat model any adversary can read in public.

We picked three libraries that between them account for roughly 71% of weekly OIDC downloads on npm, as measured in the first quarter of 2026. Two are maintained in earnest. One is maintained by a vendor whose incentives are not aligned with yours, and the difference shows up in the code.

"The defaults you ship are the threat model 95% of your users will ever see." — Engineering folk wisdom, unattributed

§2 · Method & scope

For each library we performed: (a) a line-by-line read of the authentication flow, (b) a differential run of our OAuth 2.1 compliance corpus — 148 machine-checkable assertions lifted from RFCs 6749, 7636, 9126 and the OAuth 2.1 draft — and (c) a three-day fuzz of ID-token handling using a deliberately misbehaving identity provider we maintain for exactly this purpose.

We scored against six axes, with caps: auth correctness, crypto correctness, supply-chain hygiene, documentation, maintainer responsiveness, and default-safe ergonomics. A single high finding caps at C. Two caps at D. A silent downgrade of a security behaviour — as we will see in §3 — is an automatic F on that axis.

§3 · The at_hash regression

Of the three findings we rate high, this is the one that should embarrass the most people. In auth0-spa-js v4.2 — released quietly on a Friday in late February — the validation of the at_hash claim against the access token was moved behind an opt-in flag. The changelog notes "improved performance for token rotation." The diff removes a six-line check.

// auth0-spa-js@4.1 (removed in 4.2)
if (idToken.at_hash) {
  const expected = base64url(
    sha256(accessToken).slice(0, 16)
  );
  if (expected !== idToken.at_hash) {
    throw new TokenValidationError('at_hash mismatch');
  }
}

We reproduced the practical impact: an attacker able to inject an access token into a victim's SPA — via any of the usual XSS-adjacent primitives, or a compromised CDN — can pair it with a legitimate ID token from a concurrent sign-in and the library will accept the combination without complaint. The access token need not even belong to the authenticated subject. The check that prevents this is the one that was removed.

§4 · Refresh-token rotation, or the lack of it

passport-openidconnect has never rotated refresh tokens on its own. It is a middleware; rotation is, defensibly, an application concern. But the library's own documentation demonstrates refresh with a snippet that stores the refresh token in a signed cookie and never rotates it. Every real-world integration we audited — eleven of them, chosen from public GitHub dependents by weekly-downloads — followed the snippet. None rotated.

Rotation is in the OAuth 2.1 draft for a reason: a stolen refresh token is a stolen session, forever, until the subject logs out. That "forever" is measured in the months you will take to notice.

§5 · PKCE, mis-applied

PKCE is not optional in 2026. Two of the three libraries do the right thing and enforce it on public clients. passport-openidconnect accepts any code_challenge_method the provider responds with — including the string "plain", which defeats the entire point of PKCE and is forbidden by RFC 7636 §4.3 for anything that calls itself a security control.

§6 · What to do on Monday

  1. If you are on auth0-spa-js ≥ 4.2, set validateAtHash: true today. Consider pinning to 4.1 until the default is restored.
  2. If you use passport-openidconnect, audit your refresh-token storage. Rotate on every use. Revoke on logout. Log rotation failures.
  3. Pin PKCE to S256 at the client level. Do not trust the provider to negotiate it for you.
  4. Subscribe to the re-audit feed for these packages. We will re-grade when the defaults change — not when the PR lands.

§7 · Maintainer responses

We sent drafts to all three projects on apr 11, with 10-day response windows. openid-client replied within 36 hours, accepted the documentation findings, and shipped clarifying docs before publication. passport-openidconnect acknowledged receipt and committed to a rotation example in the README. auth0-spa-js did not respond.