Skip to content

feat: mobile OTP login for Indian phone numbers#5

Merged
hashd merged 15 commits into
masterfrom
feature/mobile-otp-login
Apr 12, 2026
Merged

feat: mobile OTP login for Indian phone numbers#5
hashd merged 15 commits into
masterfrom
feature/mobile-otp-login

Conversation

@hashd
Copy link
Copy Markdown
Owner

@hashd hashd commented Apr 12, 2026

Summary

  • Adds phone-number-based sign-up and login via 6-digit SMS OTP (MSG91 provider, India-only +91 numbers)
  • Email becomes optional — users can sign up with just their phone number
  • New phone_otp_codes table with hashed OTP storage, attempt limiting (3 per OTP, 5 global per 10min), and rate limiting (3 requests per 10min)
  • SMS delivery via SMSProvider behaviour with swappable adapters: MSG91 (prod), Log (dev), Test (tests)
  • Frontend Auth page gets Phone/Email tab switcher with 3-state OTP flow (phone entry → OTP entry → name entry for new users)
  • Removes dev auto-login — auth is now required in all environments

Security

  • OTPs stored as SHA-256 hashes, never plaintext
  • Timing-safe hash comparison via Plug.Crypto.secure_compare
  • SELECT ... FOR UPDATE in transaction prevents TOCTOU race on concurrent verify
  • Global attempt counter (5 per 10min) prevents resend-based brute force
  • Anti-enumeration: OTP request always returns 200 regardless of phone existence
  • Failed SMS delivery cleans up OTP row to prevent rate-limit lockout

Test plan

  • 125 backend tests + 2 properties, 0 failures
  • TypeScript strict check passes
  • Manual test: phone entry → OTP logged to console → verify → name entry → logged in
  • Production: configure MSG91_AUTH_KEY and MSG91_TEMPLATE_ID env vars
  • Production: register DLT template with MSG91 for TRAI compliance

hashd added 15 commits April 13, 2026 01:33
Phone-number-based sign-up and login via 6-digit SMS OTP (MSG91),
with dedicated phone_otp_codes table and post-verify name collection
for new users.
Address architect, critic, and QA lead review feedback:
- Specify User changeset split (phone_registration_changeset)
- Add phone to Jason.Encoder derive list
- Wrap verify in transaction with SELECT FOR UPDATE
- Invalidate old OTPs on resend
- Add SMS behaviour + test/dev/prod adapters
- India-only phone validation with normalization module
- Handle MSG91 delivery failure (503)
- Acknowledge duplicate account limitation
- Expand test plan from 2 files to comprehensive coverage
- Add frontend error handling per 429 variant
- Add phone to User type in domain.ts
10-task TDD plan covering migration, phone normalization,
PhoneOtpCode schema, User schema changes, SMS provider behaviour,
Auth context OTP functions, controller endpoints, and Vue frontend.
Pure Moth.Auth.Phone.normalize/1 that strips formatting characters and
returns E.164 (+91XXXXXXXXXX) for Indian mobile numbers (6-9 prefix),
rejecting invalid/non-Indian input with {:error, :invalid_phone}.
- Make User.email nullable and add optional phone field
- Add requestOtp and verifyOtp methods to api.auth
- Replace Auth.vue with phone/email tab switcher: phone entry → OTP entry (with 30s resend cooldown and attempt-count errors) → name capture for new users; email tab retains magic link + Google OAuth
- Wrap verify_phone_otp in a single Repo.transaction with FOR UPDATE
  locking to eliminate TOCTOU race on concurrent verify requests
- Use Plug.Crypto.secure_compare for timing-safe hash comparison
- Add global attempt counter (sum across all OTP rows in 10-min window,
  cap at 5) to prevent attempt-count reset via resend abuse
- Handle concurrent-insert race in find_or_create_phone_user by catching
  unique constraint violation and retrying lookup
- Delete OTP row on SMS delivery failure so it doesn't count against
  the rate limit
- Add fallback clauses for request_otp/verify_otp to return 422 instead
  of 500 when phone/code params are missing
@hashd hashd merged commit 4d6249d into master Apr 12, 2026
@hashd hashd deleted the feature/mobile-otp-login branch April 12, 2026 21:10
hashd added a commit that referenced this pull request Apr 13, 2026
- Move token from localStorage to sessionStorage to limit exposure (#5)
- Enforce requiresHost route guard with server-side check (#20)
- Compensate for client/server clock skew in countdown timer (#34)
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