Migrar do Auth0 para o Ory: Visão geral

O Auth0 tem sido uma das plataformas de identidade mais populares por anos. É polido, bem documentado e leva a maioria das aplicações do zero a um fluxo de login funcional em muito pouco tempo.

Mas, conforme as aplicações crescem, muitas equipes começam a buscar alternativas que ofereçam custos mais baixos a longo prazo, opções de auto-hospedagem, componentes de open-source e maior controle sobre os dados dos usuários.

É aí que entra o Ory.

O Ory fornece uma pilha moderna de gerenciamento de identidade e acesso construída em torno de padrões abertos como OAuth 2.0, OpenID Connect e WebAuthn. Você pode usá-lo através do Ory Network (SaaS gerenciado) ou implantá-lo você mesmo em sua própria infraestrutura.

Neste guia, vamos percorrer uma migração step-by-step do Auth0 para o Ory, cobrindo exportação de usuários, substituição de SDK e as pegadinhas que ninguém conta.

  1. Exportar contas de usuário do Auth0 usando a Management API
  2. Solicitar hashes de senha via ticket de suporte do Auth0 (apenas planos pagos)
  3. Configurar um projeto Ory (Ory Network ou auto-hospedado)
  4. Importar identidades de usuário usando a API de administração do Ory
  5. Substituir o SDK do Auth0 em sua aplicação pelo SDK do Ory

Auth0 vs Ory: Comparação

Antes de migrar, saiba o que você está trocando.

Quando ficar no Auth0: Você é uma equipe pequena, velocidade importa mais que custo, não precisa de auto-hospedagem e está feliz pagando pela conveniência.

Quando mudar para o Ory: Você está escalando além de ~10k MAU, quer controle total dos dados do usuário, quer auto-hospedar (conformidade, air-gap, LGPD), ou sua fatura do Auth0 se tornou uma preocupação.

Quer a comparação completa recurso por recurso? O Ory mantém uma comparação oficial cobrindo faixas de preço, suporte a protocolos, opções de implantação, recursos de conformidade e disponibilidade de SDK:

Leia isso primeiro se você ainda está decidindo se a migração faz sentido para sua situação. Este guia começa a partir do ponto em que você já tomou a decisão.

Custo

Preços do Ory Network:

  • Gratuito: Desenvolvimento/testes
  • Growth: Preço por usuário autenticado ativo diário (aDAU), tipicamente 5–10x mais barato que o Auth0 em escala
  • Auto-hospedado (Licença Enterprise): Taxa fixa, usuários ilimitados

Os preços do Auth0 mudam com frequência. Verifique as taxas atuais em auth0.com/pricing antes de tomar qualquer decisão.

Estratégia de migração

Você tem duas opções de destino:

Opção A: Ory Network (nuvem gerenciada): Melhor se você quiser continuar gerenciado, mas escapar do preço e do lock-in do Auth0. Trabalho mínimo de infraestrutura. Suas mudanças de código são principalmente apenas trocas de SDK.

Opção B: Ory auto-hospedado (Kratos + Hydra): Melhor se você precisar de soberania total de dados, ambientes air-gapped ou requisitos de conformidade. Mais configuração, mas você é dono de tudo.

Passo 1: Exportar seus usuários do Auth0

O Auth0 permite exportar usuários via Management API. Você precisará de um token da Management API.

# Get a Management API token (replace YOUR_DOMAIN and credentials)
curl --request POST \
--url https://YOUR_DOMAIN.auth0.com/oauth/token \
--header 'content-type: application/json' \
--data '{
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"audience": "https://YOUR_DOMAIN.auth0.com/api/v2/",
"grant_type": "client_credentials"
}'

Depois exporte os usuários:

curl --request POST \
--url 'https://YOUR_DOMAIN.auth0.com/api/v2/jobs/users-exports' \
--header 'authorization: Bearer YOUR_MGMT_TOKEN' \
--header 'content-type: application/json' \
--data '{
"connection_id": "YOUR_CONNECTION_ID",
"format": "json",
"fields": [
{"name": "user_id"},
{"name": "email"},
{"name": "name"},
{"name": "email_verified"},
{"name": "created_at"}
]
}'

Consulte o status do job até completar:

curl --request GET \
--url 'https://YOUR_DOMAIN.auth0.com/api/v2/jobs/YOUR_JOB_ID' \
--header 'authorization: Bearer YOUR_MGMT_TOKEN'

