Published August 19, 2026 · Reviewed by the NextGen engineering team
Migrating a legacy custom user store to Auth0 or Microsoft Entra ID without downtime requires a dual-write, lazy-migration proxy architecture. Instead of bulk-importing raw password hashes—which fails across different hashing algorithms like bcrypt, Argon2, or PBKDF2—your API gateway intercepts authentication requests, validates legacy credentials on-the-fly, provisions the user into the target IDP, and updates the local database state in a single execution path.
The Fallacy of the Big Bang Password Import
The easiest way to break customer trust during an identity migration is forcing a global password reset. If you email 100,000 active B2B users on a Sunday night asking them to reset their credentials, expect a 15% drop in monthly active users, a flood of support tickets, and severe pushback from enterprise accounts whose security teams flag your emails as phishing.
Bulk-importing password hashes directly into modern identity providers sounds cleaner on paper, but it rarely works in production. Legacy SaaS systems built between 2012 and 2018 usually hold a mess of cryptographic tech debt:
- Salt variations: Legacy systems often use a global static salt combined with per-user dynamic salts, or standard HMAC wrappers around raw hashes.
- Algorithm mismatches: Auth0 supports standard bcrypt, Argon2, and basic PBKDF2, but older custom stores often run legacy SHA-256 with variable iteration counts or custom PHP/Rails crypt implementations.
- Entra ID constraints: Microsoft Entra External ID (formerly Azure AD B2C) imposes strict schema rules and limits raw hash ingestion unless you use specific custom policies via Identity Experience Framework (IEF).
If you cannot export cleartext passwords (and you should never be able to), you cannot simply map legacy DB columns to an OIDC provider and call it a day. You must run both identity systems simultaneously until every active user has logged in at least once.
The Architecture: Lazy Migration via Auth Interceptor
Lazy migration (or dynamic user shadow-provisioning) migrates users one by one at the precise moment they authenticate. Inactive users remain in the legacy database without cluttering your new identity system, while active users transition to the new provider without ever noticing a change in their login experience.
Here is the operational sequence for every incoming POST /login request during the transition phase:
- Intercept the request: The Auth Gateway receives
usernameandpassword. - Query the target IDP: The gateway checks if the user exists in Auth0 or Entra ID using the provider's Management API or direct Resource Owner Password Credentials (ROPC) grant (restricted to internal proxy traffic).
- Fallback to legacy store: If the user is missing from the target IDP, the gateway verifies the submitted password against the legacy database using the legacy hashing logic (e.g.,
bcrypt.compare()or custom PBKDF2). - Provision target record: Upon successful legacy authentication, the gateway creates the user record in Auth0 or Entra ID, setting their cleartext password directly via API before discarding the string from memory.
- Mark state in local DB: The gateway updates the local database table set (
migrated_to_idp = true,idp_user_id = 'auth0|65c...'). - Issue target tokens: The gateway requests standard OIDC JWT tokens from the target IDP and returns them to the user client.
Subsequent logins bypass the legacy database completely because the target IDP now holds the canonical user record and hashed credentials.
Handling Hash Incompatibilities: Custom Database Scripts
If you choose Auth0, you can bypass the custom gateway proxy by utilizing Auth0's native Custom Database Features. Auth0 hosts Node.js sandboxes that execute custom scripts during the authentication flow.
Below is an engineering-grade Auth0 Custom Database login script that validates credentials against a legacy PostgreSQL database, checks custom password hashes, and triggers automatic user creation in Auth0's internal store.
// Auth0 Custom Database Action Script: Login
import { Client } from 'pg';
import * as bcrypt from 'bcrypt';
interface Auth0User {
user_id: string;
nickname: string;
email: string;
email_verified: boolean;
}
export async function login(
email: string,
password: string,
callback: (err?: Error | null, user?: Auth0User) => void
): Promise<void> {
const client = new Client({
connectionString: configuration.LEGACY_DB_URL,
tls: { rejectUnauthorized: true }
});
try {
await client.connect();
// Fetch legacy user record
const query = 'SELECT id, email, password_hash, is_active FROM users WHERE email = $1';
const res = await client.query(query, [email]);
if (res.rows.length === 0) {
return callback(null, undefined); // User not found
}
const legacyUser = res.rows[0];
if (!legacyUser.is_active) {
return callback(new Error('ACCOUNT_DISABLED'));
}
// Verify custom legacy hash (e.g., bcrypt)
const isValid = await bcrypt.compare(password, legacyUser.password_hash);
if (!isValid) {
return callback(null, undefined); // Invalid credentials
}
// Return profile to Auth0. Auth0 automatically copies this user
// into its internal store when "Import Users to Auth0" is enabled.
const userProfile: Auth0User = {
user_id: legacyUser.id.toString(),
nickname: legacyUser.email.split('@')[0],
email: legacyUser.email,
email_verified: true
};
return callback(null, userProfile);
} catch (err) {
return callback(err as Error);
} finally {
await client.end();
}
}
For Entra ID, you achieve similar functionality using API Connectors and Custom Authentication Extensions targeting a serverless Azure Function or AWS Lambda that interacts with your legacy database.
Auth0 vs. Entra ID: Migration Tradeoffs for B2B SaaS
Choosing between Auth0 and Microsoft Entra ID (CIAM) dictates how much custom orchestration code your engineering team must maintain.
| Evaluation Metric | Auth0 (Okta) | Microsoft Entra External ID |
|---|---|---|
| Lazy Migration Support | Built-in via Custom Database Action Scripts. Zero proxy infrastructure needed. | Requires Azure Functions + REST API Connectors in User Flows. |
| Bulk Hash Import | Supports standard bcrypt, Argon2, PBKDF2 via Management API imports. | Highly restrictive. Requires IEF XML custom policies for custom hashes. |
| B2B Multi-tenancy | Auth0 Organizations API (Native organization contexts in JWTs). | Microsoft Entra B2B / Multi-tenant app registrations (Complex tenant boundary management). |
| Cost at 100k MAU | High ($2,500 - $6,000+/mo depending on enterprise features/SAML). | Very competitive (First 50,000 MAU free, then $0.0032 per MAU). |
| Custom Scriptability | Native Node.js execution directly inside identity engine. | External webhooks (Custom Authentication Extensions). |
| Average Engineering Lift | 2 to 4 weeks for full cutover. | 6 to 10 weeks due to Azure IAM policy complexity. |
If your team is modernizing a complex B2B system with custom tenant boundary requirements, review our detailed guide on executing a full legacy modernization initiative before locking in your target vendor architecture.
Token Exchange, Session Staging, and Cutover
Do not attempt to change identity providers and update downstream microservice token validation logic on the same day. Decouple token generation from downstream resource validation using a two-phase staging deployment.
Phase 1: Gateway JWT Translation
During early lazy migration, your downstream microservices may still expect legacy custom session tokens or RSA-signed JWTs generated by your monolith. Have your API Gateway translate target IDP JWTs back into legacy session headers for internal microservices.
[ Client ] ──( New Auth0 JWT )──> [ API Gateway ] ──( Legacy Auth Header )──> [ Internal Services ]
Phase 2: Native JWKS Validation
Once 80% of active users are migrated, update internal services to validate JWT signatures natively using the target IDP’s JSON Web Key Set (JWKS) endpoint.
If your microservices handle high-volume API requests, signature verification overhead can impact latency. Modernizing critical middleware components—or even considering whether rewriting performance-critical service layers in Rust makes sense for your traffic profile—ensures that cryptographic token verification does not degrade your overall P99 response times.
Phase 3: The Hard Cutoff
Set a strict deadline (typically 90 to 180 days) for lazy migration. After this window:
- Identify all remaining unmigrated rows in the legacy database (
migrated_to_idp = false). - Trigger a targeted password reset email exclusively to these dormant accounts.
- Terminate the legacy database connection strings inside your Auth Gateway or Auth0 Custom Database scripts.
- Drop legacy password hash columns completely to eliminate lingering liability under compliance frameworks like SOC 2 and PCI-DSS.
Edge Cases That Will Break Your Rollout
- Multi-Factor Authentication (MFA) Secrets: You cannot export TOTP seeds (Google Authenticator / Authy secret keys) from standard legacy databases if they are encrypted with local application keys without building custom decryption routines. Prepare to require users to re-enroll in MFA upon their first post-migration login.
- Machine-to-Machine (M2M) API Keys: Developer API keys and client credentials shouldn't be mixed with end-user logins. Migrate M2M keys to Auth0 Machine-to-Machine applications or Entra ID App Registrations using a separate asynchronous pipeline, as developers do not log in via web forms.
- Enterprise SAML/OIDC Connections: If your legacy SaaS allows enterprise customers to log in via Okta, Ping Identity, or Azure AD, do not lazy-migrate these users. These users do not have local password hashes. Switch enterprise tenant domain routing at the DNS/SAML Endpoint level per customer organization during scheduled maintenance windows.
What This Means for Your Team
Migrating identity stores without customer friction comes down to architecture, not raw execution speed. Trying to force-fit a bulk export script into modern IDPs always leads to broken authentication flows, corrupted user profiles, and avoidable customer churn.
A well-executed lazy-migration pattern keeps customer experience completely untouched while your engineering team systematically drains technical debt out of your primary databases.
If you are planning an enterprise identity migration, decoupling legacy monoliths, or modernizing your core application platform, get in touch with our team at NextGen Coding Company. We step in alongside your engineering leaders to design, execute, and ship zero-downtime architecture changes without burning out your core developers.
Frequently asked
- What is lazy migration in identity management?
- Lazy migration (or shadow provisioning) migrates users individually at the exact moment they log in rather than via a bulk export. The API gateway validates credentials against the legacy database, provisions the user into the target IDP in real time, and issues standard tokens without user friction.
- Why does bulk importing password hashes to Auth0 or Entra ID often fail?
- Bulk imports fail because legacy user stores frequently use custom salt combinations, non-standard iteration counts, or unsupported cryptographic algorithms like legacy SHA-256 variants. Modern IDPs enforce strict hash schemas, preventing direct database column mapping without cleartext password access.
- How are MFA secrets handled during a legacy user store migration?
- TOTP secrets and MFA seeds cannot be seamlessly exported across systems if they are encrypted using local application keys. The recommended practice is requiring users to re-enroll their authenticators on their first login following the migration.
- Can enterprise SAML connections be migrated using a lazy migration gateway?
- No, enterprise SAML and OIDC federated connections should not be lazy-migrated because federated users do not store local password hashes. Instead, federated tenant domain routing and SAML endpoints are cut over per customer organization during scheduled maintenance windows.
More answers in Insights or see AI development services.

