handleVerify in /mfa/verify ended its default path with router.push(...) followed by router.refresh(). Verifying the challenge raises the session to aal2, which lib/supabase/middleware.ts only re-evaluates on a fresh document request; the push and the refresh raced, the refresh won, and the user stayed on the code screen with a session that was already aal2. Re-entering the same TOTP code is then rejected as reuse, which bumps the lockout counter and makes MFA look broken. PR #1984 closed the identical shape in leave() on /mfa/enroll (#1948) and listed this file as the follow-up. Always window.location.assign(...) instead, exactly as #1984 did, and fold the now-redundant /api/ returnTo branch into the same call. The WL-14 cockpit landing is unchanged: resolvePostLoginDestination() is still awaited before navigating, and it only ever returns '/clients' or '/'. returnTo is already validated by safeReturnTo, so the unconditional hard navigation stays same-origin. The invite path keeps its own window.location.href = '/' since it deliberately ignores returnTo. Accepted trade-off, same as #1984 and DECISIONS.md 2026-07-26: a toast fired before the hard navigation does not survive the full page load. On this page that is the invite-problem warning; the invite cookie survives non-definitive outcomes so /onboarding and /select-company retry acceptance server-side. Fixes #2056 Claude-Session: https://claude.ai/code/session_015qgLgdt4mLmha1ZLFMwq1u Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -145,20 +145,23 @@ function MfaVerifyContent() {
|
||||
return
|
||||
}
|
||||
|
||||
if (returnTo.startsWith('/api/')) {
|
||||
// Route-handler destinations (e.g. the MCP OAuth consent page)
|
||||
// return raw HTML the client router cannot render: hard-navigate.
|
||||
window.location.assign(returnTo)
|
||||
return
|
||||
}
|
||||
|
||||
// Hosted byrå staff hit MFA before any dashboard, so the cockpit
|
||||
// landing (WL-14) resolves here too: only when no explicit step-up
|
||||
// destination was requested. Everyone else keeps returnTo/'/' exactly
|
||||
// as before (the helper degrades to '/' on any failure). The session
|
||||
// is AAL2 at this point, so the /api MFA gate passes.
|
||||
router.push(returnTo === '/' ? await resolvePostLoginDestination() : returnTo)
|
||||
router.refresh()
|
||||
//
|
||||
// Always a hard navigation, for two reasons that point the same way.
|
||||
// Route-handler destinations (e.g. the MCP OAuth consent page) return
|
||||
// raw HTML the client router cannot render. And verifying raises the
|
||||
// session to aal2, which lib/supabase/middleware.ts only re-evaluates
|
||||
// on a fresh document request: `router.push` followed by
|
||||
// `router.refresh` raced, the refresh won, and the user stayed on the
|
||||
// code screen; re-entering the same code is then rejected as reuse and
|
||||
// bumps the lockout counter (#2056, the shape #1984 fixed on enroll).
|
||||
// returnTo went through safeReturnTo and the helper only ever returns
|
||||
// '/clients' or '/', so the navigation stays same-origin.
|
||||
window.location.assign(returnTo === '/' ? await resolvePostLoginDestination() : returnTo)
|
||||
} catch {
|
||||
toast({
|
||||
title: t('verify_failed_title'),
|
||||
|
||||
Reference in New Issue
Block a user