newOS for Developers

Firebase Phone Auth

Phone number authentication via Firebase, including OTP verification and reCAPTCHA handling.

packages/newgraph-signals/src/actions/firebase.ts (164 lines)

NoteImplementation Status

The Firebase module contains the reference implementation for phone authentication. The actual production implementation may be handled at the app level or via Firebase-specific app configuration. This documentation covers the intended flow.

Quick Reference

Authentication Flow

  • initializeRecaptcha — Setup reCAPTCHA
  • requestPhoneAuthCode — Send OTP via SMS
  • submitPhoneVerificationCodeToFirebase — Verify OTP
  • refreshFirebaseToken — Force refresh ID token

Reactive Signals

  • auth — Firebase Auth instance
  • confirmationResult — OTP confirmation handle
  • recaptchaVerifier — reCAPTCHA verifier
  • firebaseToken — Current Firebase ID token

Utilities

  • getRecaptchaVerifier — Get/create verifier
  • clearRecaptchaVerifier — Clean up verifier
  • resetRecaptchaVerifier — Reset after error
  • logoutFromFirebase — Sign out from Firebase

Firebase Config

  • Stage: eu-prod
  • App Check: reCAPTCHA v3
  • Token auto-refresh: enabled

Phone Authentication Flow

The complete flow from phone number input to Newgraph JWT:

1. initializeRecaptcha()
   └── Setup invisible reCAPTCHA on button element
   
2. requestPhoneAuthCode("+1234567890")
   └── Firebase sends SMS with 6-digit OTP
   └── Returns ConfirmationResult
   
3. submitPhoneVerificationCodeToFirebase("123456")
   └── Verify OTP code
   └── Get Firebase User object
   └── Get Firebase ID token
   
4. signIn(firebaseIdToken)  [from auth.ts]
   └── Exchange for Newgraph JWT
   └── Store session

initializeRecaptcha

Setup

Initialize Firebase and set up the reCAPTCHA verifier for phone auth.

initRecaptchaVerifier(
  containerOrId: string | HTMLElement = "sign-in-button"
): void

Behavior

  1. Clears any existing verifier via clearRecaptchaVerifier()
  2. Creates new RecaptchaVerifier with size: "invisible"
  3. Attaches to specified container element
  4. Stores in recaptchaVerifier signal

HTML Setup

<!-- Button that triggers phone auth -->
<button id="sign-in-button">Send Code</button>

<!-- Or custom container -->
<div id="recaptcha-container"></div>

requestPhoneAuthCode

Async

Send OTP verification code via SMS to the provided phone number.

requestPhoneAuthCode(phone: string): Promise<ConfirmationResult>
ParameterTypeDescription
phonestringPhone number in E.164 format (e.g., +14155551234)

Implementation

import { signInWithPhoneNumber, getAuth } from "firebase/auth";

confirmationResult.value = await signInWithPhoneNumber(
  getAuth(),
  phone,
  getRecaptchaVerifier()
);

Returns

ConfirmationResult object with .confirm(code) method. Also stored in confirmationResult signal.

submitPhoneVerificationCodeToFirebase

Async

Verify the OTP code entered by the user and get the Firebase ID token.

submitPhoneVerificationCodeToFirebase(
  phoneCode: string
): Promise<string | undefined>
ParameterTypeDescription
phoneCodestring6-digit OTP code from SMS

Implementation

const userCreds = await confirmationResult.value?.confirm(phoneCode);
const token = await userCreds?.user.getIdToken(true);
firebaseToken.value = token;
return token;

Returns

Firebase ID token string. This token should be passed tosignIn() from auth.ts to exchange for a Newgraph JWT.

Nuances

  • Uses getIdToken(true) to force fresh token
  • Updates firebaseToken signal for reactive use
  • Returns undefined if confirmation fails

refreshFirebaseToken

Async

Force refresh the Firebase ID token for the current user.

refreshFirebaseToken(): Promise<string>

Implementation

const auth = getAuth();
const idToken = await auth.currentUser.getIdToken({ forceRefresh: true });
return idToken;

Use Case

Use this when you need to re-authenticate with Newgraph (e.g., token expired) without requiring the user to re-enter their phone number and OTP.

reCAPTCHA Management

getRecaptchaVerifier

getRecaptchaVerifier(
  containerOrId: string | HTMLElement = "sign-in-button"
): RecaptchaVerifier

Creates invisible reCAPTCHA verifier attached to specified element.

clearRecaptchaVerifier

clearRecaptchaVerifier(): void

Clears existing verifier. Call before creating a new one or on logout.

resetRecaptchaVerifier

resetRecaptchaVerifier(name: string = "recaptcha-container"): void

Resets the reCAPTCHA after an error. Fixes common issues like "Cannot read property 'style' of null" after failed verification.

Common use cases:
  • After image selector CAPTCHA challenge
  • After expired CAPTCHA
  • After network error during verification

Reactive Signals

Preact Signals for Firebase state management.

authSignal
auth: Signal<Auth | null>

Firebase Auth instance, initialized on module load.

confirmationResultSignal
confirmationResult: Signal<ConfirmationResult | null>

Handle for confirming OTP. Use .confirm(code) to verify.

recaptchaVerifierSignal
recaptchaVerifier: Signal<RecaptchaVerifier | null>

The reCAPTCHA verifier instance. Must be cleared on logout.

firebaseTokenSignal
firebaseToken: Signal<string | undefined>

Current Firebase ID token after successful verification.

logoutFromFirebase

Async

Sign out from Firebase Auth.

logoutFromFirebase(): Promise<void>

Behavior

  • Calls auth.value?.signOut()
  • Should be called alongside signOut() from auth.ts
  • Clears Firebase session state

Complete Flow Example

import { 
  initRecaptchaVerifier,
  requestPhoneAuthCode,
  submitPhoneVerificationCodeToFirebase 
} from "newgraph-signals/actions/firebase";
import { signIn } from "newgraph-signals/actions/auth";

// Step 1: Setup reCAPTCHA on component mount
useEffect(() => {
  initRecaptchaVerifier("sign-in-button");
}, []);

// Step 2: Send OTP when user submits phone
async function handleSendCode(phone: string) {
  try {
    await requestPhoneAuthCode(phone);
    setShowCodeInput(true);
  } catch (error) {
    console.error("Failed to send code:", error);
    resetRecaptchaVerifier();
  }
}

// Step 3: Verify OTP and sign in
async function handleVerifyCode(code: string) {
  const firebaseToken = await submitPhoneVerificationCodeToFirebase(code);
  
  if (firebaseToken) {
    // Step 4: Exchange for Newgraph session
    const jwt = await signIn(firebaseToken);
    // User is now authenticated!
  }
}

Firebase App Check

The module initializes Firebase App Check with reCAPTCHA v3 for added security.

import { initializeAppCheck, ReCaptchaV3Provider } from "firebase/app-check";

const appCheck = initializeAppCheck(app, {
  provider: new ReCaptchaV3Provider('6LdH3W0rA...'),
  isTokenAutoRefreshEnabled: true
});

This protects Firebase resources from abuse and is separate from the phone auth reCAPTCHA verifier.

Troubleshooting

"Cannot read property 'style' of null"

This occurs after reCAPTCHA image selector challenge. Call resetRecaptchaVerifier() to fix.

reCAPTCHA expired

The invisible reCAPTCHA token expires. Reset and retry the flow.

Invalid phone number format

Phone must be in E.164 format: +[country code][number]. Example: +14155551234

Related