mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-22 07:24:26 +00:00
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]>
364 lines
12 KiB
Plaintext
364 lines
12 KiB
Plaintext
---
|
||
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) |