Fix SSO flow-token / session type confusion (#32) #38
Reference in New Issue
Block a user
No description provided.
Delete Branch "bugfix/sso-token-confusion-32"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #32.
Problem
sessionFromDecoded(server/src/auth/session.service.js) validated sessions with a blocklist — it rejected a token only whendecoded.stagewas present (the TOTP challenge). Since every JWT is signed with the sameJWT_SECRETand distinguished only by claims, the SSO transaction cookie (sso_tx, carryingkind:'sso_tx'/id:'sso'but nostage) 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 — notablysiteMode's maintenance-preview bypass, which trusts any truthygetUserFromRequest. An attacker could start an SSO flow to obtain ansso_txhttpOnly cookie, then replay its value as the auth cookie /Authorization: Bearerto bypass the maintenance gate. The broader risk was latent: any future path trustingattachSession/getUserFromRequestwithout a DB round-trip inherited an auth bypass.Fix
Make session validation positively typed instead of blocklist-typed:
typ:'session'at mint time (createSession+mintMobileTokens).sessionFromDecodedaccepts a token only when that marker is present.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
typclaim 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: thesso_txflow token and a bare identity token are both rejected byvalidateSession/decodeIdentity/validateBearerToken/getUserFromRequest. Full suite: 98/98 passing.🤖 Generated with Claude Code
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>