Back to guides

Migrate from Firebase Authentication to Own Auth


Paginate the Firebase Admin SDK's listUsers() API and retain each UID. auth.createUser does not import Firebase password hashes, so create users without passwords and verify them by magic link. Firebase ID tokens, refresh tokens, and session cookies remain Firebase credentials.

Export Firebase Authentication users

  1. Paginate getAuth().listUsers(1000, pageToken) and retain uid, email, emailVerified, displayName, photoURL, phoneNumber, disabled, customClaims, providerData, and multiFactor.enrolledFactors.
  2. For Identity Platform tenants, enumerate each tenant and call listUsers() through its TenantAwareAuth. Preserve the tenant ID with every user mapping.
  3. Find application records and Firebase Security Rules that use the Firebase UID or token claims.
  4. Keep disabled users out of the active import. Treat UIDs referenced by application data but absent from Firebase as deleted accounts.

Map Firebase Authentication records

  • Normalize uid to id, displayName to name, and photoURL to imageUrl. Exclude disabled users from the active import.
  • Translate only the custom claims used by the application into explicit roles or permissions.
  • Preserve each tenant ID. Map it to an Own Auth organisation only when the Firebase tenant represented the same product organisation.
  • Transform providerData into a sign-in-method inventory. Do not copy provider UIDs or tokens into Own Auth. Require Apple, GitHub, or Google authentication again before linking the provider to the mapped account.
  • Require new MFA enrolment. Create phone-only users through Own Auth phone verification. Link an anonymous Firebase account only while its existing Firebase session can prove access to the application data being reassigned.
  • Own Auth sessions are not accepted by Firestore, Realtime Database, or Cloud Storage Security Rules. Replace Firebase-token authorization before switching those clients.

Map Firebase UIDs to Own Auth users

Transform each source record into the importer fields below. This Postgres table keeps the Firebase UID mapping separate from the Own Auth user record. The migration metadata lets a rerun find a previously created user and complete its mapping row.

Terminal
npm install pg
npm install --save-dev @types/pg
migration-map.sql
CREATE TABLE auth_user_migrations (
  source text NOT NULL,
  source_user_id text NOT NULL,
  own_auth_user_id text NOT NULL,
  PRIMARY KEY (source, source_user_id),
  UNIQUE (source, own_auth_user_id)
);
scripts/import-users.ts
import { Pool } from "pg";
import { auth } from "../auth";

const databaseUrl = process.env.DATABASE_URL!;

const db = new Pool({
  connectionString: databaseUrl,
  allowExitOnIdle: true,
});
const source = "firebase-auth";

export type ExportedUser = {
  id: string;
  email: string | null;
  name?: string | null;
  imageUrl?: string | null;
  disabled?: boolean;
};

export async function importUser(exportedUser: ExportedUser) {
  if (exportedUser.disabled) {
    return {
      status: "skipped",
      legacyUserId: exportedUser.id,
      reason: "disabled_account",
    } as const;
  }

  if (!exportedUser.email) {
    return {
      status: "skipped",
      legacyUserId: exportedUser.id,
      reason: "missing_email",
    } as const;
  }

  const mapped = await db.query<{ own_auth_user_id: string }>(
    `SELECT own_auth_user_id
       FROM auth_user_migrations
      WHERE source = $1 AND source_user_id = $2`,
    [source, exportedUser.id],
  );

  if (mapped.rows[0]) {
    return {
      status: "imported",
      ownAuthUserId: mapped.rows[0].own_auth_user_id,
    } as const;
  }

  let user = await auth.storage.getUserByEmail(exportedUser.email);
  if (user) {
    const matchesSource =
      user.metadata.migrationSource === source &&
      user.metadata.legacyUserId === exportedUser.id;

    if (!matchesSource) {
      throw new Error(`Email collision for ${exportedUser.id}`);
    }
  } else {
    user = await auth.createUser({
      email: exportedUser.email,
      name: exportedUser.name ?? undefined,
      imageUrl: exportedUser.imageUrl ?? undefined,
      metadata: {
        migrationSource: source,
        legacyUserId: exportedUser.id,
      },
    });
  }

  await db.query(
    `INSERT INTO auth_user_migrations
       (source, source_user_id, own_auth_user_id)
     VALUES ($1, $2, $3)`,
    [source, exportedUser.id, user.id],
  );

  return {
    status: "imported",
    ownAuthUserId: user.id,
  } as const;
}

Transform Firebase Authentication records

Transform the Admin SDK UserRecord explicitly. The importer consumes only user. Keep the remaining fields in migration state to select the first sign-in path and reconcile tenant access.

scripts/transform-firebase-user.ts
import type { UserRecord } from "firebase-admin/auth";
import type { ExportedUser } from "./import-users";

type OAuthProvider = "apple" | "github" | "google";

const oauthProviders: Record<string, OAuthProvider> = {
  "apple.com": "apple",
  "github.com": "github",
  "google.com": "google",
};

export function transformFirebaseUser(
  record: UserRecord,
  tenantId: string | null,
) {
  const providers = record.providerData.flatMap(({ providerId }) => {
    const provider = oauthProviders[providerId];
    return provider ? [provider] : [];
  });

  const user: ExportedUser = {
    id: record.uid,
    email: record.email ?? null,
    name: record.displayName ?? null,
    imageUrl: record.photoURL ?? null,
    disabled: record.disabled,
  };

  return {
    user,
    tenantId,
    providers,
    phone: record.phoneNumber ?? null,
    emailWasVerified: record.emailVerified,
    mfaWasEnabled: Boolean(record.multiFactor?.enrolledFactors.length),
  };
}

Verify each imported account

Pass the token from the email link to auth.verifyMagicLink. If it returns mfa_required, complete the challenge. Continue only after complete, which contains the new Own Auth session.

Replace Firebase UIDs in application records

Update application records from Firebase UIDs to Own Auth user IDs only after the user completes an Own Auth sign-in. Never link records by email alone.

Session replacement

Firebase ID tokens, refresh tokens, and session cookies do not become Own Auth sessions. After an Own Auth sign-in succeeds, create the Own Auth session, stop accepting Firebase credentials for the mapped UID, and revoke its Firebase refresh tokens. Existing ID tokens remain usable until expiry unless Firebase verification checks revocation.