Skip to content

fix(login): refuse hidden email-OTP sign-in everywhere it was still reachable (C10) - #130

Merged
yahyafakhroji merged 6 commits into
mainfrom
fix/otp-signin-guard-consistency
Sep 25, 2026
Merged

yahyafakhroji merged 6 commits into
mainfrom
fix/otp-signin-guard-consistency

Conversation

@yahyafakhroji

Copy link
Copy Markdown
Collaborator

What

#128 hid email-OTP sign-in behind EMAIL_OTP_SIGNIN_ENABLED = false, but only in the login view and the method chooser. Three readers of "is otp_email usable" were never updated, so the flow stayed reachable server-side:

  • shared/usable-methods.ts counted otp_email as a usable method whenever delivery was on, so the last-method guard in removeUserPasskey let a passkey + otpEmail account remove its only passkey and strand itself.
  • login/login-decision.ts still listed otp_email as an available primary method after the identifier step.
  • /login's intent=email-link action still executed on a direct POST.

This PR introduces one predicate, isEmailOtpSignInUsable(emailDeliveryEnabled) in resources/login/email-otp-signin.ts, and applies it at all three sites. intent=email-link now answers 400 while OTP sign-in is hidden. The chooser reads availability through the same predicate.

Why

Phase C of the passkey program (#112, item C10) decided that email OTP is a recovery ceremony, never a login. Hidden was not disabled. This must land before AUTH_EMAIL_DELIVERY_ENABLED flips in production, or the passkey-removal hole opens the moment mail is wired.

Tests

Six new Cypress cases across email-otp-signin.cy.ts, login-decision.cy.ts, passkeys.service.cy.ts, email-otp-signin-hidden.cy.ts (the direct-POST refusal, incl. no session cookie and no createSession), and the chooser spec. Full suite 773/773; enumeration-parity unchanged. The en.po diff is reference-line churn only.

Known, out of scope

  • resources/reauth/reauth.service.ts:130 still gates otp_email on delivery alone and dispatches a real OTP challenge for the sudo re-auth ceremony. Pre-existing. Re-auth is step-up for an already-signed-in user, not sign-in, so this is left as a documented exclusion rather than gated here; a follow-up can decide.
  • login-view.ts:48-50 inlines the same condition instead of calling the predicate; functionally identical today.
  • No CHANGELOG.md entry in this PR. The visible effect is that an account whose only other method is hidden email OTP can no longer remove its last passkey; say the word and I will add an Unreleased line.

Checklist

  • bun run typecheck passes
  • bun run lint:ci passes
  • bun run test:unit passes (773/773)
  • Public URLs unchanged
  • No raw PII in logs
  • bun run lint:boundaries passes (316 modules)

Refs #112.

@yahyafakhroji yahyafakhroji added the bug Something isn't working label Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Summary

Job Status
E2E (fast, fake provider) ✅ success
Unit Tests (component) (0) ✅ success
Unit Tests (component) (1) ✅ success
Unit Tests (component) (2) ✅ success

View workflow run

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Summary

Job Status
E2E (fast, fake provider) ✅ success
Unit Tests (component) (0) ✅ success
Unit Tests (component) (1) ✅ success
Unit Tests (component) (2) ✅ success

View workflow run

method-chooser.cy.ts's "known user → 302 to /login/verify/email" case was
asserting the hole: #128 hid the intent in the UI while the action still
honoured a crafted POST. Updated to expect the 400 the branch now returns,
with no redirect and no ceremony cookie. It flips back to the 302 when
EMAIL_OTP_SIGNIN_ENABLED does.
@yahyafakhroji
yahyafakhroji force-pushed the fix/otp-signin-guard-consistency branch from 81d0a8d to 60bd3c8 Compare September 25, 2026 07:52
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Summary

Job Status
E2E (fast, fake provider) ✅ success
Unit Tests (component) (0) ✅ success
Unit Tests (component) (1) ✅ success
Unit Tests (component) (2) ✅ success

View workflow run

@yahyafakhroji
yahyafakhroji merged commit 55303cc into main Sep 25, 2026
16 checks passed
@yahyafakhroji
yahyafakhroji deleted the fix/otp-signin-guard-consistency branch September 25, 2026 08:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants