Repository navigation
app: drop fetchSignInMethodsForEmail from login flow - #735
Conversation
The email login flow decided which card to show by calling Firebase's deprecated fetchSignInMethodsForEmail and branching on the returned methods. That API was deprecated precisely because it enables email enumeration, and it has a latent footgun: when a project enables Email Enumeration Protection (a single console toggle Firebase recommends and defaults to ON for projects created after 2023-09-15), the call returns [] for every address. The old flow read that as no account exists and routed every existing password user to the Create account card, where signup then failed with email-already-in-use -- with no in-UI path to the password card or the Trouble signing in? recovery link. Enabling a recommended security setting would have silently locked out all email/password users. Replace the method-prefetch router with a single combined sign-in card (email + password together) that attempts signInWithEmailAndPassword directly and branches on the Firebase error code. The flow is now correct whether Enumeration Protection is on or off: - invalid-credential / wrong-password / user-not-found and any non-Firebase throw collapse to one generic, enumeration-safe message; invalid-email gets a distinct message; network-request-failed and too-many-requests (account-agnostic, so they leak nothing) report a transient try-again message instead of masquerading as a bad password. - Password reset shows a neutral confirmation (If an account exists...) so it stays enumeration-safe even though sendPasswordResetEmail silently succeeds for unknown addresses. - New users reach signup via an always-present Create account link rather than auto-routing on user-not-found, which would itself leak existence when protection is off. Removing the prefetch means we can no longer name which provider an OAuth-only user signed up with (wrong password and OAuth-only account are indistinguishable under protection), so the provider-named redirect card is gone; such users get the generic message and the provider buttons remain one Cancel away on the landing screen. The component only drives the Firebase client SDK into a signed-in state; App's onAuthStateChanged observer still handles the /session handoff, so the change is confined to Login.tsx and its tests. Tests cover the full flow at 100% statement/branch/line coverage. Closes #692
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: df3ac30ad0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Neutral confirmation only: sendPasswordResetEmail succeeds even for | ||
| // addresses with no account when Email Enumeration Protection is on, so we | ||
| // must not assert an email was actually sent to a real account. | ||
| setState({ emailLoginFlow: 'recoverSent' }); |
There was a problem hiding this comment.
Keep recovery responses neutral on user-not-found
When Email Enumeration Protection is disabled (or in environments such as older Firebase projects/emulators), sendPasswordResetEmail rejects unknown addresses with auth/user-not-found; this handler returns from the catch and renders the raw error instead of showing the neutral confirmation. Because the new flow exposes “Trouble signing in?” without first proving the account exists, arbitrary emails can now be enumerated through password recovery, despite the neutral copy added here. Treat auth/user-not-found like success (while still surfacing invalid-email/network/config errors) so the recovery path stays enumeration-safe in both Firebase modes.
Useful? React with 👍 / 👎.
ReviewI reviewed the diff against the original method-prefetch flow and against the stated goal of being correct whether Email Enumeration Protection is on or off. I did not find any concrete bugs introduced by this PR that meet the flagging bar. Things I checked carefully and consider sound:
Pre-existing and noted by the PR description (not introduced here): Overall correctness verdictCorrect. No blocking issues found. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #735 +/- ##
=======================================
Coverage 90.60% 90.60%
=======================================
Files 224 224
Lines 136247 136247
=======================================
Hits 123447 123447
Misses 12800 12800 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Closes #692.
Problem
The email login flow in
src/app/Login.tsxdecided which card to show by calling Firebase's deprecatedfetchSignInMethodsForEmailand branching on the returned methods. That API was deprecated precisely because it enables email enumeration, and it carries a latent footgun:When a project enables Email Enumeration Protection (a single Firebase-console toggle Firebase recommends, default-ON for projects created after 2023-09-15),
fetchSignInMethodsForEmailreturns[]for every address. The old flow read that as "no account exists" and routed every existing email/password user to the "Create account" card, where signup then failed withauth/email-already-in-use— with no in-UI path to the password card or the "Trouble signing in?" recovery link. Enabling a recommended security setting would have silently and completely locked out all email/password users.Fix
Replace the method-prefetch router with a single combined sign-in card (email + password together) that attempts
signInWithEmailAndPassworddirectly and branches on the Firebase error code. The flow is now correct whether Enumeration Protection is on or off:invalid-credential/wrong-password/user-not-foundand any non-Firebase throw collapse to one generic, enumeration-safe message;invalid-emailgets a distinct message.network-request-failed/too-many-requestsare account-agnostic (they leak nothing), so they report a transient "try again" message instead of masquerading as a bad password.sendPasswordResetEmailsilently succeeds for unknown addresses.user-not-found(which would itself leak existence when protection is off).Known tradeoff
Removing the prefetch means we can no longer name which provider an OAuth-only user signed up with — a wrong password and an OAuth-only account are indistinguishable under protection. The provider-named "you already have an account" redirect card is therefore gone; such users get the generic message and the Apple/Google buttons remain one "Cancel" away on the landing screen. This is inherent to removing enumeration (you can't hide which provider an email uses and name it).
Scope
Login.tsxonly drives the Firebase client SDK into a signed-in state;App.tsx'sonAuthStateChangedobserver still handles theidToken→POST /sessionhandoff, so the change is confined toLogin.tsxand its tests.Testing
src/app/tests/login.test.tsx, 100% statement/branch/function/line coverage onLogin.tsx.fetchSignInMethodsForEmailnever called in any path; each error-code branch (incl. transient and non-Firebase throws); create-account +email-already-in-use→ "sign in instead"; recovery → neutral confirmation and failure-stays-with-error; Enter-to-submit structural guard (Sign in istype=submit, Cancel is not); cross-card error clearing; disabled render guard.Process
Design brainstormed, then adversarially reviewed by a sub-agent before implementation; the completed change was adversarially reviewed by a second fresh sub-agent and iterated to a clean "ship" verdict.
Note (out of scope)
Passwords are
.trim()'d before being sent to Firebase on both sign-in and signup (pre-existing onmain, carried forward unchanged). It's consistent between the two paths, but altering passwords with intentional leading/trailing whitespace is latent; flagging for a possible follow-up.