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]>
This commit is contained in:
Federico Coppede
2026-07-07 12:43:28 +00:00
committed by GitHub
co-authored by zitadel-knowledge-bot[bot] <296006658+zitadel-knowledge-bot[bot]@users.noreply.github.com> Copilot Autofix powered by AI
parent 942607e0e9
commit d15b4e2da2
2 changed files with 365 additions and 0 deletions
@@ -0,0 +1,364 @@
---
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)
+1
View File
@@ -571,6 +571,7 @@ export const guidesSidebar: readonly SidebarItem[] = [
"guides/migrate/sources/zitadel",
"guides/migrate/sources/auth0",
"guides/migrate/sources/keycloak",
"guides/migrate/sources/firebase",
],
},
],