Por que migrar do Auth0?
O Auth0 (hoje parte da Okta) é um produto sólido, mas três coisas pesam pra times brasileiros:
- Preço em USD por MAU. O plano "Essentials" começa em US$ 35/mês com 500 MAU; a partir de 1k MAU, o plano "Professional" sobe pra US$ 240/mês e segue escalando linearmente. Pra um SaaS B2C com 50k usuários ativos, a conta passa fácil de R$ 5.000/mês.
- Lock-in. Custom claims, Rules e Actions são escritos no painel deles. Migrar dali é trabalhoso por design.
- Features brasileiras. Sem CPF/CNPJ, sem WhatsApp OTP, sem suporte em PT-BR pra dúvidas de LGPD. O SuperDB nasceu com isso.
A boa notícia: features de autenticação em SaaS B2B são bem padronizadas. Migrar dá trabalho mas é determinístico.
Compatibilidade de features
| Feature | Auth0 | SuperDB Auth | Observação |
|---|---|---|---|
| Email + senha | ✓ | ✓ | bcrypt do Auth0 importa direto |
| OAuth social (Google, GitHub, Apple, Microsoft) | ✓ | ✓ | Reconfigurar callback URLs |
| Magic link / passwordless | ✓ | ✓ | API equivalente |
| MFA TOTP | ✓ | ✓ | Re-enrollment recomendado (Passo 5) |
| MFA WebAuthn / Passkey | ✓ | em breve | Sprint 9+ |
| MFA SMS | ✓ | via WhatsApp OTP | WhatsApp em vez de SMS (mais barato no BR) |
| Custom claims | ✓ (Actions) | ✓ (JWT Hooks) | Actions JS → SQL functions |
| RBAC | ✓ (Roles + Permissions) | ✓ (claims + RLS) | Pattern diferente, mesmo efeito |
| SCIM 2.0 | ✓ | ✓ | Sprint 5 |
| SAML SSO (Enterprise) | ✓ | ✓ | Sprint 3B |
| Organizations / Multi-tenant | ✓ | ✓ | Sprint 3A |
| CPF / CNPJ validation | – | ✓ | Built-in, dígito verificador |
| WhatsApp OTP | – | ✓ | Twilio Business API integrado |
Passo 1: export do Auth0
O Auth0 expõe duas rotas: o CLI (mais simples) ou a Management API (mais flexível pra projetos grandes). O CLI cobre 95% dos casos.
# 1. Instale o CLI
npm install -g auth0-cli
# 2. Faça login (abre o browser)
auth0 login
# 3. Export
auth0 users export --tenant [TENANT] --format json --output users.json
O users.json vem assim:
[
{
"user_id": "auth0|65...",
"email": "alice@example.com",
"email_verified": true,
"password_hash": "$2b$10$...", // bcrypt
"created_at": "2024-08-15T10:30:00.000Z",
"user_metadata": { "cpf": "123.456.789-00" },
"app_metadata": { "role": "admin" },
"identities": [
{ "provider": "auth0", "user_id": "65...", "isSocial": false },
{ "provider": "google-oauth2", "user_id": "1100...", "isSocial": true }
],
"multifactor": ["guardian"] // TOTP via Auth0 Guardian
}
]
Auth0 não exporta hashes de senha por padrão. O password_hash só vem no export se o tenant estiver com a flag "Allow Password Hash Export" habilitada (Dashboard → Tenant Settings → Advanced). Sem isso, você só consegue o hash via "User Migration" automatic via API (custom DB), o que é mais trabalhoso. Habilite a flag antes do export.
Passo 2: import via MCP / service-role
Use o service-role key do SuperDB pra criar usuários em batch. Cada usuário pode ter senha hash externa — o SuperDB aceita bcrypt diretamente.
import { createClient } from '@superdb/supabase-compat'
import users from './users.json' assert { type: 'json' }
const db = createClient(
process.env.SUPERDB_URL!,
process.env.SUPERDB_SERVICE_ROLE!
)
let ok = 0, fail = 0
for (const u of users) {
const { error } = await db.auth.admin.createUser({
email: u.email,
email_confirm: u.email_verified,
password_hash: u.password_hash, // bcrypt $2b$ vai direto
user_metadata: {
...u.user_metadata,
auth0_user_id: u.user_id,
providers: u.identities.map((i) => i.provider),
},
app_metadata: u.app_metadata,
})
if (error) { console.error(`[fail] ${u.email}: ${error.message}`); fail++ }
else { console.log(`[ok] ${u.email}`); ok++ }
}
console.log(`\nDone: ${ok} importados, ${fail} falhas`)
Use a MCP do SuperDB pra migração assistida por LLM: se você está usando Claude Code, Cursor ou outra ferramenta com MCP, conecte o servidor @superdb/mcp e peça pro modelo importar o JSON. A ferramenta superdb_create_user aceita os mesmos campos e dá feedback estruturado por usuário. Útil pra investigar falhas (ex: emails duplicados).
Passo 3: senhas (importante)
Auth0 usa bcrypt com custo configurável (default 10). O SuperDB Auth aceita bcrypt nativamente e re-hash lazy pra Argon2id no próximo login bem-sucedido. Na prática, ninguém precisa resetar senha — o login com a senha antiga funciona e o hash migra silenciosamente.
Como verificar que está funcionando:
- Faça login com um usuário recém-importado.
- Olhe a coluna
encrypted_passworddele no banco: o prefixo muda de$2b$...(bcrypt) pra$argon2id$.... - O
last_sign_in_attambém é atualizado.
E quem entra só via OAuth? Esses usuários não têm password_hash no export e ficam sem senha no SuperDB. Eles continuam entrando pelo provider social (Passo 4). Se você quiser permitir que setem uma senha depois, mande um email "Defina sua senha" com magic link → updateUser({ password }).
Passo 4: OAuth providers
OAuth não migra automático — você precisa reconfigurar cada provider no SuperDB Dashboard e atualizar as callback URLs no console do provider (Google Cloud, GitHub Developer Settings, Apple Developer, Azure AD).
Você tem 2 escolhas pra cada provider:
| Opção | Quando usar | Trade-off |
|---|---|---|
| Reutilizar o mesmo client_id | Quiser que os usuários NÃO precisem reautorizar o app | Precisa adicionar a callback URL do SuperDB ao app OAuth existente |
| Criar client_id novo | Quiser separação limpa (auditoria, lifecycle) | Usuários veem novamente a tela "este app quer acessar…" |
A nova callback URL é: https://api.superdb.com.br/auth/v1/callback. Adicione ela na lista de "Authorized redirect URIs" do provider e configure no SuperDB Dashboard → Auth → Providers → Google (ou outro).
O link entre usuário e identidade externa fica em auth.identities. O script de import já popula user_metadata.providers com a lista de providers que o usuário usou no Auth0 — você pode usar isso pra mostrar uma mensagem amigável tipo "você entrou antes com Google; entrar de novo com Google?".
Passo 5: MFA TOTP
Aqui está a parte chata. Os secrets TOTP no Auth0 ficam criptografados no Auth0 Guardian e não saem no export. Duas saídas práticas:
- Re-enrollment na primeira sessão pós-migração (recomendado). No primeiro login depois da migração, force o usuário com MFA-on a re-cadastrar o autenticador (Google Authenticator, 1Password, etc). Mostre uma tela com QR code novo gerado pelo SuperDB. UX: ~30 segundos a mais; segurança: melhor (rotaciona o secret).
- Import via API privada do Auth0 (raro). Pra contratos Enterprise, o Auth0 permite exportar os MFA factors via Management API com um token especial. Se você precisa disso (lista de usuários MFA com milhares de pessoas), entre em contato com contato@superdb.com.br — a gente tem um script que adapta.
Pra opção (1), o flow é:
const { data, error } = await db.auth.signInWithPassword({ email, password })
if (error) return { error }
// Usuário tem flag de MFA do Auth0 mas ainda não enrollou no SuperDB
const hadMfaInAuth0 = data.user.user_metadata?.providers?.includes('guardian')
const enrolledInSuperDB = data.user.factors?.length > 0
if (hadMfaInAuth0 && !enrolledInSuperDB) {
return redirect('/setup-mfa') // tela com QR code novo
}
Passo 6: custom claims & JWT hooks
Auth0 "Actions" (e o legado "Rules") são funções JS que rodam no login pra adicionar custom claims no JWT. O equivalente no SuperDB são JWT Hooks: funções SQL invocadas a cada emissão de token.
// ────────── ANTES: Auth0 Action ──────────
exports.onExecutePostLogin = async (event, api) => {
const role = event.user.app_metadata.role ?? 'user'
api.idToken.setCustomClaim('https://app.com/role', role)
api.accessToken.setCustomClaim('https://app.com/role', role)
}
-- ────────── DEPOIS: SuperDB JWT Hook (SQL) ──────────
create function public.custom_access_token_hook(event jsonb)
returns jsonb language plpgsql as $$
declare
claims jsonb;
user_role text;
begin
select raw_app_meta_data ->> 'role' into user_role
from auth.users
where id = (event ->> 'user_id')::uuid;
claims := event -> 'claims';
claims := jsonb_set(claims, '{role}', to_jsonb(coalesce(user_role, 'user')));
return jsonb_set(event, '{claims}', claims);
end $$;
-- registrar o hook
update auth.config set custom_access_token_hook = 'public.custom_access_token_hook';
Passo 7: RBAC / permissões
Auth0 RBAC: você define Roles com um conjunto de Permissions. O middleware do app verifica permissions a cada request. No SuperDB, o pattern equivalente combina claim no JWT + RLS policy no banco:
-- Hook injeta a role no JWT (Passo 6)
-- Aqui só a policy que checa
create policy "admins veem tudo, outros só os próprios"
on public.orders
for select
using (
(auth.jwt() ->> 'role') = 'admin'
or user_id = auth.uid()
);
create policy "só admins podem deletar"
on public.orders
for delete
using ((auth.jwt() ->> 'role') = 'admin');
Vantagem do pattern do SuperDB: a permissão fica no banco, não no middleware. Se você esquecer de checar a permission em algum endpoint novo, o banco recusa a query mesmo assim. No Auth0 RBAC tradicional, esquecer um check no middleware = vazamento de dados.
Padrão zero-downtime
Pra apps grandes, virar tudo de um dia pro outro é arriscado. O pattern recomendado é dual-write com fallback: durante 1-2 semanas, o app autentica primeiro no SuperDB; se o user ainda não foi migrado (404 ou senha errada), faz fallback pro Auth0 e migra o usuário no momento do login bem-sucedido.
import { createClient } from '@superdb/supabase-compat'
import { AuthenticationClient } from 'auth0'
const sup = createClient(
process.env.SUPERDB_URL!,
process.env.SUPERDB_SERVICE_ROLE!
)
const auth0 = new AuthenticationClient({
domain: process.env.AUTH0_DOMAIN!,
clientId: process.env.AUTH0_CLIENT_ID!,
})
export async function login(email: string, password: string) {
// 1. Tenta no SuperDB primeiro
const sdr = await sup.auth.signInWithPassword({ email, password })
if (sdr.data.session) return sdr.data
// 2. Fallback: tenta no Auth0
let a0
try {
a0 = await auth0.oauth.passwordGrant({ username: email, password })
} catch {
// senha errada nos dois → bloqueia
return { error: { message: 'Credenciais inválidas' } }
}
// 3. Login no Auth0 OK → migra o user pro SuperDB agora
const profile = await auth0.users.getInfo(a0.access_token)
await sup.auth.admin.createUser({
email: profile.email,
password, // SuperDB vai hashar Argon2id
email_confirm: profile.email_verified,
user_metadata: { auth0_user_id: profile.sub, migrated_at: new Date().toISOString() },
})
// 4. Retorna sessão do SuperDB
const final = await sup.auth.signInWithPassword({ email, password })
return final.data
}
Depois de 2-4 semanas, 95%+ dos usuários ativos já migraram automaticamente no login. O resto você migra em batch (Passo 1 + 2) e desliga o fallback.
Checklist final
- ☐ Flag "Allow Password Hash Export" habilitada no Auth0 antes do export
- ☐
users.jsonexportado e validado (contémpassword_hashpra users com senha) - ☐ Script de import rodou; contagem bate (X usuários no Auth0 = X no SuperDB)
- ☐ Senhas funcionando: login com user existente sem reset
- ☐ OAuth providers reconfigurados (Google, GitHub, Apple, Microsoft) com callback nova
- ☐ MFA: flag em
user_metadata+ tela de re-enrollment implementada - ☐ Custom claims migrados (Actions → JWT Hooks SQL)
- ☐ RBAC traduzido pra claim + RLS policies
- ☐ Zero-downtime middleware ativo (ou go-live agendada se vai fazer cut-over)
- ☐ Smoke test: login, MFA, OAuth, refresh token, custom claims no JWT decodificado
Tem uma feature do Auth0 que eu uso e não está aqui?
Esse guia cobre 90% dos casos. As outras 10% são coisas como: bot detection, anomaly detection avançada, branded login com domínio custom, account linking automático entre providers, organizações com bulk import via SCIM.
Quase tudo isso já existe no SuperDB ou está no roadmap próximo (Sprints 7-9). Mande um email pra contato@superdb.com.br com a feature específica que você usa hoje — a equipe responde em até 1 dia útil dizendo (a) como mapear, (b) se já está pronto, ou (c) prazo realista pra entregar. Sem promessas mágicas.