ZITADEL Docs
Integrate & AuthenticateMigrate

Migrate from Firebase to ZITADEL

1. Introduction

This guide will walk you through the steps to migrate users from Firebase Authentication to ZITADEL. Due to Firebase's proprietary password hashing algorithm, you'll need to choose between two migration strategies based on your requirements.

What you'll learn with this guide

  • Understanding Firebase's password hash incompatibility
  • Two migration strategies: Bulk Import with Password Reset and Just-In-Time (JIT) Migration
  • How to export user data from Firebase
  • Performing the user import via ZITADEL's API
  • Testing and validating the migration

2. Understanding the Password Hash Challenge

2.1. The Blocker: Incompatible Password Hashes

Firebase uses a proprietary, modified scrypt algorithm that requires project-specific keys (salt separator, signer key, and rounds). This custom format is not compatible with standard password hashing algorithms.

Key Points:

  • ZITADEL supports standard hashing algorithms (bcrypt, argon2, PBKDF2, etc.)
  • ZITADEL does not have a verifier for Firebase's custom scrypt format
  • Result: Firebase password hashes cannot be directly imported and must be discarded

This limitation means you must choose one of two migration strategies outlined below.


3. Migration Strategy Overview

You have two options for migrating your Firebase users to ZITADEL:

StrategyUser ExperienceComplexityBest For
Bulk Import + Password ResetUsers must reset password on first loginLowSmaller user bases, planned maintenance windows
Just-In-Time (JIT) MigrationSeamless, transparent migrationMediumLarger user bases, zero-downtime requirements

4. Prerequisites

4.1. Create a ZITADEL Instance and Organization

You'll need a target organization in ZITADEL to import your users. You can create a new organization or use an existing one.

If you don't have a ZITADEL instance, you can sign up for free here to create a new one for you. See: Managing Organizations in ZITADEL.

Note: Copy your Organization ID since you will use the id in the later steps.

4.2. Install Firebase CLI (for export)

Install the Firebase CLI to export user data:

npm install -g firebase-tools

Authenticate with your Firebase project:

firebase login

5. Strategy 1: Bulk Import & Password Reset (Standard)

This approach imports all users at once but requires them to set a new password on their first login.

5.1. Export Users from Firebase

Export your Firebase users to a JSON file:

firebase auth:export users.json --format=json --project YOUR_PROJECT_ID

This creates a users.json file containing all user data, including the Firebase password hashes (which we will discard).

5.2. Transform Firebase Data to ZITADEL Format

You need to map Firebase fields to ZITADEL's schema. Here's a sample transformation:

Firebase Export Sample:

{
  "users": [
    {
      "localId": "firebase-user-123",
      "email": "user@example.com",
      "emailVerified": true,
      "displayName": "John Doe",
      "createdAt": "1609459200000",
      "passwordHash": "...",
      "salt": "..."
    }
  ]
}

ZITADEL Import Format:

{
  "dataOrgs": {
    "orgs": [
      {
        "orgId": "<your-org-id>",
        "humanUsers": [
          {
            "userId": "firebase-user-123",
            "user": {
              "userName": "user@example.com",
              "profile": {
                "firstName": "John",
                "lastName": "Doe"
              },
              "email": {
                "email": "user@example.com",
                "isEmailVerified": true
              },
              "passwordChangeRequired": true
            }
          }
        ]
      }
    ]
  },
  "timeout": "5m0s"
}

Key Mapping:

  • localIduserId
  • emailemail.email and userName
  • emailVerifiedemail.isEmailVerified
  • displayNameprofile.firstName and profile.lastName (both are required; if you can't split, set firstName to the full display name, set a placeholder lastName like "-", and optionally set profile.displayName to the full name as well)
  • Omit hashedPassword object entirely
  • Include "passwordChangeRequired": true to trigger password reset

5.3. Obtain Access Token for API Access

To call the ZITADEL Admin API, you need to authenticate using a Service Account with the IAM_OWNER Manager permissions.

There are two recommended authentication methods:

Reference: Service Accounts & API Authentication

5.4. Import Users into ZITADEL

Use the Admin API – Import Data endpoint:

curl --location 'https://${CUSTOM_DOMAIN}/admin/v1/import' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <access-token>' \
--data @importBody.json

5.5. Notify Users

Send an email to your users informing them that they need to reset their password on their next login. ZITADEL will automatically prompt them to create a new password when passwordChangeRequired is set to true.


6. Strategy 2: Just-In-Time (JIT) Migration (Seamless)

This approach migrates users transparently during their first login after migration begins, providing a seamless experience.

6.1. Architecture Overview

    User Login Attempt

       Backend (Proxy)

       Check ZITADEL

    User exists? → Yes → Allow login
           ↓ No
    Verify with Firebase Auth REST API

    Valid? → Yes → Create user in ZITADEL → Allow login
           ↓ No
       Reject login

6.2. Implementation Steps

Step 1: Set Up Login Interception

Your backend must intercept login requests and capture the plain-text password during the grace period.

Security Requirements:

  • HTTPS only – Never transmit passwords over unencrypted connections
  • Do not log plain-text passwords – Handle credentials in memory only
  • Limit grace period – Set a reasonable migration window (e.g., 30-90 days)

Step 2: Verify Credentials with Firebase

When a user attempts to login, verify their credentials against Firebase using the REST API:

curl --location 'https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=[WEB_API_KEY]' \
--header 'Content-Type: application/json' \
--data '{
  "email": "user@example.com",
  "password": "user-password",
  "returnSecureToken": true
}'

