active? was only checked at the password step of sign-in. A user disabled
afterwards could (a) still complete the 2FA step and mint a valid session, and
(b) keep using any existing session until natural expiry, because per-request
auth only checked session expiry, not user status.
Three enforcement points:
- Mid-flow guard: verify_totp and webauthn_verify re-check active? before
start_new_session_for, clearing the pending session and rejecting if disabled.
- Request-time guard: find_session_by_cookie now uses Session.for_active_user,
so a session whose user is disabled no longer authenticates (authoritative,
catches any disable path including direct DB changes).
- Immediate cleanup: User#revoke_sessions_when_deactivated destroys a user's
sessions when status changes away from active, so access is revoked everywhere
at once rather than on the next request.
Tests cover the mid-flow TOTP rejection, request-time rejection of an existing
session after disable, session destruction on disable, and that unrelated
updates leave sessions intact.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Safari enforces form-action against every hop in a form submission's
redirect chain. When a user signed in (with TOTP, or through a
skip_consent OIDC app), the chain /signin or /totp-verification ->
/oauth/authorize -> external client got blocked at the cross-origin
hop because form-action was 'self'. The existing dynamic CSP widening
in OidcController#authorize only ran when the consent page rendered,
so skip_consent and pre-consented flows had no widening at all.
Add allow_oauth_redirect_in_csp on the sign-in and TOTP pages, which
pulls the OAuth redirect_uri out of session[:return_to_after_authenticating]
and appends its host to form-action for the rendered page.
- Add "Remember me for 30 days" checkbox (30-day vs 24-hour session expiry)
- Center heading and constrain form width to max-w-md
- Preserve remember_me preference through TOTP and WebAuthn auth flows
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a user has both passkeys and TOTP configured, auto-trigger the
passkey flow on login to save them from the password→TOTP path. Also
add a "Use Passkey Instead" button on the TOTP verification page as
an escape hatch for users who end up there.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace CGI.parse (removed in Ruby 4.0) with Rack::Utils.parse_query
in application controller, sessions controller, and OIDC tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>