Skip to content

docs(auth): add email link authentication guide - #9170

Open
rutvik24 wants to merge 1 commit into
invertase:mainfrom
rutvik24:docs/auth-email-link-auth
Open

docs(auth): add email link authentication guide#9170
rutvik24 wants to merge 1 commit into
invertase:mainfrom
rutvik24:docs/auth-email-link-auth

Conversation

@rutvik24

@rutvik24 rutvik24 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add Auth email-link authentication guide and wire it into the docs nav.
  • Cross-link related Auth docs (usage, phone, OIDC) and update the spellcheck dictionary.

Test plan

  • yarn lint:markdown
  • yarn lint:spellcheck
  • Confirm Documentation / Spelling & Grammar check is green on the PR

@codecov

codecov Bot commented Aug 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 68.09%. Comparing base (b7c4749) to head (9987e2b).

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #9170      +/-   ##
============================================
- Coverage     68.10%   68.09%   -0.01%     
+ Complexity     1912     1911       -1     
============================================
  Files           510      510              
  Lines         37877    37877              
  Branches       5193     5193              
============================================
- Hits          25792    25788       -4     
- Misses        10261    10262       +1     
- Partials       1824     1827       +3     
Flag Coverage Δ
android-native 63.73% <ø> (-<0.01%) ⬇️
e2e-ts-android 53.92% <ø> (-0.06%) ⬇️
e2e-ts-ios 56.53% <ø> (ø)
e2e-ts-macos 49.74% <ø> (+0.01%) ⬆️
ios-native 68.63% <ø> (ø)
ios-ruby 100.00% <ø> (ø)
jest 45.04% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment on lines +19 to +20
This page focuses on wiring the flow in React Native Firebase, plus two community helpers that make hosting and association
files easier.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to follow the upstream as closely as possible to start and emphasize the non-custom / default path first I think

Going with that - please drop the "plus two community helpers…" framing here so the intro matches upstream (official Hosting / App Links / Universal Links only), but tease the custom domain info for advanced integrations. Suggested replacement:

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.

Comment on lines +32 to +34
`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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleCodeInApp / omit dynamicLinkDomain / omit default linkDomain is correct — please keep that.

Please add a short Migrate from Dynamic Links subsection (after this paragraph or after Console setup) covering the Admin SDK API call integrators need to run from the Android migration guide. This is the #1 failure mode in #8405 (DYNAMIC_LINK_NOT_ACTIVATED, emails still using FDL).

Suggested snippet:

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',
  },
});

Also note: if emails still contain *.page.link after this, the project is still on FDL; linkDomain in ActionCodeSettings is only for a custom Hosting domain, not this switch.

Comment on lines +36 to +47
# Choose a hosting path

You need a domain that can serve association files and act as the continue / App Links host. Pick the path that matches your
situation:

| Situation | Use | Links |
| ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| **No website yet** — need a branded continue page + association files | Deploy **[email-link-host](https://github.com/rutvik24/email-link-host)** (Firebase Hosting Spark, Cloudflare Pages, Vercel, Netlify, Amplify, or Docker/Go) | [Docs](https://rutvik24.github.io/email-link-host/) · [Live demo](https://fir-email-link-host.web.app) |
| **Already have a website** — want that domain to open the app for email links (or general deep links) | Generate files with **[App Universal Links Helper](https://github.com/rutvik24/app-universal-links-helper)**, then host them under `/.well-known/` on your domain | [Live helper](https://rutvik24.github.io/app-universal-links-helper/) |

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make the default Firebase Hosting domain the first and most prominent path. Every project already has PROJECT_ID.firebaseapp.com; that is how people will first integrate, and it matches upstream. Branded / custom-domain hosting is as-needed, not the getting-started path.

Suggested table (community tools mentioned, not promoted):

# 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:

| Situation | Use |
| --- | --- |
| **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 domain** | Attach 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. |

Then keep a short "Optional: custom domain" section with the official association-file format (link Digital Asset Links + Apple associated domains). If you mention generators or sample hosts, one sentence is enough — e.g. "Community generators exist for these JSON files; they are optional." Please don't lead with or deep-link a specific third-party repo as Path A/B.

Comment on lines +49 to +79
## Path A — no site: email-link-host

[email-link-host](https://github.com/rutvik24/email-link-host) is a static (or tiny Go/Docker) host purpose-built for this
flow: branded landing page, optional store buttons, and association JSON generated from environment variables /
`config/site.json`.

1. Clone the repo and copy `.env.example` → `.env` (or set the same vars in your host's CI / console).
2. Set at least:
- `ANDROID_PACKAGE_NAME`
- `ANDROID_SHA256_CERT_FINGERPRINTS` (comma-separated)
- `IOS_TEAM_ID` / `IOS_BUNDLE_ID`
- Optional brand / theme / store URLs (`config/site.json` + `SITE_*` / `THEME_*` vars)

3. Build and deploy (Firebase Hosting example):

```bash
# .firebaserc → your Firebase project id
bun install
bun run build
npx -y firebase-tools@latest deploy --only hosting
```

Other platforms publish the same `out/` directory — see the [hosting overview](https://rutvik24.github.io/email-link-host/docs/hosting/overview).
Docker uses a pure Go image instead of `out/`.

4. Confirm these URLs **render JSON in the browser** (not a file download):
- `https://YOUR_DOMAIN/.well-known/assetlinks.json`
- `https://YOUR_DOMAIN/.well-known/apple-app-site-association`

