Skip to content

fix(middleware): fail-closed JWT/OAuth auth adapters (C2)#59

Open
ucekmez wants to merge 1 commit into
fix/py-middleware-gate-resolution-ssrffrom
fix/auth-adapters-fail-closed
Open

fix(middleware): fail-closed JWT/OAuth auth adapters (C2)#59
ucekmez wants to merge 1 commit into
fix/py-middleware-gate-resolution-ssrffrom
fix/auth-adapters-fail-closed

Conversation

@ucekmez

@ucekmez ucekmez commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

C2 — Fail-closed JWT / OAuth auth adapters

Stacked on C1 (#58). Base branch is fix/py-middleware-gate-resolution-ssrf so this PR's diff shows only the C2 changes. Merge #58 first (or retarget to main after #58 lands).

The vulnerability

The @eep-dev/middleware (TS) and eep-middleware (Python) auth adapters converted unverified input into trusted EEP proofs:

  • JWTAuthAdapter base64-decoded the JWT payload and emitted { type: "identity", method: "did_verified" } plus capability proofs without checking the signature or the alg header. An alg: none token — or any forged token — was accepted, so a caller could impersonate any DID and grant itself any capability/scope. Combined with structural-only proof checks (or any verifier that trusts did_verified), this is a full authentication bypass.
  • The TS OAuthAuthAdapter read scopes straight from a client-supplied X-OAuth-Scope header / scope query param, so a caller could assert arbitrary capabilities.

The fix (both languages, fail-closed, at parity)

JWTAuthAdapter

  • Rejects alg: none (any casing) unconditionally.
  • Verifies HS256/384/512 against a configured secret with a constant-time compare.
  • Delegates RSA / ECDSA / EdDSA (or any custom alg) to a verifyToken / verify_token callback that returns verified claims or null (wrap jose / PyJWT / your AS). Sync and async callbacks supported.
  • Enforces exp / nbf / iat with a configurable clock-skew tolerance (default 60s).
  • Emits no proofs (and warns once at construction) when neither secret nor verifyToken is configured.
  • Algorithm-confusion safe: an HMAC secret never verifies an asymmetric token, and the asymmetric callback is never used to rubber-stamp an HS token. An explicit algorithms allowlist is honored.

OAuthAuthAdapter (TypeScript)

  • Requires an RFC 7662 introspect callback (mirrors APIKeyAuthAdapter's required resolver).
  • Scope and subject DID come only from the authorization server's response; the X-OAuth-Scope header is ignored.

EEPServer defaults are unaffected (it uses HeaderProofAuthAdapter). The TS JWTAuthAdapterOptions gains optional secret / verifyToken / algorithms / clockToleranceSec (non-breaking). The TS OAuthAuthAdapter now takes a required { introspect } option (behavioral break for the previously trust-the-header constructor — justified, since the old behavior was the vulnerability).

Files

  • packages/@eep-dev/middleware/src/auth/jwt.ts — native HS verification + verifyToken delegation + temporal checks.
  • packages/@eep-dev/middleware/src/auth/oauth.ts — introspection-based, header no longer trusted.
  • packages/@eep-dev/middleware/src/index.ts — export new option types.
  • packages/eep-middleware-python/eep_middleware/auth/jwt.py — Python parity (stdlib hmac).
  • READMEs (TS + Python) — new "Authentication adapters" guidance.
  • CHANGELOG.md — Security entry.

Test plan

  • TS npx vitest run --coverage (Node 22, mirrors CI test-middleware): 94 passed; jwt.ts, oauth.ts, index.ts at 100% stmts/branch/funcs/lines.
  • TS tsc --noEmit: clean.
  • Python pytest with the package gate --cov-fail-under=100: 48 passed, total coverage 100% (jwt.py 100% statements + branches).
  • scripts/verify-llms-docs.py: OK (no doc drift).
  • New tests: signed/forged/tampered/expired/alg:none/wrong-length-sig/algorithm-confusion/allowlist/clock-tolerance, verifyToken delegation (sync+async, null, throw, temporal), OAuth introspection (active/inactive/null/throw/empty-scope/header-ignored), and fail-closed construction warnings.

Notes for reviewer

  • Native verification is intentionally limited to HMAC (the EEP ecosystem's existing primitive); asymmetric verification is delegated via the callback to avoid bundling/hand-rolling RSA/ECDSA. No new mandatory dependencies were added.
  • This is the second item from the security audit (C2). C3 (gates default-tier wildcard specificity bypass) follows next, stacked on this branch.

The auth adapters in `@eep-dev/middleware` (TypeScript) and `eep-middleware`
(Python) turned attacker-controlled credentials into trusted EEP proofs:

- `JWTAuthAdapter` base64-decoded the JWT payload and emitted
  `{type:"identity", method:"did_verified"}` and capability proofs WITHOUT
  verifying the signature or the `alg` header. An `alg: none` token, or any
  forged token, was accepted at face value.
- The TypeScript `OAuthAuthAdapter` derived capability proofs straight from a
  client-supplied `X-OAuth-Scope` header (or `scope` query param), so a caller
  could assert any scope it wanted.

Both adapters now fail closed:

- `JWTAuthAdapter` rejects `alg: none` unconditionally, verifies HS256/384/512
  signatures against a configured `secret` (constant-time compare), delegates
  asymmetric/custom algorithms to a `verifyToken` / `verify_token` callback,
  enforces `exp`/`nbf`/`iat` with a configurable clock-skew tolerance (60s
  default), and emits no proofs (warning once) when no verification material is
  configured. The algorithm router never lets an HMAC secret verify an
  asymmetric token, and never lets the asymmetric callback rubber-stamp an HS
  token. An explicit `algorithms` allowlist is supported.
- `OAuthAuthAdapter` requires an RFC 7662 `introspect` callback and reads scope
  and subject only from the authorization server's response. A client-supplied
  `X-OAuth-Scope` header is ignored.

The TypeScript `JWTAuthAdapterOptions` is extended (non-breaking) with `secret`,
`verifyToken`, `algorithms`, and `clockToleranceSec`; `OAuthAuthAdapter` now
takes a required `{ introspect }` option (mirrors `APIKeyAuthAdapter`'s required
resolver). `EEPServer` defaults are unaffected (it uses `HeaderProofAuthAdapter`).

Tests cover signed/forged/tampered/expired tokens, `alg: none`, algorithm
confusion, the allowlist, callback delegation (sync + async), introspection
states, and fail-closed construction. Coverage of the changed files is 100%
(TS jwt.ts/oauth.ts; Python jwt.py at 100% statements + branches, package gate
`--cov-fail-under=100` satisfied). READMEs and CHANGELOG updated.

Surfaced by the EEP protocol audit.

Signed-off-by: Ugur Cekmez <ucekmez@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant