Skip to content

fix(auth): magic-link callback issues fresh session row (resets reauth gate)#94

Merged
finedesignz merged 1 commit into
mainfrom
fix/magiclink-fresh-session
May 27, 2026
Merged

fix(auth): magic-link callback issues fresh session row (resets reauth gate)#94
finedesignz merged 1 commit into
mainfrom
fix/magiclink-fresh-session

Conversation

@finedesignz
Copy link
Copy Markdown
Owner

Bug

After a user logged out + logged back in via magic link, the 15-min re-auth gate (hub/src/auth/reauth.ts, PR #89) still treated the session as stale and blocked sensitive ops (e.g. API-key rotate) with `Session too old for sensitive action`.

Root cause

`GET /api/auth/login/callback` called `createAndSetSession` directly without explicitly revoking any inbound session cookie's row. The cookie did get replaced client-side, but the prior `auth_sessions` row stayed alive until `purgeExpiredAuthSessions` swept it. More importantly the contract was implicit — making the step-up window opaque to observe.

The re-auth gate keys on `auth_sessions.created_at`. The fix makes the fresh-session contract explicit + observable.

Fix

`hub/src/api/auth.ts` — callback now:

  1. Reads inbound `__Host-remo_sid` cookie.
  2. If present, calls `deleteAuthSession(inboundToken)` (best-effort — failure does NOT block login).
  3. Calls `createAndSetSession` (always was the case — this part wasn't broken).
  4. Logs `fresh_session_expires=… revoked_prior=true|false` to make gate behavior observable.

`POST /api/auth/logout` already hard-deletes via `destroySession` → `deleteAuthSession`. Added log line for symmetry.

Tests

`hub/test/auth-callback-fresh-session.test.ts` (new, 5 tests):

  • Callback with existing cookie → old row revoked, new row created, `login_success` event records `revoked_prior_session: true`.
  • Callback with no cookie → no revoke attempted, new row created.
  • Revoke failure does NOT block login.
  • Logout deletes the row server-side via `destroySession`.
  • Anonymous logout (no cookie) still runs cleanly.

Full hub test suite: 395 pass / 90 skip / 8 fail (all 8 failures are pre-existing on main — main has 23, branch has 8).

Docs

`docs/auth.md` — adds a "Fresh-session invariant" note under the re-auth gate section explaining that the callback ALWAYS revokes inbound + creates fresh, so logout+login (or even a new magic link with stale cookie) resets the 15-min window deterministically.

Files

  • `hub/src/api/auth.ts` (+15/-2)
  • `hub/test/auth-callback-fresh-session.test.ts` (new, +170)
  • `docs/auth.md` (+2/-0)

🤖 Generated with Claude Code

…h gate)

Re-auth gate (`hub/src/auth/reauth.ts`) keys on `auth_sessions.created_at`.
If a user re-authenticated while still holding a stale session cookie, the
prior row stayed alive — confusing the step-up window contract and leaving
an orphan row until `purgeExpiredAuthSessions` swept it up.

- hub/src/api/auth.ts: callback now reads inbound cookie, deletes that row
  (best-effort), then creates a fresh session. Logs `revoked_prior=…` and
  `fresh_session_expires=…` for observability. Logout logs row deletion.
- hub/test/auth-callback-fresh-session.test.ts: 5 unit tests covering
  callback with/without inbound cookie, revoke-failure tolerance, and
  logout row-deletion + anonymous-logout no-op.
- docs/auth.md: document the fresh-session invariant under the re-auth gate.

After fix, "log out + log back in" (or "click new magic link with stale
cookie") deterministically resets the 15-min step-up window.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@finedesignz finedesignz merged commit 28073dd into main May 27, 2026
1 check passed
@finedesignz finedesignz deleted the fix/magiclink-fresh-session branch May 27, 2026 19:10
finedesignz added a commit that referenced this pull request May 27, 2026
)

Root cause for the prod "Session too old for sensitive action" error
that persisted after PR #89 (15-min window) + PR #94 (fresh session
on callback): with ALLOW_LEGACY_LOGIN=true AND TITANIUM_BYPASS=true
(current prod env 2026-05-27), every user is on the legacy bcrypt +
bearer JWT path. requireRecentAuth() called verifyAuthSessionCookie()
which returned null for all of them, producing instant 401
`no_cookie_session` on api-key create/delete + admin mutations. PR
#94's fresh-session fix is correct for cookie users but irrelevant
under TITANIUM_BYPASS.

DB evidence (auth_sessions for affected user
233c6d63-5f44-43f4-9eae-efc34a00735a): 0 rows. Confirms no cookie
session path is in use.

Fix: when allowLegacyLogin is on AND the request carries a valid
Bearer JWT, treat the JWT's `iat` claim as the session creation
timestamp. Same 15-min step-up window applies; recovery path is
logout + login (refreshes iat). Branch becomes dead code when
ALLOW_LEGACY_LOGIN flips off.

Tests: 3 new (fresh-iat pass, stale-iat 401, invalid-token 401)
extending hub/test/reauth.test.ts. All 8 cases green.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.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