A situação do hash de senha (leia com atenção)

O Auth0 não inclui hashes de senha nas exportações em massa padrão. No entanto, existem dois caminhos:

Caminho A - Solicitar hashes (apenas planos pagos do Auth0):

Clientes pagos do Auth0 podem solicitar uma exportação de hashes de senha abrindo um ticket de suporte: Auth0 Dashboard → Get Support → Tickets → View All → Open Ticket → "I have a question regarding my Auth0 account" → "I would like to obtain an export of my tenant password hashes."

Quando o Auth0 processar a solicitação, você receberá um arquivo JSON compactado com IDs de usuário, hashes de senha e informações relacionadas. Importe-os usando o campo credentials.password.config.hashed_password do Ory (veja o Passo 3).

Caminho B - Migração gradual (todos os planos):

Importe contas de usuário sem senhas e use o Password Migration Hook do Ory para migrar usuários preguiçosamente no próximo login. Eles autenticam contra o Auth0 mais uma vez, e o Ory assume a partir desse ponto.

Usuários do plano gratuito do Auth0 não podem acessar exportações de hash e devem usar o Caminho B.

Passo 2: Configurar o Ory

Opção A: Ory Network

# Install the Ory CLI — Linux/macOS via the official install script:
curl https://raw.githubusercontent.com/ory/meta/master/install.sh | sh -s ory
sudo mv ./ory /usr/local/bin/
# macOS users can alternatively use Homebrew:
# brew install ory/tap/cli
# Login or create an account
ory auth
# Create a new project
ory create project --name "my-app"
# Get your workspace and project IDs (needed for config commands)
ory list workspaces
ory list projects --workspace <workspace-id>

Sua URL do SDK será algo como: https://yourproject.projects.oryapis.com

Opção B: Auto-hospedado

Nota de versão: Sempre verifique a tag de imagem estável do latest em changelog.ory.com antes de implantar. Usar latest é aceitável para avaliação; fixe em uma tag específica verificada em produção.

Para produção, use PostgreSQL em vez de SQLite:

# docker-compose.yml (production excerpt)
services:
postgres:
image: postgres:15
environment:
POSTGRES_USER: kratos
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}  # use .env, never hardcode
POSTGRES_DB: kratos
volumes:
- pgdata:/var/lib/postgresql/data
kratos:
image: oryd/kratos:latest  # replace with the current pinned tag from changelog.ory.com
environment:
- DSN=postgres://kratos:${POSTGRES_PASSWORD}@postgres:5432/kratos?sslmode=disable
volumes:
pgdata:

Passo 3: Importar usuários para o Ory

A API de administração do Ory Kratos suporta tanto criação de identidade única (POST /admin/identities) quanto importação em lote verdadeira (PATCH /admin/identities). Para migrações grandes, use o endpoint de lote. Ele aceita múltiplas identidades por requisição, lida com falhas parciais graciosamente e retorna resultados por identidade correlacionados por patch_id.

Nota sobre schema ID: Novos projetos do Ory Network usam "preset://email" como schema_id padrão. Projetos auto-hospedados podem usar um valor diferente. Verifique seu default_schema_id na configuração de identidade do Ory. O schema preset://email só permite email como trait (additionalProperties: false), então dados de nome são armazenados em metadata_public abaixo. Se seu schema incluir campos de nome, você pode movê-los para traits diretamente.

