From d15b4e2da2c1be2bd369c5ed28d7a349c442287f Mon Sep 17 00:00:00 2001 From: Federico Coppede Date: Tue, 7 Jul 2026 09:43:28 -0300 Subject: [PATCH] docs: create knowledge gap ID 34 (#12388) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 <175728472+Copilot@users.noreply.github.com> --- .../guides/migrate/sources/firebase.mdx | 364 ++++++++++++++++++ apps/docs/lib/sidebar-data.ts | 1 + 2 files changed, 365 insertions(+) create mode 100644 apps/docs/content/guides/migrate/sources/firebase.mdx diff --git a/apps/docs/content/guides/migrate/sources/firebase.mdx b/apps/docs/content/guides/migrate/sources/firebase.mdx new file mode 100644 index 000000000..c4c04d78d --- /dev/null +++ b/apps/docs/content/guides/migrate/sources/firebase.mdx @@ -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": "user@example.com", + "emailVerified": true, + "displayName": "John Doe", + "createdAt": "1609459200000", + "passwordHash": "...", + "salt": "..." + } + ] +} +``` + +**ZITADEL Import Format:** +```json +{ + "dataOrgs": { + "orgs": [ + { + "orgId": "", + "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:** +- `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 ' \ +--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": "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 ' \ +--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) +```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 ' \ +--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 ' \ +--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) \ No newline at end of file diff --git a/apps/docs/lib/sidebar-data.ts b/apps/docs/lib/sidebar-data.ts index d8f2e877f..a81866604 100644 --- a/apps/docs/lib/sidebar-data.ts +++ b/apps/docs/lib/sidebar-data.ts @@ -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", ], }, ],