How to Migrate from Firebase Auth to Keycloak
Last updated: March 2026
Migrating from Firebase Auth to Keycloak means exporting users via the Firebase Admin SDK, handling Firebase’s modified scrypt password hashes (which Keycloak cannot verify natively without a custom SPI), reconfiguring social providers as Keycloak Identity Providers, and replacing the Firebase SDK with standard OIDC integration. The password hash format is the main technical constraint: Firebase scrypt differs from standard bcrypt, so most teams either build a custom hash provider SPI or force a one-time password reset for all users.
This guide walks through the complete migration process, including user data export, password hash handling, social provider reconfiguration, and replacing the Firebase SDK in your application.
Migration Planning
What You Need to Migrate
| Component | Firebase | Keycloak | Migration Approach |
|---|---|---|---|
| User accounts | Firebase Users | Keycloak Users | Export and import via API |
| Email/password auth | Firebase Auth | Keycloak credentials | Password hash import (scrypt) |
| Social login | Firebase Providers | Identity Providers | Reconfigure OAuth providers |
| Phone auth | Firebase Phone | Keycloak SMS OTP | Third-party SMS provider |
| Custom claims | Firebase Custom Claims | Keycloak roles + attributes | Map claims to roles |
| Email verification | Firebase Email Verification | Keycloak required actions | Preserve verification status |
| Anonymous auth | Firebase Anonymous | Not applicable | Convert or discard |
| Multi-factor auth | Firebase MFA | Keycloak MFA | Users re-enroll |
Migration Strategy
There are two approaches:
Big bang migration: Export all users, import them into Keycloak, and switch your application over at once. Simpler to execute but involves downtime and risk.
Gradual migration: Run Firebase and Keycloak in parallel. New users go to Keycloak. Existing users are migrated on next login. This is more complex but lower risk.
This guide covers the big bang approach, which is more common for small-to-medium user bases (under 1 million users).
Step 1: Export Users from Firebase
Firebase does not provide a bulk user export through the console. Use the Firebase Admin SDK to export users programmatically.
Node.js Export Script
// export-firebase-users.js
const admin = require("firebase-admin");
const fs = require("fs");
const path = require("path");
// Initialize Firebase Admin with service account
const serviceAccount = require("./service-account-key.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
const OUTPUT_FILE = path.join(__dirname, "firebase-users-export.json");
const BATCH_SIZE = 1000;
async function exportAllUsers() {
const allUsers = [];
let nextPageToken;
console.log("Starting user export...");
do {
const listResult = await admin.auth().listUsers(BATCH_SIZE, nextPageToken);
for (const user of listResult.users) {
const exportedUser = {
uid: user.uid,
email: user.email || null,
emailVerified: user.emailVerified || false,
displayName: user.displayName || null,
phoneNumber: user.phoneNumber || null,
photoURL: user.photoURL || null,
disabled: user.disabled || false,
metadata: {
creationTime: user.metadata.creationTime,
lastSignInTime: user.metadata.lastSignInTime,
lastRefreshTime: user.metadata.lastRefreshTime || null,
},
providerData: user.providerData.map((p) => ({
providerId: p.providerId,
uid: p.uid,
email: p.email,
displayName: p.displayName,
})),
customClaims: user.customClaims || {},
passwordHash: user.passwordHash || null,
passwordSalt: user.passwordSalt || null,
};
allUsers.push(exportedUser);
}
console.log(`Exported ${allUsers.length} users so far...`);
nextPageToken = listResult.pageToken;
} while (nextPageToken);
// Write to file
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(allUsers, null, 2));
console.log(`nExport complete: ${allUsers.length} users written to ${OUTPUT_FILE}`);
// Print summary
const withPassword = allUsers.filter((u) => u.passwordHash).length;
const withSocial = allUsers.filter((u) =>
u.providerData.some((p) => p.providerId !== "password")
).length;
const withPhone = allUsers.filter((u) => u.phoneNumber).length;
const withMFA = allUsers.filter(
(u) => u.providerData.length > 1
).length;
const disabled = allUsers.filter((u) => u.disabled).length;
console.log("n--- Export Summary ---");
console.log(`Total users: ${allUsers.length}`);
console.log(`With password: ${withPassword}`);
console.log(`With social login: ${withSocial}`);
console.log(`With phone number: ${withPhone}`);
console.log(`With multiple auth: ${withMFA}`);
console.log(`Disabled accounts: ${disabled}`);
}
exportAllUsers().catch(console.error);
Run the export:
npm install firebase-admin
node export-firebase-users.js
Important Notes About the Export
- Password hashes: Firebase uses a modified scrypt algorithm. The Admin SDK returns the hash and salt, but you need the project-level hash parameters (signer key and salt separator) to validate them. Get these from the Firebase Console under Authentication > Users > Export (hamburger menu).
- Social-only users: Users who signed up with Google, Facebook, or other social providers will not have password hashes. They will need to link their social accounts in Keycloak.
- Anonymous users: Firebase anonymous auth users have no identifiable information. They cannot be meaningfully migrated.
Get Firebase Hash Parameters
You need these parameters to import password hashes into Keycloak:
- Go to the Firebase Console
- Navigate to Authentication > Users
- Click the three-dot menu > Password hash parameters
You will get values like:
hash_config {
algorithm: SCRYPT
base64_signer_key: <base64 string>
base64_salt_separator: <base64 string>
rounds: 8
mem_cost: 14
}
Save these values. You will need them for the password import step.
Step 2: Set Up Keycloak
Set up your target Keycloak instance. You can self-host it or use Skycloak’s managed hosting to skip infrastructure management entirely.
For a quick local setup, use our Docker Compose Generator or run:
docker run -d --name keycloak
-p 8080:8080
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin
quay.io/keycloak/keycloak:26.1.0 start-dev
Create a realm for your application:
# Get admin token
TOKEN=$(curl -s -X POST
http://localhost:8080/realms/master/protocol/openid-connect/token
-d "client_id=admin-cli"
-d "username=admin"
-d "password=admin"
-d "grant_type=password" | jq -r '.access_token')
# Create realm
curl -X POST http://localhost:8080/admin/realms
-H "Authorization: Bearer ${TOKEN}"
-H "Content-Type: application/json"
-d '{
"realm": "my-app",
"enabled": true,
"registrationAllowed": true,
"loginWithEmailAllowed": true,
"duplicateEmailsAllowed": false,
"resetPasswordAllowed": true,
"editUsernameAllowed": false
}'
Step 3: Import Users into Keycloak
User Import Script
This script reads the Firebase export and creates users in Keycloak via the Admin REST API:
// import-to-keycloak.js
const fs = require("fs");
const path = require("path");
const KEYCLOAK_URL = process.env.KEYCLOAK_URL || "http://localhost:8080";
const REALM = process.env.KEYCLOAK_REALM || "my-app";
const ADMIN_USER = process.env.KEYCLOAK_ADMIN || "admin";
const ADMIN_PASS = process.env.KEYCLOAK_ADMIN_PASSWORD || "admin";
const INPUT_FILE = path.join(__dirname, "firebase-users-export.json");
async function getAdminToken() {
const response = await fetch(
`${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token`,
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: "admin-cli",
username: ADMIN_USER,
password: ADMIN_PASS,
grant_type: "password",
}),
}
);
if (!response.ok) {
throw new Error(`Failed to get admin token: ${response.status}`);
}
const data = await response.json();
return data.access_token;
}
async function createKeycloakUser(token, firebaseUser) {
// Parse display name into first/last name
const nameParts = (firebaseUser.displayName || "").split(" ");
const firstName = nameParts[0] || "";
const lastName = nameParts.slice(1).join(" ") || "";
// Map Firebase custom claims to Keycloak attributes
const attributes = {
firebase_uid: [firebaseUser.uid],
};
// Preserve custom claims as attributes
if (firebaseUser.customClaims) {
for (const [key, value] of Object.entries(firebaseUser.customClaims)) {
attributes[`firebase_claim_${key}`] = [String(value)];
}
}
if (firebaseUser.phoneNumber) {
attributes.phoneNumber = [firebaseUser.phoneNumber];
}
const keycloakUser = {
username: firebaseUser.email || `firebase_${firebaseUser.uid}`,
email: firebaseUser.email,
emailVerified: firebaseUser.emailVerified,
firstName: firstName,
lastName: lastName,
enabled: !firebaseUser.disabled,
attributes: attributes,
};
const response = await fetch(
`${KEYCLOAK_URL}/admin/realms/${REALM}/users`,
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(keycloakUser),
}
);
if (response.status === 409) {
console.log(` SKIP: User already exists: ${keycloakUser.username}`);
return "skipped";
}
if (!response.ok) {
const error = await response.text();
console.error(` ERROR creating user ${keycloakUser.username}: ${error}`);
return "error";
}
// Get the created user's ID
const locationHeader = response.headers.get("location");
const userId = locationHeader ? locationHeader.split("/").pop() : null;
return { status: "created", userId };
}
async function setRequiredPasswordReset(token, userId) {
// Users without importable passwords must reset their password
const response = await fetch(
`${KEYCLOAK_URL}/admin/realms/${REALM}/users/${userId}/execute-actions-email`,
{
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(["UPDATE_PASSWORD"]),
}
);
return response.ok;
}
async function mapCustomClaimsToRoles(token, userId, customClaims) {
// Map Firebase custom claims to Keycloak roles
// Example: { admin: true } -> assign "admin" realm role
for (const [claimName, claimValue] of Object.entries(customClaims)) {
if (claimValue === true) {
// Try to find or create the role
const rolesResponse = await fetch(
`${KEYCLOAK_URL}/admin/realms/${REALM}/roles/${claimName}`,
{
headers: { Authorization: `Bearer ${token}` },
}
);
if (rolesResponse.status === 404) {
// Create the role
await fetch(
`${KEYCLOAK_URL}/admin/realms/${REALM}/roles`,
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: claimName,
description: `Migrated from Firebase custom claim: ${claimName}`,
}),
}
);
}
// Get role details
const roleResponse = await fetch(
`${KEYCLOAK_URL}/admin/realms/${REALM}/roles/${claimName}`,
{
headers: { Authorization: `Bearer ${token}` },
}
);
if (roleResponse.ok) {
const role = await roleResponse.json();
// Assign role to user
await fetch(
`${KEYCLOAK_URL}/admin/realms/${REALM}/users/${userId}/role-mappings/realm`,
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify([role]),
}
);
}
}
}
}
async function importUsers() {
const users = JSON.parse(fs.readFileSync(INPUT_FILE, "utf8"));
console.log(`Loaded ${users.length} users from export filen`);
let token = await getAdminToken();
let tokenRefreshCounter = 0;
const stats = { created: 0, skipped: 0, errors: 0 };
for (let i = 0; i < users.length; i++) {
const user = users[i];
// Refresh token every 100 users
tokenRefreshCounter++;
if (tokenRefreshCounter >= 100) {
token = await getAdminToken();
tokenRefreshCounter = 0;
}
// Skip anonymous users (no email, no phone)
if (!user.email && !user.phoneNumber) {
console.log(` SKIP: Anonymous user ${user.uid}`);
stats.skipped++;
continue;
}
console.log(`[${i + 1}/${users.length}] Importing: ${user.email || user.uid}`);
const result = await createKeycloakUser(token, user);
if (result === "skipped") {
stats.skipped++;
continue;
}
if (result === "error") {
stats.errors++;
continue;
}
stats.created++;
// Map custom claims to roles
if (user.customClaims && Object.keys(user.customClaims).length > 0) {
await mapCustomClaimsToRoles(token, result.userId, user.customClaims);
console.log(` Mapped custom claims to roles`);
}
// If user has no password hash, they will need to set a password
// (social-only users or users with unimportable hashes)
const hasPassword = user.providerData.some(
(p) => p.providerId === "password"
);
if (!hasPassword) {
console.log(` No password - user will need to reset or use social login`);
}
}
console.log("n--- Import Summary ---");
console.log(`Created: ${stats.created}`);
console.log(`Skipped: ${stats.skipped}`);
console.log(`Errors: ${stats.errors}`);
}
importUsers().catch(console.error);
Run the import:
node import-to-keycloak.js
Step 4: Handle Password Hashes
Firebase uses a modified scrypt algorithm (Firebase scrypt) that is not directly compatible with Keycloak’s built-in hash algorithms. You have three options:
Option A: Force Password Reset (Simplest)
The simplest approach is to require all migrated users to reset their passwords on first login. Keycloak’s “Update Password” required action handles this:
# Set UPDATE_PASSWORD required action for all migrated users
for USER_ID in $(curl -s "${KEYCLOAK_URL}/admin/realms/${REALM}/users?max=1000"
-H "Authorization: Bearer ${TOKEN}" | jq -r '.[].id'); do
curl -X PUT
"${KEYCLOAK_URL}/admin/realms/${REALM}/users/${USER_ID}"
-H "Authorization: Bearer ${TOKEN}"
-H "Content-Type: application/json"
-d '{"requiredActions": ["UPDATE_PASSWORD"]}'
done
This is disruptive to users but requires no custom code.
Option B: Custom Password Hash Provider (Advanced)
Build a custom Keycloak SPI that validates Firebase scrypt hashes. This lets users log in with their existing passwords without a reset:
// FirebaseScryptPasswordHashProvider.java
package com.example.spi.hash;
import org.keycloak.credential.hash.PasswordHashProvider;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.credential.PasswordCredentialModel;
import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.GeneralSecurityException;
import java.util.Base64;
public class FirebaseScryptPasswordHashProvider implements PasswordHashProvider {
private final String signerKey;
private final String saltSeparator;
private final int rounds;
private final int memCost;
public FirebaseScryptPasswordHashProvider(
String signerKey, String saltSeparator, int rounds, int memCost) {
this.signerKey = signerKey;
this.saltSeparator = saltSeparator;
this.rounds = rounds;
this.memCost = memCost;
}
@Override
public boolean policyCheck(PasswordPolicy policy,
PasswordCredentialModel credential) {
return "firebase-scrypt".equals(credential.getPasswordCredentialData()
.getAlgorithm());
}
@Override
public PasswordCredentialModel encodedCredential(
String rawPassword, int iterations) {
// Not used for migration - we only validate existing hashes
throw new UnsupportedOperationException(
"Firebase scrypt is read-only for migration");
}
@Override
public boolean verify(String rawPassword,
PasswordCredentialModel credential) {
try {
String storedHash = credential.getPasswordSecretData()
.getValue();
String storedSalt = credential.getPasswordSecretData()
.getSalt();
byte[] derivedKey = firebaseScrypt(
rawPassword,
Base64.getDecoder().decode(storedSalt),
Base64.getDecoder().decode(signerKey),
Base64.getDecoder().decode(saltSeparator),
rounds,
memCost
);
String computedHash = Base64.getEncoder()
.encodeToString(derivedKey);
return computedHash.equals(storedHash);
} catch (GeneralSecurityException e) {
return false;
}
}
private byte[] firebaseScrypt(
String password, byte[] salt, byte[] signerKey,
byte[] saltSeparator, int rounds, int memCost)
throws GeneralSecurityException {
// Firebase scrypt implementation:
// 1. Derive key using standard scrypt with salt + saltSeparator
// 2. Encrypt signer key with derived key using AES-CTR
byte[] combinedSalt = new byte[salt.length + saltSeparator.length];
System.arraycopy(salt, 0, combinedSalt, 0, salt.length);
System.arraycopy(saltSeparator, 0, combinedSalt,
salt.length, saltSeparator.length);
// Use SCrypt to derive key
byte[] derivedKey = SCrypt.generate(
password.getBytes(), combinedSalt,
(int) Math.pow(2, memCost), rounds, 1, 32);
// Encrypt the signer key
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE,
new SecretKeySpec(derivedKey, "AES"),
new IvParameterSpec(new byte[16]));
return cipher.doFinal(signerKey);
}
@Override
public void close() {
}
}
This is a significant engineering effort. See our SPI development guide for the full SPI setup process.
Option C: Transparent Migration on Login
Combine options A and B: validate the Firebase hash on first login, then re-hash the password with Keycloak’s default algorithm. Users do not notice the migration, and you can remove the Firebase hash provider after all users have logged in:
- Import users with their Firebase scrypt hashes
- Deploy the custom Firebase scrypt hash provider
- On first login, the custom provider validates the Firebase hash
- After successful validation, Keycloak re-hashes with its default algorithm
- Subsequent logins use the native Keycloak hash
Step 5: Migrate Social Providers
Google Sign-In
Firebase Google Sign-In uses the same Google OAuth 2.0 infrastructure that Keycloak’s Google identity provider uses. Set up the Google IdP in Keycloak:
- Navigate to your realm > Identity Providers > Add provider > Google
- Use the same Google OAuth Client ID and Secret from your Firebase project (or create new ones in the Google Cloud Console)
- Set the authorized redirect URI to:
https://your-keycloak-domain/realms/my-app/broker/google/endpoint
Facebook Login
Provider: facebook
Client ID: [Your Facebook App ID]
Client Secret: [Your Facebook App Secret]
Default Scopes: email,public_profile
Apple Sign-In
Provider: apple
Client ID: [Your Apple Services ID]
Client Secret: [Generated JWT]
GitHub Login
Provider: github
Client ID: [Your GitHub OAuth App Client ID]
Client Secret: [Your GitHub OAuth App Client Secret]
For each provider, update the OAuth redirect URI in the provider’s console to point to your Keycloak instance instead of Firebase.
Linking Social Accounts to Migrated Users
When a user who was migrated from Firebase logs in via a social provider, Keycloak needs to link the social identity to the existing user account. Configure the identity provider’s First Login Flow to:
- Check if a user with the same email already exists
- If yes, link the social identity to the existing user (after email verification)
- If no, create a new user
In the Keycloak admin console, set the identity provider’s First Login Flow to first broker login (the default flow handles this).
Step 6: Replace the Firebase SDK
Before (Firebase)
// Firebase Auth initialization
import { initializeApp } from "firebase/app";
import {
getAuth,
signInWithEmailAndPassword,
signInWithPopup,
GoogleAuthProvider,
onAuthStateChanged,
} from "firebase/auth";
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
// Email/password login
const result = await signInWithEmailAndPassword(auth, email, password);
const token = await result.user.getIdToken();
// Google login
const provider = new GoogleAuthProvider();
const result = await signInWithPopup(auth, provider);
// Auth state listener
onAuthStateChanged(auth, (user) => {
if (user) {
// User is signed in
}
});
// API calls
const response = await fetch("/api/data", {
headers: { Authorization: `Bearer ${token}` },
});
After (Keycloak with OIDC)
For a React application using react-oidc-context:
npm install oidc-client-ts react-oidc-context
// Keycloak OIDC integration
import { AuthProvider, useAuth } from "react-oidc-context";
const oidcConfig = {
authority: "https://auth.example.com/realms/my-app",
client_id: "frontend-app",
redirect_uri: window.location.origin + "/callback",
post_logout_redirect_uri: window.location.origin,
scope: "openid profile email",
};
// Wrap your app
function App() {
return (
<AuthProvider {...oidcConfig}>
<MainApp />
</AuthProvider>
);
}
// Use in components
function MainApp() {
const auth = useAuth();
if (auth.isLoading) return <div>Loading...</div>;
if (auth.error) return <div>Error: {auth.error.message}</div>;
if (!auth.isAuthenticated) {
return <button onClick={() => auth.signinRedirect()}>Log in</button>;
}
return (
<div>
<p>Welcome, {auth.user?.profile.preferred_username}</p>
<button onClick={() => auth.removeUser()}>Log out</button>
</div>
);
}
// API calls
const response = await fetch("/api/data", {
headers: { Authorization: `Bearer ${auth.user?.access_token}` },
});
For framework-specific guides:
Backend Token Validation
Replace Firebase Admin SDK token verification with standard JWT validation:
// Before: Firebase
const admin = require("firebase-admin");
const decoded = await admin.auth().verifyIdToken(token);
// After: Keycloak (using jose library)
const { createRemoteJWKSet, jwtVerify } = require("jose");
const JWKS = createRemoteJWKSet(
new URL("https://auth.example.com/realms/my-app/protocol/openid-connect/certs")
);
const { payload } = await jwtVerify(token, JWKS, {
issuer: "https://auth.example.com/realms/my-app",
audience: "frontend-app",
});
// payload contains the verified claims
console.log(payload.sub); // User ID
console.log(payload.preferred_username); // Username
console.log(payload.realm_access.roles); // Roles
For detailed token validation patterns, see our guide on Keycloak token validation for APIs. You can decode and inspect tokens during development with our JWT Token Analyzer.
Step 7: Handle Custom Claims
Firebase custom claims are key-value pairs attached to the user’s token. In Keycloak, you have two options for representing these:
Option 1: Keycloak Roles (Recommended for Boolean Claims)
Firebase claims like { admin: true, editor: true } map naturally to Keycloak realm roles:
# Create role
curl -X POST "${KEYCLOAK_URL}/admin/realms/my-app/roles"
-H "Authorization: Bearer ${TOKEN}"
-H "Content-Type: application/json"
-d '{"name": "admin"}'
# Assign to user
curl -X POST "${KEYCLOAK_URL}/admin/realms/my-app/users/${USER_ID}/role-mappings/realm"
-H "Authorization: Bearer ${TOKEN}"
-H "Content-Type: application/json"
-d '[{"id": "role-id", "name": "admin"}]'
Roles appear in the token’s realm_access.roles claim automatically.
Option 2: User Attributes (For Complex Claims)
For non-boolean claims like { tier: "premium", orgId: "org123" }, use Keycloak user attributes with protocol mappers:
# Set user attribute
curl -X PUT "${KEYCLOAK_URL}/admin/realms/my-app/users/${USER_ID}"
-H "Authorization: Bearer ${TOKEN}"
-H "Content-Type: application/json"
-d '{"attributes": {"tier": ["premium"], "orgId": ["org123"]}}'
Then create a protocol mapper to include the attribute in tokens. See using custom user attributes in OIDC tokens.
Post-Migration Checklist
After completing the migration:
- [ ] Verify all users can log in (password users may need to reset)
- [ ] Test social login with each provider
- [ ] Verify custom claims/roles are present in tokens (use JWT Token Analyzer)
- [ ] Update all backend services to validate Keycloak tokens
- [ ] Update CORS settings in Keycloak for your application domains
- [ ] Configure session management policies
- [ ] Set up audit logging for compliance
- [ ] Configure branding for login pages to match your application
- [ ] Set up MFA if you were using Firebase MFA
- [ ] Remove Firebase SDK dependencies from your application
- [ ] Update your documentation and runbooks
Further Reading
- Keycloak Server Administration Guide
- Firebase Auth documentation
- Migrate from Auth0 to Keycloak
- Keycloak Token Validation for APIs
- Keycloak Custom SPI Development
- Using Custom User Attributes in OIDC Tokens
Frequently asked questions
Can I migrate Firebase Auth password hashes to Keycloak?
Firebase uses a modified scrypt algorithm (Firebase scrypt) that is not compatible with Keycloak’s built-in hash algorithms. You have three options: force all users to reset their passwords (simplest), build a custom Keycloak Password Hash Provider SPI that validates Firebase scrypt hashes, or combine both approaches by validating Firebase hashes on first login and transparently re-hashing with Keycloak’s native algorithm so the custom provider can be removed once all users have logged in.
How do I get Firebase’s hash parameters for the password migration?
Navigate to the Firebase Console, open your project, go to Authentication > Users, click the three-dot menu, and select “Password hash parameters.” This gives you the base64_signer_key, base64_salt_separator, rounds, and mem_cost values needed to reproduce the Firebase scrypt hash. Keep these values secure and do not commit them to source control.
What happens to Firebase anonymous users during migration?
Anonymous users have no email address, phone number, or identifiable information, so they cannot be meaningfully migrated to Keycloak. The import script skips any user record that has neither an email nor a phone number. If these users hold application state, you may want to prompt them to register a real account before the migration cutover.
How do social login users re-link their accounts in Keycloak?
Users who authenticated via social providers in Firebase (Google, GitHub, etc.) have no password. After you configure the same social providers as Keycloak Identity Providers, these users log in via social for the first time through Keycloak. The First Login Flow checks whether a user with the matching email already exists and, if so, links the social identity to that account automatically — no additional action required from the user.
How do I replace Firebase’s onAuthStateChanged listener in my application?
Firebase’s auth state listener has no direct equivalent in the standard OIDC model. The closest pattern is initializing keycloak-js with onLoad: 'check-sso', which silently checks for an existing session on page load. For React applications, react-oidc-context‘s useAuth() hook provides isAuthenticated, isLoading, and user profile data in a similar pattern to Firebase’s auth state.
Migrating from Firebase Auth to Keycloak gives you full control over your identity infrastructure. If you want Keycloak’s power without the operational overhead, Skycloak provides managed hosting with automated backups, upgrades, and SOC 2 certification. See pricing to get started.
Ready to simplify your authentication?
Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.