Files
zitadel/apps/docs/content/guides/migrate/sources/firebase.mdx
T
Federico CoppedeGitHubzitadel-knowledge-bot[bot] <296006658+zitadel-knowledge-bot[bot]@users.noreply.github.com>Copilot Autofix powered by AI
d15b4e2da2 docs: create knowledge gap ID 34 (#12388)
Automatically generated PR resolving Knowledge Gap ID 34.

**Thread ID:** manual-1783339162398
**Action:** CREATE
**New File:** `content/docs/drafts/gap-manual-1783339162398.mdx`

### AI Summary
> Add a user migration guide for firebase to Zitadel

### Human Reviewer Instructions
> Firebase to Zitadel Migration Summary
> The Blocker: Incompatible Password Hashes
> 
> Firebase uses a proprietary, modified scrypt algorithm requiring
project-specific keys.
> 
> Zitadel supports standard algorithms but lacks a verifier for
Firebase's custom format.
> 
> Result: Passwords cannot be directly imported. Firebase hashes must be
discarded.
> 
> Strategy 1: Bulk Import & Password Reset (Standard)
> Requires users to set a new password on their first login.
> 
> Export: Run firebase auth:export users.json --format=json.
> 
> Map: Convert Firebase fields (e.g., localId) to Zitadel's schema.
> 
> Import to Zitadel: Call the Zitadel import API.
> 
> Action: Omit the hashedPassword object entirely.
> 
> Action: Include "passwordChangeRequired": true in the JSON payload to
trigger a reset flow via email or login prompt.
> 
> Strategy 2: Just-In-Time (JIT) Migration (Seamless)
> Migrates users transparently behind the scenes during an active grace
period.
> 
> Intercept Login: Your backend captures the plain-text password during
login.
> 
> Verify: Backend POSTs credentials to Firebase Auth REST API:
> 
>
https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=[WEB_API_KEY]
> 
> Migrate to Zitadel:
> 
> If Valid (200 OK): Create the user in Zitadel immediately using the
plain-text password (Zitadel will natively hash it).
> 
> If Invalid (400): Reject login or check if the user is already in
Zitadel.
> 
> Sunset: After the grace period ends, migrate remaining inactive users
using Strategy 1.
> (Note: Do not log plain-text passwords and strictly enforce HTTPS
during this phase).

---
🤖 **Need adjustments?**
Leave a comment below and tag **@zitadel-knowledge-bot** with your
requested changes, and I will automatically update the files and push a
new commit!

---------

Co-authored-by: zitadel-knowledge-bot[bot] <296006658+zitadel-knowledge-bot[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-07-07 12:43:28 +00:00

364 lines
12 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Migrate from Firebase to ZITADEL
description: "Migrate users from Firebase Authentication to ZITADEL using bulk import or just-in-time migration strategies."
sidebar_label: From Firebase
---
## 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:
| Strategy | User Experience | Complexity | Best For |
|----------|----------------|------------|----------|
| **Bulk Import + Password Reset** | Users must reset password on first login | Low | Smaller user bases, planned maintenance windows |
| **Just-In-Time (JIT) Migration** | Seamless, transparent migration | Medium | Larger 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](https://zitadel.com) to create a new one for you.
See: [Managing Organizations in ZITADEL](https://zitadel.com/docs/guides/manage/console/organizations-overview).
> **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:**
```json
{
"users": [
{
"localId": "firebase-user-123",
"email": "[email protected]",
"emailVerified": true,
"displayName": "John Doe",
"createdAt": "1609459200000",
"passwordHash": "...",
"salt": "..."
}
]
}
```
**ZITADEL Import Format:**
```json
{
"dataOrgs": {
"orgs": [
{
"orgId": "<your-org-id>",
"humanUsers": [
{
"userId": "firebase-user-123",
"user": {
"userName": "[email protected]",
"profile": {
"firstName": "John",
"lastName": "Doe"
},
"email": {
"email": "[email protected]",
"isEmailVerified": true
},
"passwordChangeRequired": true
}
}
]
}
]
},
"timeout": "5m0s"
}
```
**Key Mapping:**
- `localId` → `userId`
- `email` → `email.email` and `userName`
- `emailVerified` → `email.isEmailVerified`
- `displayName` → `profile.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:
- **Client Credentials Flow**
[Learn how to authenticate with client credentials.](https://zitadel.com/docs/guides/integrate/service-accounts/client-credentials)
- **Personal Access Token (PAT)**
[Learn how to create and use a PAT.](https://zitadel.com/docs/guides/integrate/service-accounts/authenticate-service-accounts#personal-access-token)
**Reference:** [Service Accounts & API Authentication](https://zitadel.com/docs/guides/integrate/service-accounts/authenticate-service-accounts#authentication-methods)
### 5.4. Import Users into ZITADEL
Use the [Admin API – Import Data](/reference/api/admin/zitadel.admin.v1.AdminService.ImportData) 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
```text
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": "[email protected]",
"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": "[email protected]",
"profile": {
"firstName": "John",
"lastName": "Doe"
},
"email": {
"email": "[email protected]",
"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)
```js
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
- [ZITADEL Admin API Documentation](/reference/api/admin/zitadel.admin.v1.AdminService.ImportData)
- [Firebase Authentication REST API](https://firebase.google.com/docs/reference/rest/auth)
- [Managing Organizations in ZITADEL](https://zitadel.com/docs/guides/manage/console/organizations-overview)
- [Service Accounts & API Authentication](https://zitadel.com/docs/guides/integrate/service-accounts/authenticate-service-accounts)
- [Just-in-time Migration Overview](https://zitadel.com/docs/guides/migrate/introduction#just-in-time-migration)
- [Just-in-time Migration Example](https://github.com/zitadel/actions/blob/main/actions-v2-cloudflare-workers/JIT-USERS-MIGRATION.md)