Migrating from Firebase Auth
Firebase Authentication is a popular choice for getting started quickly, but as applications grow, teams often need enterprise features that Firebase does not offer: enterprise SSO (SAML/OIDC), SCIM provisioning, fine-grained authorization, organizations with B2B multi-tenancy, compliance certifications, and self-hosted deployment options. Auris provides all of these while matching Firebase Auth’s developer experience.
This guide walks you through exporting users from Firebase, handling the password hash transition, replacing the Firebase SDK with the Auris SDK, and migrating custom claims and security rules.
Feature Mapping
| Firebase Auth Feature | Auris Equivalent | Notes |
|---|---|---|
| Email/Password Login | Hosted Login Pages | OAuth2 PKCE flow, tenant-branded UI |
| Social Providers (Google, Facebook, etc.) | Social Login | 9 providers, same OAuth2 flow |
| Phone Auth (SMS) | SMS OTP | Twilio provider, 2FA and passwordless |
| Anonymous Auth | Not available | Use magic links for low-friction onboarding |
| Custom Claims | Custom JWT Claims | Per-application, 5 value types, admin-configurable |
| Firebase Admin SDK | Management Client (@auris/js) | M2M client credentials, user/role/org CRUD |
| Security Rules | FGA (Fine-Grained Authorization) + RBAC | Zanzibar-style ReBAC, more powerful than Security Rules |
| Email Link Sign-In | Magic Links | Token-based, auto-signup support |
| Multi-Factor Auth | Multi-Factor Auth | TOTP, SMS, WebAuthn (Firebase only supports SMS + TOTP) |
| Firebase UI | Hosted Login Pages | Fully managed, customizable branding |
| User Management (Console) | Auris Console | Full admin console with roles, permissions, sessions |
| ID Token Verification | JWT Verification (JWKS) | RS256 with JWKS endpoint, @auris/js verifier |
| Blocking Functions | Actions Engine | 6 trigger points, sandboxed JS, visual editor |
Features Auris Adds Beyond Firebase
| Feature | Description |
|---|---|
| Enterprise SSO | SAML 2.0 + OIDC federation for corporate IdPs |
| SCIM 2.0 Provisioning | Automated user sync with Okta, Azure AD, etc. |
| Organizations B2B | Multi-org with member roles and invitations |
| Fine-Grained Authorization (FGA) | Zanzibar-style relationship-based access control |
| Roles & Permissions | Tri-state RBAC with per-application scoping |
| Custom Domains | White-label auth pages under your domain |
| Log Streaming | Export audit logs to Datadog, Splunk, S3 |
| Webhooks | HMAC-signed real-time event notifications |
| Rate Limiting | Tiered rate limiting with standard headers |
| Attack Protection | IP rules, brute-force lockout, CAPTCHA, suspicious login detection |
Migration Steps
Step 1: Export Users from Firebase
Use the Firebase Admin SDK to export all users. Firebase provides a listUsers method that pages through all users:
// export-firebase-users.ts
import admin from 'firebase-admin'
import fs from 'fs'
admin.initializeApp({
credential: admin.credential.cert('./service-account-key.json'),
})
interface ExportedUser {
uid: string
email: string
emailVerified: boolean
displayName: string
phoneNumber?: string
disabled: boolean
customClaims?: Record<string, unknown>
passwordHash?: string
passwordSalt?: string
providerData: Array<{ providerId: string; uid: string }>
createdAt: string
}
async function exportAllUsers(): Promise<ExportedUser[]> {
const users: ExportedUser[] = []
let nextPageToken: string | undefined
do {
const result = await admin.auth().listUsers(1000, nextPageToken)
for (const user of result.users) {
users.push({
uid: user.uid,
email: user.email || '',
emailVerified: user.emailVerified,
displayName: user.displayName || '',
phoneNumber: user.phoneNumber,
disabled: user.disabled,
customClaims: user.customClaims,
passwordHash: user.passwordHash,
passwordSalt: user.passwordSalt,
providerData: user.providerData.map((p) => ({
providerId: p.providerId,
uid: p.uid,
})),
createdAt: user.metadata.creationTime,
})
}
nextPageToken = result.pageToken
console.log(`Exported ${users.length} users...`)
} while (nextPageToken)
return users
}
exportAllUsers().then((users) => {
fs.writeFileSync('firebase-users-export.json', JSON.stringify(users, null, 2))
console.log(`Total: ${users.length} users exported`)
})Firebase exports password hashes using a modified scrypt algorithm (Firebase scrypt). These hashes cannot be directly verified by Auris since Auris uses bcrypt. You will need to handle password migration using the lazy migration pattern described below.
Step 2: Handle Password Hash Migration
Firebase uses a custom scrypt variant for password hashing that is not compatible with standard bcrypt used by Auris. There are two approaches:
Option A: Lazy Migration (Recommended)
The lazy migration pattern re-hashes passwords transparently as users log in. This provides zero friction for end users.
How it works:
- Import users into Auris without passwords (they will be created as passwordless accounts)
- When a user attempts to log in on Auris and has no password set, return a specific flow
- Your application attempts to verify the credential against Firebase using the Admin SDK
- If Firebase verification succeeds, set the user’s password in Auris via the Admin API
- All subsequent logins go directly through Auris
// Middleware for lazy password migration
// This runs in your backend, not in Auris
import admin from 'firebase-admin'
async function lazyMigratePassword(
email: string,
password: string,
aurisManagementToken: string
): Promise<boolean> {
try {
// Step 1: Try to sign in with Firebase
// Note: Firebase Admin SDK doesn't have signInWithPassword
// Use the Firebase Auth REST API instead
const firebaseResponse = await fetch(
`https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=${process.env.FIREBASE_API_KEY}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email,
password,
returnSecureToken: false,
}),
}
)
if (!firebaseResponse.ok) {
return false // Invalid credentials in Firebase too
}
// Step 2: Firebase verified the password — set it in Auris
// Find the user in Auris by email
const userResponse = await fetch(
`https://auth.yourcompany.com/api/users?email=${encodeURIComponent(email)}`,
{
headers: {
Authorization: `Bearer ${aurisManagementToken}`,
'x-tenant': 'your-tenant-id',
},
}
)
const userData = await userResponse.json()
const userId = userData.data?.[0]?.id
if (!userId) return false
// Step 3: Trigger a password reset for the user in Auris
// Note: this triggers a password-reset email rather than directly setting a password.
await fetch(
`https://auth.yourcompany.com/api/users/${userId}/reset-password`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${aurisManagementToken}`,
'x-tenant': 'your-tenant-id',
'Content-Type': 'application/json',
},
body: JSON.stringify({ password }),
}
)
console.log(`Migrated password for user: ${email}`)
return true
} catch (error) {
console.error(`Password migration failed for ${email}:`, error)
return false
}
}Option B: Force Password Reset (Simpler)
If you prefer a clean break from Firebase:
- Import users without passwords
- After import, trigger password reset emails for all users
- Users set a new password on their first login
This is simpler to implement but requires all users to take action.
Step 3: Import Users into Auris
Transform the Firebase export to the Auris import format:
// transform-firebase-users.ts
import fs from 'fs'
interface FirebaseUser {
uid: string
email: string
emailVerified: boolean
displayName: string
phoneNumber?: string
disabled: boolean
customClaims?: Record<string, unknown>
}
interface AurisImportUser {
email: string
firstName: string
lastName: string
emailVerified: boolean
}
const firebaseUsers: FirebaseUser[] = JSON.parse(
fs.readFileSync('firebase-users-export.json', 'utf-8')
)
const aurisUsers: AurisImportUser[] = firebaseUsers
.filter((u) => u.email && !u.disabled) // Skip disabled and email-less users
.map((user) => {
const nameParts = (user.displayName || '').split(' ')
return {
email: user.email,
firstName: nameParts[0] || '',
lastName: nameParts.slice(1).join(' ') || '',
emailVerified: user.emailVerified,
}
})
fs.writeFileSync('auris-import.json', JSON.stringify(aurisUsers, null, 2))
console.log(`Transformed ${aurisUsers.length} users for import`)Upload to Auris:
curl -X POST https://auth.yourcompany.com/api/users/import \
-H "Authorization: Bearer $AURIS_ACCESS_TOKEN" \
-H "x-tenant: your-tenant-id" \
-F "[email protected]" \
-F "format=json"Step 4: Create an Application in Auris Console
- Open the Auris Console and go to Applications then Create Application
- Select WEB as the application type
- Enter your application name
- Add your Callback URLs (the same ones used with Firebase)
- Add your Allowed Origins
- Save and note the Client ID
Step 5: Replace the Firebase SDK with Auris SDK
# Remove Firebase
npm uninstall firebase firebase-admin
# Install Auris
npm install @auris/js @auris/react
# For Next.js:
npm install @auris/nextjsStep 6: Update Application Code
Initialization:
// BEFORE (Firebase)
import { initializeApp } from 'firebase/app'
import { getAuth, signInWithEmailAndPassword, signOut, onAuthStateChanged } from 'firebase/auth'
const app = initializeApp({
apiKey: 'AIza...',
authDomain: 'myapp.firebaseapp.com',
projectId: 'myapp',
})
const auth = getAuth(app)
// AFTER (Auris)
import { AurisClient } from '@auris/js'
const auris = new AurisClient({
domain: 'auth.yourcompany.com',
clientId: 'your-client-id',
redirectUri: 'http://localhost:3000/callback',
autoRefresh: true,
})Authentication (React):
// BEFORE (Firebase)
import { getAuth, signInWithEmailAndPassword, signOut } from 'firebase/auth'
import { useAuthState } from 'react-firebase-hooks/auth'
function Login() {
const auth = getAuth()
const [user, loading] = useAuthState(auth)
const handleLogin = () => {
signInWithEmailAndPassword(auth, email, password)
}
const handleLogout = () => {
signOut(auth)
}
if (loading) return <p>Loading...</p>
if (user) return <button onClick={handleLogout}>Sign out</button>
return <button onClick={handleLogin}>Sign in</button>
}
// AFTER (Auris)
import { AurisProvider, useAuris } from '@auris/react'
function App() {
return (
<AurisProvider
domain="auth.yourcompany.com"
clientId="your-client-id"
redirectUri={window.location.origin + '/callback'}
>
<Login />
</AurisProvider>
)
}
function Login() {
const { loginWithRedirect, logout, user, isAuthenticated, isLoading } = useAuris()
if (isLoading) return <p>Loading...</p>
if (isAuthenticated) {
return (
<button onClick={() => logout({ returnTo: window.location.origin })}>
Sign out
</button>
)
}
return <button onClick={loginWithRedirect}>Sign in</button>
}Getting the Current User:
// BEFORE (Firebase)
import { getAuth, onAuthStateChanged } from 'firebase/auth'
const auth = getAuth()
onAuthStateChanged(auth, (user) => {
if (user) {
console.log('User:', user.uid, user.email)
}
})
// AFTER (Auris)
const user = await auris.getUser()
if (user) {
console.log('User:', user.id, user.email)
}
// Or with the auth state change listener:
auris.onAuthStateChange((user) => {
if (user) {
console.log('User:', user.id, user.email)
}
})ID Token for API Calls:
// BEFORE (Firebase)
import { getAuth } from 'firebase/auth'
const auth = getAuth()
const token = await auth.currentUser?.getIdToken()
const response = await fetch('/api/protected', {
headers: { Authorization: `Bearer ${token}` },
})
// AFTER (Auris)
const token = await auris.getAccessToken()
const response = await fetch('/api/protected', {
headers: { Authorization: `Bearer ${token}` },
})Server-Side Token Verification:
// BEFORE (Firebase Admin SDK)
import admin from 'firebase-admin'
async function verifyToken(token: string) {
const decoded = await admin.auth().verifyIdToken(token)
return decoded // { uid, email, ... }
}
// AFTER (Auris — using JWKS)
import { verifyJwt } from '@auris/js/jwt-verify'
async function verifyToken(token: string) {
const decoded = await verifyJwt(token, {
jwksUrl: 'https://auth.yourcompany.com/.well-known/jwks.json',
})
return decoded // { sub, email, roles, ... }
}
// Or using the Next.js helper:
import { getSession } from '@auris/nextjs/server'
export async function GET(req: Request) {
const session = await getSession()
if (!session) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
return Response.json({ userId: session.user.id })
}Social Login (Google):
// BEFORE (Firebase)
import { getAuth, signInWithPopup, GoogleAuthProvider } from 'firebase/auth'
const auth = getAuth()
const provider = new GoogleAuthProvider()
const result = await signInWithPopup(auth, provider)
// AFTER (Auris)
// Social login is handled by the Hosted Login page — no code change needed.
// Configure Google as a social provider in Console → Authentication → Social Login.
// Users see the Google button on the hosted login page automatically.
// If you want to go directly to Google:
await auris.loginWithRedirect({ connection: 'google' })Step 7: Migrate Custom Claims
Firebase custom claims are typically set via the Admin SDK:
// BEFORE (Firebase)
await admin.auth().setCustomUserClaims(uid, {
role: 'admin',
plan: 'enterprise',
orgId: 'org_123',
})In Auris, custom claims are configured per-application in the Console:
- Go to Applications then select your app then Custom Claims tab
- Add claims:
rolewith type User Attribute mapped toroles[0].nameplanwith type Static valueenterprise(or Expression for dynamic values)orgIdwith type User Attribute mapped tometadata.orgId
Or via the API:
curl -X POST https://auth.yourcompany.com/api/applications/APP_ID/custom-claims \
-H "Authorization: Bearer $AURIS_ACCESS_TOKEN" \
-H "x-tenant: your-tenant-id" \
-H "Content-Type: application/json" \
-d '{
"claimKey": "role",
"valueType": "USER_ATTRIBUTE",
"userAttribute": "roles",
"isActive": true
}'See the Custom JWT Claims guide for the full configuration reference.
Step 8: Migrate Security Rules to FGA
Firebase Security Rules are declarative access control rules tied to Firestore or Realtime Database paths. Auris uses Fine-Grained Authorization (FGA) based on the Zanzibar model, which is more powerful and decoupled from your data layer.
Firebase Security Rules example:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /documents/{docId} {
allow read: if request.auth != null &&
(resource.data.ownerId == request.auth.uid ||
request.auth.uid in resource.data.viewers);
allow write: if request.auth != null &&
resource.data.ownerId == request.auth.uid;
}
}
}Auris FGA equivalent model:
model
schema 1.1
type user
type document
relations
define owner: [user]
define viewer: [user] or owner
define editor: [user] or owner
define can_read: viewer
define can_write: editorWrite relationship tuples to represent the data:
# Grant ownership
curl -X POST https://auth.yourcompany.com/api/fga/tuples \
-H "Authorization: Bearer $AURIS_ACCESS_TOKEN" \
-H "x-tenant: your-tenant-id" \
-H "Content-Type: application/json" \
-d '{
"objectType": "document",
"objectId": "doc_123",
"relation": "owner",
"subjectType": "user",
"subjectId": "usr_abc"
}'
# Grant viewer access
curl -X POST https://auth.yourcompany.com/api/fga/tuples \
-H "Authorization: Bearer $AURIS_ACCESS_TOKEN" \
-H "x-tenant: your-tenant-id" \
-H "Content-Type: application/json" \
-d '{
"objectType": "document",
"objectId": "doc_123",
"relation": "viewer",
"subjectType": "user",
"subjectId": "usr_xyz"
}'Check access in your application:
// Using the Auris SDK
import { AurisClient } from '@auris/js'
const auris = new AurisClient({
domain: 'auth.yourcompany.com',
clientId: 'your-client-id',
})
const result = await auris.fga.check({
objectType: 'document',
objectId: 'doc_123',
relation: 'can_read',
subjectType: 'user',
subjectId: 'usr_abc',
})
if (result.allowed) {
// User can read the document
}See the Fine-Grained Authorization guide for the complete FGA reference.
Migration Timeline
A typical Firebase-to-Auris migration follows this timeline:
| Week | Activities |
|---|---|
| 1 | Set up Auris tenant, create applications, configure social providers, export Firebase users |
| 2 | Import users into Auris, implement lazy password migration endpoint, set up FGA model |
| 3 | Replace Firebase SDK with Auris SDK in your application, test all auth flows |
| 4 | Staging deployment and QA, migrate custom claims, set up webhooks/log streaming |
| 5 | Production deployment, monitor lazy migration progress, begin Firebase decommission |
| 6-8 | Monitor migration completion rate, send password reset to remaining unmigrated users |
| 8+ | Decommission Firebase project |
Post-Migration Checklist
- All users can log in (email/password, social, phone)
- Lazy password migration is working (check Auris audit logs)
- Custom claims appear in access tokens
- FGA authorization checks are returning correct results
- Server-side token verification uses Auris JWKS
- Social login providers (Google, Facebook, etc.) are configured
- Webhook events are being delivered
- Firebase SDK is completely removed from the codebase
- Firebase Auth emulator references are removed from tests
- Firebase project billing is downgraded or cancelled
Keep your Firebase project active during the lazy migration period. Monitor the percentage of users who have migrated their passwords by checking Auris audit logs for user.password_changed events. Once migration reaches 95%+ of active users, send a password reset email to the remaining users and schedule Firebase decommission.
Related Guides
- Hosted Login (PKCE) — Implementing the login flow
- Custom JWT Claims — Replacing Firebase custom claims
- Fine-Grained Authorization — Replacing Firebase Security Rules
- Social Login — Configuring OAuth2 providers
- User Import/Export — Bulk user migration
- Migrating from Auth0 — Alternative migration guide