By default the host includes AASA coverage for Firebase Auth paths (`/__/auth/links*`). Full configuration reference:
[email-link-host configuration](https://rutvik24.github.io/email-link-host/docs/email-link-host/configuration).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for documenting a full custom-host flow - this will be useful for people that need to step up to doing links with branding.

However, please demote this so it is not Path A. After the default PROJECT_ID.firebaseapp.com path, a short optional subsection is enough focusing on the steps and then showing your projects as possible implementations (what files to publish, Content-Type: application/json, include /__/auth/links*). Please don't clone/deploy a specific third-party host as the recommended getting-started steps; that diverges from upstream style. Mentioning that community hosts/generators exist is fine.

Comment on lines +81 to +98
## Path B — existing site: App Universal Links Helper

If the continue URL should live on a domain you already operate:

1. Open [App Universal Links Helper](https://rutvik24.github.io/app-universal-links-helper/) (or run it locally from
[the repo](https://github.com/rutvik24/app-universal-links-helper)).
2. Enter hosts, package name, SHA-256 fingerprints, Apple Team ID + bundle ID, and path rules (include
`/__/auth/links` / `pathPrefix` as needed for Auth links).
3. Copy or download:
- `assetlinks.json`
- `apple-app-site-association`
- AndroidManifest `<intent-filter>` snippet
- Optional `adb` / `xcrun` / `curl` / `swcutil` test commands

4. Publish the JSON files on your site at `/.well-known/…` with `Content-Type: application/json` (and no attachment
disposition). Paste the intent-filter into your app (see below).

The helper does **not** upload files for you — it only generates what you host.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same ask as Path A: treat this as optional custom-domain material, not a primary path, and mention but don't promote a specific helper UI as the way to generate association files.

Suggested replacement for this whole subsection - official files only:

### Association files (custom domain only)

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. AASA needs your Apple Team ID + bundle ID and paths covering `/__/auth/links*`.

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

Comment on lines +230 to +242
async function completeFromUrl(url) {
if (!url || !isSignInWithEmailLink(getAuth(), url)) {
return;
}

const email = await AsyncStorage.getItem(EMAIL_KEY);
if (!email) {
// Prompt the user to re-enter email (different device / cleared storage).
throw new Error('Enter the email that requested the sign-in link.');
}

await signInWithEmailLink(getAuth(), email, url);
await AsyncStorage.removeItem(EMAIL_KEY);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't throw when the stored email is missing - that's the normal cross-device case. Prompt for the email, then call signInWithEmailLink (you already describe that in §3; the snippet should match).

Suggested replacement for the missing-email branch:

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;
}

Also wrap signInWithEmailLink in try/catch and surface error.code (expired/reused links show up as auth/invalid-action-code).

Comment on lines +260 to +263
## 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional but useful - the upstream guides cover these and they're easy to miss:

  • Link / re-auth: EmailAuthProvider.credentialWithLink(email, url) then linkWithCredential / reauthenticateWithCredential (same URL handling as sign-in).
  • Email enumeration protection: projects created after 2023-09-15 should not use fetchSignInMethodsForEmail for identifier-first UI; see Firebase's note on the email-link pages.

Comment on lines +265 to +282
# Example app

End-to-end sample (React Native + Auth modular API + Android App Links + iOS Associated Domains + AsyncStorage):

- **Repository:** [rutvik24/firebase-email-link-host-demo](https://github.com/rutvik24/firebase-email-link-host-demo)
- Pairs with the hosted continue domain from [email-link-host](https://github.com/rutvik24/email-link-host)
([live demo](https://fir-email-link-host.web.app))

Useful files in the sample:

| Area | Location |
| ------------------------------------------------ | ------------------------------------------ |
| Continue URL / package IDs | `src/config/auth.ts` |
| Auth wrapper (`send` / `complete` / `isSignIn…`) | `src/services/firebaseAuth.ts` |
| `Linking` + UI state | `src/hooks/useEmailLinkAuth.ts` |
| Android intent-filters | `android/app/src/main/AndroidManifest.xml` |
| iOS Associated Domains | `ios/.../*.entitlements` |
| Universal Links → RN | `ios/.../AppDelegate.swift` |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't make a specific third-party sample the “Example app” centerpiece - same style as other RNFB auth pages, which stay in-repo / official.

Suggested replacement: reduce it to one optional sentence at the end of Related, e.g. "Community sample apps exist that wire App Links + Universal Links + the modular Auth APIs." The in-page snippets + checklist should be the worked example.

Comment on lines +284 to +292
# Checklist

1. Email/Password + Email link providers enabled.
2. Continue URL domain is **Authorized**.
3. Association files live at `/.well-known/` and return JSON inline.
4. Android SHA fingerprints registered; intent-filter uses `autoVerify` and your host(s).
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).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checklist is great. Please add a Troubleshooting heading after it - this is the part people (and agents these days...) actually search for. Suggested rows:

Symptom What to check
DYNAMIC_LINK_NOT_ACTIVATED / email still uses *.page.link Run Admin SDK mobileLinksConfig.domain: 'HOSTING_DOMAIN'. Do not set dynamicLinkDomain.
Link opens the browser, not the app Physical 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 does Custom 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-code Link expired or already used; send a new one.
auth/internal-error Check native logs (adb logcat / Console.app on a real device), not just the JS error.

Comment on lines +294 to +300
# Related

- [Authentication usage](/auth/usage)
- [email-link-host documentation](https://rutvik24.github.io/email-link-host/)
- [App Universal Links Helper](https://rutvik24.github.io/app-universal-links-helper/)
- Firebase: [Android email link](https://firebase.google.com/docs/auth/android/email-link-auth) ·
[iOS email link](https://firebase.google.com/docs/auth/ios/email-link-auth)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please lead Related with official docs only (same as phone/social pages):

# Related

- [Authentication usage](/auth/usage)
- Firebase: [Android email link](https://firebase.google.com/docs/auth/android/email-link-auth) · [iOS email link](https://firebase.google.com/docs/auth/ios/email-link-auth) · [Migrate off Dynamic Links](https://firebase.google.com/docs/auth/android/email-link-migration)

If you want a community-tools mention, one trailing sentence is enough — no extra doc/demo/helper links in this list.

@mikehardy

Copy link
Copy Markdown
Collaborator

Thank you for this - it's a substantial, carefully researched guide on a genuinely hard flow, and filling this gap in the RNFB docs is really appreciated.

Requesting a structural pass so the page matches the style of the upstream Firebase email-link docs: official default / non-branded path first (the way people will first integrate), then optional custom-domain / branded hosting. Community tools can be mentioned where they help, but please don't promote specific projects - same approach as our other auth pages (phone, social), which link official Firebase docs and leave third-party helpers as optional asides.

Inline comments have suggested copy where that might reduce back-and-forth. None of this is a knock on the work you've already done; it's aligning the page with how we document the rest of Auth and I think this will help a lot of people in one of the more difficult areas of cross-platform mobile dev

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.

2 participants