Skip to content

Security: a two-factor challenge is not a session - #220

Merged
davior merged 1 commit into
mainfrom
claude/beautiful-wright-og4xur
Nov 4, 2026
Merged

davior merged 1 commit into
mainfrom
claude/beautiful-wright-og4xur

Conversation

@davior

@davior davior commented Nov 4, 2026

Copy link
Copy Markdown
Owner

Summary

A 2FA bypass, live in production; this is the source of it. When someone with two-factor on signs in, POST /api/auth/login returns a five-minute challenge token for the second step. It is signed with JWT_SECRET_KEY and carries the user's sub. decode_token, which every session check uses (the API middleware and the live-audio websockets stt_stream and flux_stream), did not look at the twofa_pending claim. So the challenge worked as a full session: the password alone opened the API, as a bearer header or as the gecko_session cookie. The same token opened GAM and GVC (fixed in gam#62 and gvc#41, both merged) and the suite (fixed in its M6 accounts work).

  • decode_token now refuses a token with twofa_pending (it raises JWTError, so every caller answers 401 as for any bad token).
  • The second step reads its challenge through decode_challenge_token, which decodes directly and still requires the claim, so 2FA sign-in works exactly as before.

Test plan

  • New in tests/test_auth_cookie.py:
    • test_a_two_factor_challenge_does_not_open_the_api: a challenge is refused as a bearer header and as the cookie.
    • test_the_second_step_still_reads_its_challenge: the challenge still works for step two, and a session still cannot stand in for a challenge.
  • Full pytest -q: 1399 passed, 1 skipped.

Deployment

  • Services rebuilt: backend only (docker compose up --build -d). No configuration, no migrations.
  • Verify: curl -f http://localhost:18081/api/health. Signing in with 2FA still works end to end (password, then code). Normal sessions are unaffected.
  • Effect on people: none, except that a half-finished 2FA sign-in no longer works as a session. Nobody legitimately uses one.
  • Roll back: redeploy the previous commit (which reopens the gap).

Generated by Claude Code

The password step of a 2FA sign-in returns a five-minute challenge, signed with the shared
JWT secret and carrying the user's sub. decode_token, which every session check uses (the
API middleware and the live-audio websockets), accepted it, so the password alone opened
the API. decode_token now refuses it; the second step reads it through
decode_challenge_token as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1geD2ELWgL2sDpxYbx3Aw
@davior
davior marked this pull request as ready for review November 4, 2026 20:52
@davior
davior merged commit 8c1bcd4 into main Nov 4, 2026
2 checks passed
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.

2 participants