Skip to content

Fix SMS auth channel on login and signup - #312

Merged
kaareal merged 1 commit into
masterfrom
feature/pr-304-review-a74401
Sep 23, 2026
Merged

kaareal merged 1 commit into
masterfrom
feature/pr-304-review-a74401

Conversation

@kaareal

@kaareal kaareal commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

AUTH_CHANNEL=sms is a supported configuration that could not be used from the UI. Both auth screens assumed the email channel. Verified end to end in the browser against a local API on both channels.

Why it was broken

Login collected only an email, so with channel: 'sms' the API resolved the user by email and then delivered by SMS. Three outcomes:

Case Before
User exists, has a phone Worked
User exists, no phone 500 — sendSms throws "No phone number specified."
No user 200, but createChallenge reads phone off the request body, which had none. The confirm screen rendered "code sent to ." and typing a code did nothing — its guard requires an identifier, so no request was ever made.

Signup had the inverse problem: it required an email and made the phone optional whatever the channel, while routes/signup.js requires the identifier that matches the channel. Under SMS the form demanded an email the API does not want and allowed submitting without the phone it does, so the failure arrived as a server error in the page-level banner instead of on the field.

What changed

  • Login picks the identifier from the channel — a phone input under SMS, email otherwise — and sends whichever one applies. Identifying by phone means the resolved user always has one, which closes both failure rows above.
  • Signup derives the required identifier from the channel, plus an email whenever AUTH_TYPE=password, since /auth/password/login only accepts an email. Without that, a password + sms configuration would create phone-only accounts that can never log in.
  • Both request bodies are whitelisted. The routes reject unknown fields, so any future form field would otherwise break the request the moment it is added.
  • normalizePhone moves from Signup.js into utils/phone, since both screens now need it.

Layout

Making the code flow reachable exposed that /confirm-code sat on BasicLayout while /login and /signup use SplitAuthLayout — different card width, vertical position, logo and background, all swapping mid-flow. BasicLayout also owned chrome its consumers disagreed about: Onboard brought its own logo and card, so it rendered two logos in nested cards.

BasicLayout is now a centered shell and each screen supplies its own logo and card through the new AuthLogo. /confirm-code deliberately keeps BasicLayout rather than moving to the split layout, so the marketing panel stays off the code step.

Reviewer notes

  • This supersedes the Login.js hunk in Fix signup email, user updates and code login #304. It includes that PR's authChannel → channel rename and the dropped password field, so the two will conflict — merge order matters.
  • The other two fixes in Fix signup email, user updates and code login #304 (the template ENAMETOOLONG and the unique-check exclusion) are untouched here.
  • Web has no screen tests, so none were added. Verified manually instead: both channels complete login to the dashboard, an unknown phone now returns a real "User not found." rather than a dead screen, and signup validates the right field per channel.
  • Not fixed, pre-existing: /otp/send returns a challenge whether or not the user exists, but /otp/login then answers "User not found." versus "Incorrect code.", so the enumeration protection at the send step is undone at the confirm step on both channels.

The auth screens assumed the email channel. `AUTH_CHANNEL=sms` is a
supported configuration but was unreachable from the UI:

- Login collected only an email. With `channel: 'sms'` the API resolved
  the user by email, so a user without a phone got a 500 from `sendSms`,
  and an unknown identifier produced a challenge with no recipient — a
  confirm screen with a blank target where entering a code did nothing.
  Login now collects a phone when the channel is SMS and sends whichever
  identifier matches, so the challenge always carries one back.
- Signup required an email and made the phone optional regardless of
  channel, the opposite of what the signup route validates. The required
  identifier now follows the channel, plus an email whenever
  `AUTH_TYPE=password`, since password login only accepts one.
- Both request bodies are whitelisted. The routes reject unknown fields,
  so a new form field would otherwise break the request.

`normalizePhone` moves from Signup into `utils/phone` now that both
screens use it.

Also aligns the code-entry screen with the screens around it, which the
above makes reachable for the first time. `BasicLayout` was drawing a
different card width, position and logo mid-flow, and owned chrome its
consumers disagreed about — Onboard was rendering a second logo inside a
nested card. The layout is now a centered shell and each screen supplies
its own logo and card via the new `AuthLogo`.
@kaareal
kaareal merged commit b35aed1 into master Sep 23, 2026
3 checks passed
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