Email Link Authentication

Passwordless sign-in with email links using App Links and Universal Links.

Email link (passwordless) authentication sends the user a one-time sign-in link. Opening that link on a device completes sign-in and verifies ownership of the email address — no password required.

Firebase Auth no longer relies on Firebase Dynamic Links for this flow. Mobile apps complete sign-in through Android App Links and iOS Universal Links on a domain you control (often a Firebase Hosting domain). Official platform guides:

This page focuses on wiring the flow in React Native Firebase. It starts with the same default Hosting-domain path as the official Android and Apple guides. There is information on optionally adding custom domains and branded landing pages later.

How the pieces fit together

PieceRole
Continue URL (ActionCodeSettings.url)HTTPS URL embedded in the email. Its domain must be an Authorized domain in the Firebase Console.
Hosting link domainDomain Firebase uses for the mobile Auth link (default PROJECT_ID.firebaseapp.com, or a custom Hosting / linkDomain).
Association files/.well-known/assetlinks.json (Android) and /.well-known/apple-app-site-association (iOS) so the OS opens your app.
Native app configAndroid intent-filter + SHA certificates; iOS Associated Domains + Linking handlers.
JS Auth APIssendSignInLinkToEmail → store email → isSignInWithEmailLinksignInWithEmailLink.

handleCodeInApp must be true for email-link sign-in. Do not set deprecated dynamicLinkDomain. Prefer omitting linkDomain unless you have configured a custom Hosting link domain for the project — Auth selects the project default Hosting domain when it is omitted. Setting linkDomain to a bare *.web.app / *.firebaseapp.com default is often rejected.

Migrate from Dynamic Links

If the project previously used Firebase Dynamic Links for email sign-in, run the Admin SDK project-config update from the Android migration guide so Auth issues Hosting mobile links instead of FDL:

js
import { getAuth } from 'firebase-admin/auth';

await getAuth()
  .projectConfigManager()
  .updateProjectConfig({
    mobileLinksConfig: {
      // Literal value from the Firebase migration docs — switches the project
      // from Dynamic Links to Firebase Hosting mobile links.
      domain: 'HOSTING_DOMAIN',
    },
  });

If emails still contain *.page.link after this, the project is still using Firebase Dynamic Links. linkDomain in ActionCodeSettings is only for a custom Hosting domain — it does not perform this migration switch. Omitting deprecated dynamicLinkDomain is required; see Troubleshooting if you still see DYNAMIC_LINK_NOT_ACTIVATED.

Choose a hosting path

You need a domain that can receive /__/auth/links (App Links / Universal Links). Pick the simplest path that matches your app:

SituationUse
Getting started (no custom domain)Use the project's default Hosting domain PROJECT_ID.firebaseapp.com. Register Android SHA-1 + SHA-256 in Firebase project settings so Firebase can serve association files.
Custom / branded domainAttach a custom domain in Firebase Hosting (or reuse a domain you already operate). Serve /.well-known/assetlinks.json and /.well-known/apple-app-site-association on that host. Set linkDomain in ActionCodeSettings to that custom domain.

Both paths end the same way: Authorized domain in Firebase Auth, association files available on the Hosting link domain, and the React Native app claiming that domain via App Links / Universal Links.

Association files (custom domain only)

On the default PROJECT_ID.firebaseapp.com path, Firebase serves association files after SHA fingerprints are registered — no separate generator required.

For a custom domain, publish these at the Hosting / custom domain (inline JSON, not a file download):

  • https://YOUR_DOMAIN/.well-known/assetlinks.json
  • https://YOUR_DOMAIN/.well-known/apple-app-site-association

assetlinks.json needs package_name + SHA-256 fingerprints (Digital Asset Links). AASA needs your Apple Team ID + bundle ID and paths covering /__/auth/links* (Supporting associated domains). Serve both with Content-Type: application/json and no attachment disposition.

Community generators exist for these JSON files; they are optional and unofficial — see Optional community helpers at the end of this page.

Firebase Console setup

  1. Enable Email/Password, then enable Email link (passwordless sign-in) under Authentication → Sign-in method.
  2. Add your continue-URL host under Authentication → Settings → Authorized domains.
  3. Android: in Project settings, register the app's package name plus SHA-1 and SHA-256 certificate fingerprints (required for App Links verification on the default Hosting domain).
  4. iOS: ensure the bundle ID in the Firebase iOS app matches Xcode / Associated Domains.

Native app setup

Two domains are involved — confusing them is a common reason the link opens Safari or Chrome instead of the app:

  1. Hosting link domain (must match Associated Domains / autoVerify intent-filter): https://PROJECT_ID.firebaseapp.com/__/auth/links?... — this is the URL the OS must open in the app.
  2. Continue URL (ActionCodeSettings.url): any authorized HTTPS URL; used when the app is not installed and as embedded state. It does not have to be the same host as (1).