Response Handling:

  • 200 OK – Credentials are valid, proceed to Step 3
  • 400 Bad Request – Invalid credentials or user doesn't exist in Firebase

Step 3: Create User in ZITADEL

If Firebase validates the credentials, immediately create the user in ZITADEL using the plain-text password (ZITADEL will hash it using its native algorithm):

curl --location 'https://${CUSTOM_DOMAIN}/management/v1/users/human/_import' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <access-token>' \
--data '{
  "userName": "user@example.com",
  "profile": {
    "firstName": "John",
    "lastName": "Doe"
  },
  "email": {
    "email": "user@example.com",
    "isEmailVerified": true
  },
  "password": "user-password"
}'

Step 4: Allow Login

Once the user is created in ZITADEL, allow the login to proceed. Future logins will authenticate directly against ZITADEL.

6.3. Handle the Grace Period

Set a migration deadline (e.g., 90 days). After this period:

  1. Disable Firebase Authentication for your application
  2. Migrate remaining inactive users using Strategy 1 (Bulk Import + Password Reset)
  3. Notify inactive users that they need to reset their password

6.4. Sample Backend Implementation (Pseudocode)

async function handleLogin(email, password) {
  // Check if user exists in ZITADEL
  const zitadelUser = await checkZitadelUser(email);
  
  if (zitadelUser) {
    // User already migrated, authenticate with ZITADEL
    return authenticateWithZitadel(email, password);
  }
  
  // User not in ZITADEL, verify with Firebase
  const firebaseValid = await verifyWithFirebase(email, password);
  
  if (firebaseValid) {
    // Create user in ZITADEL with plain-text password
    await createZitadelUser(email, password, firebaseValid.profile);
    
    // Authenticate with ZITADEL
    return authenticateWithZitadel(email, password);
  }
  
  // Invalid credentials
  throw new Error('Invalid email or password');
}

7. Testing the Migration

7.1. Test User Login

For Strategy 1 (Bulk Import):

  • Attempt to login with an imported user
  • Verify that the password reset flow is triggered
  • Complete the password reset and confirm successful login

For Strategy 2 (JIT Migration):

  • Attempt to login with a Firebase user that hasn't been migrated yet
  • Verify that the login succeeds seamlessly
  • Check that the user now exists in ZITADEL
  • Attempt a second login to confirm authentication happens directly with ZITADEL

7.2. Verify User Data

Check that user data was imported correctly:

curl --location 'https://${CUSTOM_DOMAIN}/management/v1/users/human/_search' \
--header 'Authorization: Bearer <access-token>' \
--header 'Content-Type: application/json' \
--data '{
  "query": {
    "offset": "0",
    "limit": 100
  }
}'

7.3. Troubleshooting

Common issues:

  • Strategy 1:

    • Users not receiving password reset emails (check email configuration)
    • Import API timeout (reduce batch size)
    • Malformed JSON (validate against ZITADEL schema)
  • Strategy 2:

    • Firebase API authentication errors (verify Web API Key)
    • HTTPS certificate issues (ensure valid SSL/TLS)
    • User creation failures (check ZITADEL service account permissions)

Where to check logs and get help

You can verify that users were imported by calling the events endpoint and checking for the following event type:

"user.human.added"

Example request:

curl --location 'https://${CUSTOM_DOMAIN}/admin/v1/events/_search' \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{
  "asc": true,
  "limit": 1000,
  "event_types": [
    "user.human.added"
  ]
}'

8. Additional Resources

Was this page helpful?

On this page