OTP phone sign-in flow
This document explains how the phone-number + OTP sign-in works end‑to‑end (backend + frontend) and how to extend or debug it,
High-level overview
- Goal: allow a user to sign in using only their phone number and a one‑time code (OTP) sent via Firebase Phone Authentication.
- Key constraints:
- We only send OTPs for numbers that already belong to a user (either in our DB or at least in Firebase Auth).
- We still reuse the existing Firebase session cookie mechanism (
POST /auth/session-login) for server‑side authentication. - Users created in Firebase but not yet in our MongoDB are auto‑synced on first successful sign‑in.
Firebase is the only provider involved here for OTP delivery (no Twilio). SMS are billed through the Firebase / Google Cloud project associated with this environment.
End-to-end flow (diagram)
At a glance:
- Backend decides whether a phone number is allowed to receive an OTP.
- Firebase actually sends and verifies the SMS code.
- Backend turns a verified Firebase ID token into a session cookie and a synced user record.
Backend responsibilities
Backend pieces live under src/firebase/* in the NestJS app.
1. Phone existence check: POST /auth/check-phone
- Controller:
AuthController.checkPhoneinsrc/firebase/auth.controller.ts. - DTO/schema:
CheckPhoneDtoSchemainsrc/firebase/schemas/auth.dto.ts(shape:{ phoneNumber: string }). - Responsibilities:
- Normalize the phone number to our canonical E.164 format (
+XXXXXXXXXXX). - Reject obviously invalid numbers early (length / characters).
- Check in our MongoDB user collection for a
PHONEidentity. - If not found, ask Firebase Admin (
getUserByPhoneNumber) whether a Firebase Auth user uses that phone. - If found in either place → return
{ exists: true }. - If not found anywhere → return 404 (
"Phone number not registered").
- Normalize the phone number to our canonical E.164 format (
This endpoint is public (no auth header) because it is part of the login flow.
2. Firebase lookup and user sync
Key services:
FirebaseService.getUserByPhoneNumberinsrc/firebase/firebase.service.ts:- Thin wrapper around
admin.auth().getUserByPhoneNumberwith logging. - Used by
checkPhoneExiststo validate Firebase‑only users.
- Thin wrapper around
FirebaseUserService.handleUserLogininsrc/firebase/firebase-user.service.ts:- Called after a successful OTP verification when the frontend sends
POST /auth/session-login. - Fetches the Firebase user by UID.
- Delegates to
syncUserWithFirebase:- If a user already exists in Mongo for this Firebase UID, update identities and metadata as needed.
- If not, create a new user record (phone/email identities from Firebase) and, if needed, a default company/membership.
- Called after a successful OTP verification when the frontend sends
Because of this, it is safe for /auth/check-phone to allow numbers that only exist in Firebase: the first successful OTP sign‑in will automatically create/sync the Mongo user.
3. Session cookie issuance: POST /auth/session-login
- Controller:
AuthController.sessionLogininsrc/firebase/auth.controller.ts. - Input:
{ idToken: string }from the Firebase client SDK. - Responsibilities:
- Verify the ID token using Firebase Admin.
- Issue a Firebase session cookie and attach it to the response.
- Call
handleUserLogin(uid)so our database is in sync with Firebase identities.
This endpoint is shared between email/password and phone‑OTP sign‑in flows.
Frontend responsibilities
Frontend pieces live in the agri_frontend project.
1. Auth context API (AuthContext)
File: agri_frontend/src/contexts/AuthContext.tsx.
The auth context exposes two OTP‑specific methods:
requestPhoneOtp(phoneNumber, recaptchaVerifier):- Normalizes the raw phone input (
normalizePhoneE164). - Calls backend
POST /auth/check-phoneto ensure the number is allowed. - On success, calls
signInWithPhoneNumber(auth, normalizedPhone, recaptchaVerifier)from the Firebase Web SDK. - Returns the
ConfirmationResultobject that is later used to confirm the OTP.
- Normalizes the raw phone input (
confirmPhoneOtp(confirmationResult, otpCode):- Calls
confirmationResult.confirm(otpCode)to verify the 6‑digit code with Firebase. - Obtains an ID token from the resulting Firebase user.
- Calls backend
POST /auth/session-loginwith that ID token. - Resolves with the authenticated
Userinstance on success.
- Calls
These methods are the only API surface the SignIn UI needs to deal with OTP.
2. SignIn component – phone tab
File: agri_frontend/src/components/auth/SignIn.tsx.
The sign‑in page has two tabs: Email and Phone. The phone tab handles:
Phone number entry:
- Uses a
PhoneInputcomponent with country selector and auto‑detection. - On mount, calls external IP geolocation (
ipapi.co) to:- Get the default country calling code for the placeholder.
- Get the ISO country code used as
defaultCountryforPhoneInput(for the initial flag).
- Uses a
Sending the OTP:
handleSendOtpvalidates that a phone number is present.- Obtains (or creates) a reCAPTCHA verifier instance.
- Calls
requestPhoneOtp(phoneNumber, verifier)fromAuthContext. - On success:
- Stores the
ConfirmationResult. - Resets the 6‑digit input array.
- Switches from the “number” step to the “otp” step.
- Stores the
- On error:
- Maps “not registered” errors to a user‑friendly
signIn.phoneNotRegisteredmessage.
- Maps “not registered” errors to a user‑friendly
OTP entry and auto‑verification:
- Renders 6 individual text fields bound to
otpDigits[0…5]. - As soon as all 6 digits are filled and we are not already loading:
- Concatenates the digits into a string code.
- Calls
confirmPhoneOtp(confirmationResult, code). - On success, navigates to
/dashboardjust like email/password login. - On error, shows a generic
signIn.failedSignInmessage.
- Renders 6 individual text fields bound to
The UI deliberately hides most implementation details behind AuthContext, so future refactors of the backend or Firebase integration do not require UI changes.
Infrastructure, configuration & billing
Provider choice
- We use Firebase Phone Authentication for OTP delivery and verification.
- SMS costs are billed through the Firebase / Google Cloud project associated with the running environment.
Firebase configuration
To enable and operate phone sign‑in, make sure the following are set up in the Firebase console:
- In Firebase Console → Auth → Sign‑in method:
- Enable Phone as a sign‑in provider.
- Configure any required reCAPTCHA / safety settings for web clients.
- In Firebase Console → Project settings:
- Ensure the web app credentials (
apiKey,authDomain, etc.) match the values used in the frontend.env(VITE_FIREBASE_*variables).
- Ensure the web app credentials (
If phone sign‑in is disabled or misconfigured, signInWithPhoneNumber will fail before any backend logic is called.
Billing & quotas
Firebase charges per SMS for phone authentication. For details:
- See Firebase pricing (Authentication & Phone Auth section).
- Monitor usage & errors under Firebase Auth → Usage and in Cloud Logging for the project.
Operationally, if OTP suddenly fails in production, check in order:
/auth/check-phonestill returns{ exists: true }for known numbers.- Firebase Phone Auth is enabled and not rate‑limited or blocked.
- Project billing is active and has not hit SMS quota / spending limits.
Links & references
Use these as jumping‑off points; the exact URLs may vary by project/environment.
- Firebase console (Auth – Phone sign‑in): https://console.firebase.google.com
- Firebase Auth phone sign‑in docs: https://firebase.google.com/docs/auth/web/phone-auth
- Firebase pricing (Authentication / SMS): https://firebase.google.com/pricing
- Internal credentials: search in 1Password for “Firebase – Maison Bleue” to find the console login for the relevant project.