Claim the Hosting link domain in native config — typically PROJECT_ID.firebaseapp.com. Add PROJECT_ID.web.app or a custom Hosting domain only if your project uses that host for Auth links.

Android

Add an autoVerify intent-filter on the activity that should receive the link (launchMode="singleTask" is typical so a cold/warm start reuses one activity). Prefer separate <data> tags for scheme, host, and path — Android merges siblings. See the upstream Android intent-filter.

xml
<intent-filter android:autoVerify="true">
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="https" />
  <data android:host="YOUR_PROJECT_ID.firebaseapp.com" />
  <data android:pathPrefix="/__/auth/links" />
</intent-filter>

If you use a custom Hosting link domain, add another <data android:host="…" /> for that host. If your branded continue URL is the site root (or other paths), add another filter (or additional path rules) for those hosts.

After install, verify App Links with:

bash
adb shell pm get-app-links YOUR.PACKAGE.NAME
# optional force re-verify
adb shell pm verify-app-links --re-verify YOUR.PACKAGE.NAME

iOS

  1. Enable the Associated Domains capability and add applinks:YOUR_PROJECT_ID.firebaseapp.com. Add applinks:YOUR_PROJECT_ID.web.app or your custom Hosting domain only if Auth links use that host.

  2. Forward Universal Links and custom URLs to React Native Linking (React Native's RCTLinkingManager), for example in AppDelegate:

swift
func application(
  _ application: UIApplication,
  continue userActivity: NSUserActivity,
  restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
  return RCTLinkingManager.application(
    application,
    continue: userActivity,
    restorationHandler: restorationHandler
  )
}

func application(
  _ app: UIApplication,
  open url: URL,
  options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
  return RCTLinkingManager.application(app, open: url, options: options)
}

On a Mac with Apple's Shared Web Credentials tooling you can sanity-check AASA matching:

bash
sudo swcutil verify -d YOUR_DOMAIN -j ./apple-app-site-association -u https://YOUR_DOMAIN/__/auth/links

Expo Setup

Email-link auth needs native App Links / Universal Links, so a development build is required (Expo Go cannot claim applinks:).

In app.json / app.config.js:

  • iOS: expo.ios.associatedDomains: ["applinks:YOUR_PROJECT_ID.firebaseapp.com"]
  • Android: an intentFilters entry with autoVerify: true, https, host YOUR_PROJECT_ID.firebaseapp.com, pathPrefix /__/auth/links
  • Keep @react-native-firebase/app (and auth) config plugins as in the Expo install docs

JS handling is unchanged: React Native Linking.getInitialURL() + Linking.addEventListener('url'). For more info on using Expo with React Native Firebase, see our Expo docs.

JavaScript / React Native flow

Ensure @react-native-firebase/app and @react-native-firebase/auth are installed (see Authentication usage).

1. Send the link

Persist the email locally (for example AsyncStorage) before or immediately after sending — completing sign-in requires the same address, and you must not put it in the continue URL query string.

js
import AsyncStorage from '@react-native-async-storage/async-storage';
import { getAuth, sendSignInLinkToEmail } from '@react-native-firebase/auth';

const CONTINUE_URL = 'https://YOUR_PROJECT_ID.firebaseapp.com'; // Authorized domain
const EMAIL_KEY = 'emailForSignIn';

async function sendSignInLink(email) {
  await sendSignInLinkToEmail(getAuth(), email, {
    url: CONTINUE_URL,
    handleCodeInApp: true,
    // Omit linkDomain unless you configured a custom Hosting link domain.
    iOS: {
      bundleId: 'com.yourcompany.yourapp',
    },
    android: {
      packageName: 'com.yourcompany.yourapp',
      installApp: true,
      minimumVersion: '1',
    },
  });

  await AsyncStorage.setItem(EMAIL_KEY, email);
}

Modular sendSignInLinkToEmail requires actionCodeSettings (unlike the legacy namespaced helper defaults). See Migrating to v25 / v26 for API notes.

2. Listen for the incoming URL

Use React Native Linking for both cold start (getInitialURL) and warm start (url events):

js
import { useEffect } from 'react';
import { Linking } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { getAuth, isSignInWithEmailLink, signInWithEmailLink } from '@react-native-firebase/auth';

const EMAIL_KEY = 'emailForSignIn';

async function completeFromUrl(url) {
  if (!url || !isSignInWithEmailLink(getAuth(), url)) {
    return;
  }

  const email = await AsyncStorage.getItem(EMAIL_KEY);
  if (!email) {
    // Same device: shouldn't happen. Other device / cleared storage:
    // prompt the user to type the address the link was sent to, then continue.
    return;
  }

  try {
    await signInWithEmailLink(getAuth(), email, url);
    await AsyncStorage.removeItem(EMAIL_KEY);
  } catch (error) {
    // Expired or reused links commonly surface as auth/invalid-action-code.
    console.error(error.code, error.message);
  }
}

useEffect(() => {
  Linking.getInitialURL().then(completeFromUrl);
  const sub = Linking.addEventListener('url', ({ url }) => {
    completeFromUrl(url);
  });
  return () => sub.remove();
}, []);

From React Native Firebase v26, isSignInWithEmailLink returns a synchronous boolean (matching firebase-js-sdk). Do not await it. On v25 it returned Promise<boolean> — see Migrating to v26.

After a successful signInWithEmailLink, any onAuthStateChanged listeners receive the signed-in user.

3. Different device / missing stored email

If the user opens the link on another device, ask them to type the email again, then call signInWithEmailLink with that address and the incoming URL. Firebase requires the email to match the address the link was sent to.

4. Link / re-authenticate with an email link

To link an email-link credential to an existing signed-in user, or to re-authenticate, build a credential from the same incoming URL and call linkWithCredential / reauthenticateWithCredential:

js
import {
  EmailAuthProvider,
  getAuth,
  linkWithCredential,
  reauthenticateWithCredential,
} from '@react-native-firebase/auth';

const user = getAuth().currentUser;
if (user) {
  const credential = EmailAuthProvider.credentialWithLink(email, url);
  await linkWithCredential(user, credential);
  // or: await reauthenticateWithCredential(user, credential);
}

URL handling (App Links / Universal Links + isSignInWithEmailLink) is the same as for sign-in.

5. Email enumeration protection

Projects created after 2023-09-15 enable email enumeration protection by default. Do not rely on fetchSignInMethodsForEmail for identifier-first UI when that protection is on — see the note on Firebase's Android and iOS email-link pages.

Checklist

  1. Email/Password + Email link providers enabled.
  2. Continue URL domain is Authorized.
  3. Default path: Android SHA fingerprints registered so Firebase can serve association files — or, for a custom domain, association files live at /.well-known/ and return JSON inline.
  4. Android intent-filter uses autoVerify and your Hosting link host(s) (including /__/auth/links).
  5. iOS Associated Domains match the host(s); RCTLinkingManager receives Universal Links.
  6. App stores the email, sends the link with handleCodeInApp: true, then completes with signInWithEmailLink.
  7. Test on a physical device (App Links / Universal Links verification is unreliable or limited on many emulators/simulators).

Troubleshooting

SymptomWhat to check
DYNAMIC_LINK_NOT_ACTIVATED / email still uses *.page.linkRun Admin SDK mobileLinksConfig.domain: 'HOSTING_DOMAIN'. Do not set dynamicLinkDomain.
Link opens the browser, not the appPhysical device; AASA/assetlinks 200 + application/json; intent-filter / applinks: host is the Hosting link domain (PROJECT_ID.firebaseapp.com/__/auth/links), not only the continue URL.
iOS: Firebase email URL doesn't open the app, but pasting the Hosting URL doesCustom SMTP (e.g. SendGrid) wrapping the Auth URL in a redirect. Universal Links do not follow that redirect into the app. Use Firebase's default email sender, or a custom SMTP that does not wrap the destination URL. Reported in #8405.
auth/invalid-action-codeLink expired or already used; send a new one.
auth/internal-errorCheck native logs (adb logcat / Console.app on a real device), not just the JS error.

Related

Optional community helpers

The default Hosting path above is the simplest integration for most apps. The projects below were built by community contributors for setups that need more than that — a custom domain, your brand in the sign-in email (instead of the default firebaseapp.com link), a branded continue page, or association files on a host you already operate. That work may genuinely help when you have those requirements.

React Native Firebase does not maintain, control, or endorse these tools; we cannot vouch for their security or long-term availability. Prefer the default Hosting path when it fits, and evaluate each project for your own requirements.

No existing website — ready-made hosting helper

email-link-host is an optional community helper that provides a ready-made hosting solution so you do not have to create association files and a continue-page host from scratch. It generates assetlinks.json and apple-app-site-association from environment variables, serves a branded continue page, and can deploy to Firebase Hosting, Cloudflare Pages, Vercel, Netlify, Amplify, or Docker.

Set your Android package name, SHA-256 fingerprints, and Apple Team ID / bundle ID, then confirm /.well-known/assetlinks.json and /.well-known/apple-app-site-association render as inline JSON (not a file download).

Existing website — form-based file generator

If you already operate a website or Hosting domain, App Universal Links Helper (or run locally) is an optional community helper: fill in your hosts, package name, fingerprints, and path rules (include /__/auth/links for Auth links) in a form, then download the generated assetlinks.json, apple-app-site-association, AndroidManifest <intent-filter> snippet, and optional adb / curl / swcutil verification commands. It does not upload files — publish the JSON under /.well-known/ on your domain.

End-to-end sample app

firebase-email-link-host-demo is an optional community sample that wires React Native modular Auth + App Links + Universal Links + AsyncStorage. Useful starting points: src/services/firebaseAuth.ts, useEmailLinkAuth.ts, and the platform manifest / entitlements files.