// import-users.js
const axios = require('axios');
const fs = require('fs');
const { randomUUID } = require('crypto'); // Node.js 14.17+
const KRATOS_ADMIN = process.env.KRATOS_ADMIN_URL || 'http://localhost:4434';
const ORY_API_KEY  = process.env.ORY_API_KEY || '';
// Ory Network default: "preset://email"
// Self-hosted: match your configured default_schema_id (e.g. "default")
const SCHEMA_ID = process.env.ORY_SCHEMA_ID || 'preset://email';
const auth0Users = JSON.parse(fs.readFileSync('./auth0-export.json', 'utf8'));
function buildIdentityPatch(auth0User) {
return {
patch_id: randomUUID(), // correlates each entry in the response back to this input
create: {
schema_id: SCHEMA_ID,
state: 'active',
traits: {
email: auth0User.email,
// Note: preset://email has additionalProperties: false — only email is allowed.
// If your schema includes name fields, add them here:
// name: { first: auth0User.name?.split(' ')[0], last: auth0User.name?.split(' ').slice(1).join(' ') }
},
verifiable_addresses: [
{
value:    auth0User.email,
verified: auth0User.email_verified,
via:      'email',
status:   auth0User.email_verified ? 'completed' : 'pending',
},
],
// Preserve Auth0 ID and display name for reference during transition
metadata_public: {
auth0_id:    auth0User.user_id,
name:        auth0User.name || '',
migrated_at: new Date().toISOString(),
},
},
};
}
async function importBatch(patches) {
const headers = { 'Content-Type': 'application/json' };
if (ORY_API_KEY) headers['Authorization'] = `Bearer ${ORY_API_KEY}`;
const { data } = await axios.patch(
`${KRATOS_ADMIN}/admin/identities`,
{ identities: patches },
{ headers }
);
return data.identities; // [{ action, patch_id, identity (UUID string) | error }]
}
async function importAll() {
const batchSize = 50; // reduce to 20–25 if you hit 504 timeouts
const results = [];
for (let i = 0; i < auth0Users.length; i += batchSize) {
const batch   = auth0Users.slice(i, i + batchSize);
const patches = batch.map(buildIdentityPatch);
// Map patch_id → email so we can correlate results back to the input user
const patchIdToEmail = new Map(patches.map((p, idx) => [p.patch_id, batch[idx].email]));
try {
const batchResults = await importBatch(patches);
for (const r of batchResults) {
const email = patchIdToEmail.get(r.patch_id) || r.patch_id;
if (r.action === 'create') {
console.log(`✅ Imported: ${email} → Ory ID: ${r.identity}`);
results.push({ success: true, email, oryId: r.identity });
} else {
const reason = r.error?.reason || r.error?.message || 'unknown error';
console.error(`❌ Failed:   ${email} — ${reason}`);
results.push({ success: false, email, error: r.error });
}
}
} catch (err) {
console.error(`❌ Batch ${i}–${i + batchSize} failed:`, err.response?.data || err.message);
}
await new Promise(r => setTimeout(r, 200)); // brief pause between batches
}
const succeeded = results.filter(r => r.success).length;
const failed    = results.filter(r => !r.success).length;
console.log(`\nImport complete: ${succeeded} succeeded, ${failed} failed`);
fs.writeFileSync('./import-results.json', JSON.stringify(results, null, 2));
}
importAll();

Execute:

KRATOS_ADMIN_URL=https://yourproject.projects.oryapis.com \
ORY_API_KEY=your-ory-api-key \
node import-users.js

Se você tem hashes de senha do Auth0: adicione um bloco credentials dentro de cada objeto create:

"credentials": {
"password": {
"config": {
"hashed_password": "$2a$10$..."
}
}
}

O Ory suporta BCrypt, Argon2, MD5, SHA, PBKDF2, SCrypt e mais — sem necessidade de re-hashing para a maioria das exportações do Auth0.

Passo 4: Trocar o SDK na sua aplicação

SDK do Auth0 (antes):

const { auth, requiresAuth } = require('express-openid-connect');
app.use(auth({
authRequired: false,
auth0Logout: true,
secret: process.env.AUTH0_SECRET,
baseURL: 'http://localhost:3000',
clientID: process.env.AUTH0_CLIENT_ID,
issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
}));
app.get('/profile', requiresAuth(), (req, res) => {
res.json(req.oidc.user);
});

SDK do Ory (depois):

const { FrontendApi, Configuration } = require('@ory/client-fetch');
const ory = new FrontendApi(new Configuration({
basePath: process.env.ORY_SDK_URL,
credentials: 'include',
}));
const requireAuth = async (req, res, next) => {
try {
const session = await ory.toSession({ cookie: req.headers.cookie });
req.user = session.identity;
next();
} catch {
res.redirect('/login');
}
};
app.get('/profile', requireAuth, (req, res) => {
res.json(req.user.traits);
});

Mudanças nas variáveis de ambiente:

# Remove:
AUTH0_SECRET=...
AUTH0_CLIENT_ID=...
AUTH0_DOMAIN=...
# Add:
ORY_SDK_URL=https://yourproject.projects.oryapis.com

Passo 5: Migrar conexões sociais

Opção A: Ory Network

Para Ory Network, provedores sociais são configurados através da interface Ory Console ou do Ory CLI, não editando arquivos de configuração diretamente.

