Fix SSO flow-token / session type confusion (#32) #38

Merged
whitlocktech merged 1 commits from bugfix/sso-token-confusion-32 into main 2026-07-05 02:56:33 +00:00
Member

Closes #32.

Problem

sessionFromDecoded (server/src/auth/session.service.js) validated sessions with a blocklist — it rejected a token only when decoded.stage was present (the TOTP challenge). Since every JWT is signed with the same JWT_SECRET and distinguished only by claims, the SSO transaction cookie (sso_tx, carrying kind:'sso_tx' / id:'sso' but no stage) passed validation and was accepted as a bogus { userId:'sso' } session.

requireAuth's DB re-load (getById('sso') → no row → 401) blocked protected admin routes, but non-DB identity checks were fooled — notably siteMode's maintenance-preview bypass, which trusts any truthy getUserFromRequest. An attacker could start an SSO flow to obtain an sso_tx httpOnly cookie, then replay its value as the auth cookie / Authorization: Bearer to bypass the maintenance gate. The broader risk was latent: any future path trusting attachSession/getUserFromRequest without a DB round-trip inherited an auth bypass.

Fix

Make session validation positively typed instead of blocklist-typed:

  • Real sessions are stamped with typ:'session' at mint time (createSession + mintMobileTokens).
  • sessionFromDecoded accepts a token only when that marker is present.
  • Belt-and-suspenders: it also rejects any token carrying a non-session marker (stage || kind), so a future minting path that forgets to omit those still can't produce an accepted session.

Flow/challenge tokens (TOTP challenge, sso_tx) are never stamped, so they can no longer be mistaken for sessions. siteMode's bypass is closed as a direct consequence.

Compatibility

Existing web cookie sessions predating this change lack the typ claim and will be rejected once — users re-login. Mobile clients recover automatically on their next token refresh.

Tests

Adds regression coverage in server/test/session.test.js: the sso_tx flow token and a bare identity token are both rejected by validateSession / decodeIdentity / validateBearerToken / getUserFromRequest. Full suite: 98/98 passing.

🤖 Generated with Claude Code

Closes #32. ## Problem `sessionFromDecoded` (`server/src/auth/session.service.js`) validated sessions with a **blocklist** — it rejected a token only when `decoded.stage` was present (the TOTP challenge). Since every JWT is signed with the same `JWT_SECRET` and distinguished only by claims, the SSO transaction cookie (`sso_tx`, carrying `kind:'sso_tx'` / `id:'sso'` but no `stage`) passed validation and was accepted as a bogus `{ userId:'sso' }` session. `requireAuth`'s DB re-load (`getById('sso')` → no row → 401) blocked protected admin routes, but **non-DB identity checks were fooled** — notably `siteMode`'s maintenance-preview bypass, which trusts any truthy `getUserFromRequest`. An attacker could start an SSO flow to obtain an `sso_tx` httpOnly cookie, then replay its value as the auth cookie / `Authorization: Bearer` to bypass the maintenance gate. The broader risk was latent: any future path trusting `attachSession`/`getUserFromRequest` without a DB round-trip inherited an auth bypass. ## Fix Make session validation **positively typed** instead of blocklist-typed: - Real sessions are stamped with `typ:'session'` at mint time (`createSession` + `mintMobileTokens`). - `sessionFromDecoded` accepts a token **only** when that marker is present. - Belt-and-suspenders: it also rejects any token carrying a non-session marker (`stage || kind`), so a future minting path that forgets to omit those still can't produce an accepted session. Flow/challenge tokens (TOTP challenge, `sso_tx`) are never stamped, so they can no longer be mistaken for sessions. `siteMode`'s bypass is closed as a direct consequence. ## Compatibility Existing **web cookie** sessions predating this change lack the `typ` claim and will be rejected once — users re-login. **Mobile** clients recover automatically on their next token refresh. ## Tests Adds regression coverage in `server/test/session.test.js`: the `sso_tx` flow token and a bare identity token are both rejected by `validateSession` / `decodeIdentity` / `validateBearerToken` / `getUserFromRequest`. Full suite: **98/98 passing**. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
wtclaude added 1 commit 2026-07-05 02:55:31 +00:00
sessionFromDecoded validated sessions with a blocklist — it rejected a
token only when `decoded.stage` was present (the TOTP challenge). Because
every JWT is signed with the same JWT_SECRET and distinguished only by
claims, the SSO transaction cookie (sso_tx, which carries kind:'sso_tx'
and id:'sso' but no stage) passed validation and was accepted as a bogus
{ userId:'sso' } session.

requireAuth's DB re-load blocked protected admin routes, but non-DB
identity checks were fooled — notably siteMode's maintenance-preview
bypass, which trusts any truthy getUserFromRequest. An attacker could
start an SSO flow to obtain an sso_tx cookie and replay it as the auth
cookie / Bearer token to bypass the maintenance gate. The broader risk
was latent: any future code path trusting attachSession/getUserFromRequest
without a DB round-trip inherited an auth bypass.

Make session validation positively typed: real sessions are now stamped
with typ:'session' (createSession + mintMobileTokens), and
sessionFromDecoded accepts a token only when that marker is present. As
belt-and-suspenders it also rejects any token carrying a non-session
marker (stage || kind). Flow/challenge tokens are never stamped, so they
can no longer be mistaken for sessions.

Note: existing web cookie sessions predating this change lack the typ
claim and will be rejected once — users re-login. Mobile clients recover
automatically on next refresh.

Adds regression tests: the sso_tx flow token and a bare identity token
are both rejected by validateSession / decodeIdentity / getUserFromRequest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
whitlocktech approved these changes 2026-07-05 02:56:27 +00:00
whitlocktech merged commit 15cf8ea286 into main 2026-07-05 02:56:33 +00:00
whitlocktech deleted branch bugfix/sso-token-confusion-32 2026-07-05 02:56:34 +00:00
Sign in to join this conversation.
No description provided.