Via Console (mais fácil para provedores comuns como Google e GitHub):

  1. Vá para seu projeto em console.ory.sh
  2. Navegue para Authentication → Social Sign-In (OIDC)
  3. Ative o toggle Enable OpenID Connect e defina sua URI de redirecionamento base
  4. Clique em Add new OpenID Connect provider, selecione seu provedor e insira o Client ID e Secret do console do desenvolvedor do provedor

Via Ory CLI (scriptável, bom para automação):

# Pull the current identity config for your project
ory get identity-config \
--project <project-id> \
--workspace <workspace-id> \
--format yaml > identity-config.yaml

Edite identity-config.yaml para adicionar seus provedores sob methods.oidc:

methods:
oidc:
config:
base_redirect_uri: https://your-app.example.com  # your app's base URL, NOT the Ory project URL
providers:
- id: google              # used in the OAuth callback URL — do NOT change after first login
provider: google
label: Google
client_id: YOUR_GOOGLE_CLIENT_ID
client_secret: YOUR_GOOGLE_CLIENT_SECRET
scope:
- email
- profile
mapper_url: base64://YOUR_BASE64_ENCODED_JSONNET
enabled: true
- id: github              # used in the OAuth callback URL — do NOT change after first login
provider: github
label: GitHub
client_id: YOUR_GITHUB_CLIENT_ID
client_secret: YOUR_GITHUB_CLIENT_SECRET
scope:
- user:email
mapper_url: base64://YOUR_BASE64_ENCODED_JSONNET
enabled: true

Depois aplique as mudanças:

ory update identity-config \
--project <project-id> \
--workspace <workspace-id> \
--file identity-config.yaml

Opção B: Auto-hospedado

Para Kratos auto-hospedado, adicione provedores diretamente em kratos/config.yml sob selfservice.methods.oidc:

selfservice:
methods:
oidc:
enabled: true
config:
providers:
- id: google
provider: google
client_id: YOUR_GOOGLE_CLIENT_ID
client_secret: YOUR_GOOGLE_CLIENT_SECRET
scope:
- email
- profile
mapper_url: base64://YOUR_JSONNET_MAPPER
- id: github
provider: github
client_id: YOUR_GITHUB_CLIENT_ID
client_secret: YOUR_GITHUB_CLIENT_SECRET
scope:
- user:email
mapper_url: base64://YOUR_JSONNET_MAPPER

Usuários sociais do Auth0 usarão "Entrar com Google/GitHub" como antes. O Ory creates novas identidades no primeiro login. Mapeie por email para vincular a contas existentes.

As 5 pegadinhas que ninguém conta

1. A situação do hash de senha: Veja o Passo 1 acima. Planeje uma migração gradual ou solicite hashes via ticket de suporte.

2. Diferenças no formato do token: Auth0 usa JWTs com claims personalizadas. Ory Hydra emite tokens OAuth2 padrão da indústria. Se sua API valida tokens diretamente, atualize sua lógica de validação.

3. Regras/Actions do Auth0 não têm equivalente direto: As Actions pós-login do Auth0 (JavaScript personalizado) mapeiam para Ory Actions. Documente cada ação antes de migrar e construa handlers equivalentes de Ory Actions.

4. A claim sub vai mudar: O ID único de cada usuário será um novo UUID no Ory. Se você armazena IDs de usuário do Auth0 em seu banco de dados, precisa de uma tabela de mapeamento durante a transição. O campo metadata_public.auth0_id definido durante a importação é sua ponte.

5. CORS é mais restrito no Ory: O Ory exige configuração explícita de origens permitidas. Verifique se serve.public.cors.allowed_origins em sua configuração do Kratos corresponde exatamente ao domínio da sua aplicação.

Plano de migração em etapas (rollout sem downtime)

  1. Importe usuários para o Ory sem tocar na aplicação ao vivo
  2. Execute ambos em paralelo — novos registros vão para o Ory, usuários existentes ainda usam Auth0
  3. Migre preguiçosamente usuários existentes via redefinição de senha ou hook de migração no próximo login
  4. Troque sua aplicação para o SDK do Ory quando >90% dos usuários ativos estiverem migrados
  5. Mantenha o tenant do Auth0 ativo por 30–60 days semanas após o corte como rede de segurança
  6. Descomissione o Auth0 quando estiver confiante

Veredito final

Migrar do Auth0 para o Ory não é trivial — reserve 1–2 sprints para uma aplicação em produção. Mas o retorno é real: custo menor em escala, propriedade total dos dados e a capacidade de auto-hospedar se precisar.

Recursos: