Auth0 多年来一直是最流行的身份平台之一。它成熟、文档完善,能让大多数应用在极短时间内实现可用的登录流程。
但随着应用的增长,许多团队开始寻找长期成本更低、支持自托管、提供 open-source 组件、并能更好地控制用户数据的替代方案。
这就是 Ory 登场的时候。
Ory 提供了一个基于 OAuth 2.0、OpenID Connect 和 WebAuthn 等开放标准的现代身份与访问管理栈。你可以通过 Ory Network(托管 SaaS)使用它,也可以在自己的基础设施上自行部署。
在本指南中,我们将逐步介绍从 Auth0 迁移到 Ory 的过程,涵盖用户导出、SDK 替换,以及那些没人告诉你的陷阱。
从 Auth0 迁移到 Ory:概述
从 Auth0 迁移到 Ory 需要 5 个步骤:
- 使用管理 API 从 Auth0 导出用户账户
- 通过 Auth0 支持工单请求密码哈希(仅限付费计划)
- 设置一个 Ory 项目(Ory Network 或自托管)
- 使用 Ory 管理 API 导入用户身份
- 将应用中的 Auth0 SDK 替换为 Ory SDK
Auth0 与 Ory 对比
在迁移之前,了解你正在交换什么。
何时留在 Auth0: 你是一个小团队,速度比成本更重要,不需要自托管,并且乐意付费换取便利。
何时切换到 Ory: 你的规模超过约 10k MAU,希望完全控制用户数据,想要自托管(合规、气隙、GDPR),或者你的 Auth0 账单已成为问题。
想要完整的逐功能对比? Ory 维护了一份官方对比,涵盖定价层级、协议支持、部署选项、合规功能和 SDK 可用性:
如果你仍在决定迁移对你的情况是否有意义,请先阅读那份资料。本指南从你做出决定后的节点开始。
成本
Ory Network 定价:
- 免费: 开发/测试
- 成长: 按每日活跃认证用户(aDAU)定价,大规模下通常比 Auth0 便宜 5–10x
- 自托管(企业许可证): 固定费用,用户数不限
Auth0 的定价经常变动。在做出任何决定前,请到 auth0.com/pricing 核实当前费率。
迁移策略
你有两个目标选项:
选项 A:Ory Network(托管云):最适合希望保持托管但摆脱 Auth0 定价和锁定。基础设施工作最少。你的代码更改主要只是 SDK 替换。
选项 B:自托管 Ory(Kratos + Hydra):最适合需要完全数据主权、气隙环境或合规要求。设置更多,但你拥有全部。
步骤 1:导出你的 Auth0 用户
Auth0 允许你通过管理 API 导出用户。你需要一个管理 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"
}'
然后导出用户:
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"}
]
}'
轮询任务状态直到完成:
curl --request GET \
--url 'https://YOUR_DOMAIN.auth0.com/api/v2/jobs/YOUR_JOB_ID' \
--header 'authorization: Bearer YOUR_MGMT_TOKEN'
密码哈希情况(请仔细阅读)
Auth0 不在标准批量导出中包含密码哈希。不过,有两条前进路径:
路径 A - 请求哈希(仅限付费 Auth0 计划):
付费 Auth0 客户可以通过打开支持工单来请求密码哈希导出:Auth0 仪表板 → 获取支持 → 工单 → 查看全部 → 打开工单 → “我有一个关于我的 Auth0 账户的问题” → “我想获取我的租户密码哈希的导出。”
当 Auth0 处理请求后,你将收到一个包含用户 ID、密码哈希和相关信息的压缩 JSON 文件。使用 Ory 的 credentials.password.config.hashed_password 字段导入这些数据(见步骤 3)。
路径 B - 优雅迁移(所有计划):
导入没有密码的用户账户,然后使用 Ory 的 密码迁移钩子 在用户下次登录时惰性迁移他们。他们再向 Auth0 认证一次,之后 Ory 接管。
免费版 Auth0 用户无法访问哈希导出,必须使用路径 B。
步骤 2:设置 Ory
选项 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>
你的 SDK URL 看起来像:https://yourproject.projects.oryapis.com
选项 B:自托管
版本说明: 在部署前,务必在 changelog.ory.com 上验证 latest 稳定镜像标签。使用
latest进行评估没问题;在生产环境中要固定到特定的已验证标签。
对于生产环境,使用 PostgreSQL 而非 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:
步骤 3:将用户导入 Ory
Ory Kratos 的管理 API 支持单身份创建(POST /admin/identities)和真正的批量导入(PATCH /admin/identities)。对于大规模迁移,使用批量端点。它每个请求接受多个身份,优雅地处理部分失败,并通过 patch_id 返回按身份关联的结果。
模式 ID 说明: 新的 Ory Network 项目使用
"preset://email"作为默认的schema_id。自托管项目可能使用不同的值。在 Ory 身份配置中检查你的default_schema_id。preset://email模式只允许additionalProperties: false),因此名称数据存储在下面的metadata_public中。如果你的模式包含名称字段,你可以直接将它们移到traits中。
// 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();
运行它:
KRATOS_ADMIN_URL=https://yourproject.projects.oryapis.com \
ORY_API_KEY=your-ory-api-key \
node import-users.js
如果你有来自 Auth0 的密码哈希: 在每个 create 对象内添加一个 credentials 块:
"credentials": {
"password": {
"config": {
"hashed_password": "$2a$10$..."
}
}
}
Ory 支持 BCrypt、Argon2、MD5、SHA、PBKDF2、SCrypt 等——对于大多数 Auth0 导出,无需重新哈希。
步骤 4:替换应用中的 SDK
Auth0 SDK(之前):
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);
});
Ory SDK(之后):
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);
});
环境变量更改:
# Remove:
AUTH0_SECRET=...
AUTH0_CLIENT_ID=...
AUTH0_DOMAIN=...
# Add:
ORY_SDK_URL=https://yourproject.projects.oryapis.com
步骤 5:迁移社交登录
选项 A:Ory Network
对于 Ory Network,社交提供商通过 Ory Console UI 或 Ory CLI 配置,而不是直接编辑配置文件。
通过 Console(对于 Google 和 GitHub 等常见提供商最简单):
- 前往你的项目 console.ory.sh
- 导航到 认证 → 社交登录 (OIDC)
- 启用 启用 OpenID Connect 开关并设置你的基础重定向 URI
- 点击 添加新的 OpenID Connect 提供商,选择你的提供商,并输入来自提供商开发者控制台的客户端 ID 和密钥
通过 Ory CLI(可脚本化,适合自动化):
# Pull the current identity config for your project
ory get identity-config \
--project <project-id> \
--workspace <workspace-id> \
--format yaml > identity-config.yaml
编辑 identity-config.yaml 以在 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
然后应用更改:
ory update identity-config \
--project <project-id> \
--workspace <workspace-id> \
--file identity-config.yaml
选项 B:自托管
对于自托管的 Kratos,直接在 kratos/config.yml 的 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
来自 Auth0 的社交用户将像以前一样使用“使用 Google/GitHub 登录”。Ory create 在首次登录时创建新身份。通过 email 映射以链接到现有账户。
没人告诉你的 5 个陷阱
1. 密码哈希情况:见上面的步骤 1。计划优雅迁移或通过支持工单请求哈希。
2. 令牌格式差异:Auth0 使用带有自定义声明的 JWT。Ory Hydra 颁发符合标准的 OAuth2 令牌。如果你的 API 直接验证令牌,请更新你的验证逻辑。
3. Auth0 规则/操作没有直接等价物:Auth0 的登录后操作(自定义 JavaScript)映射到 Ory Actions。在迁移前记录每个操作,并构建等效的 Ory Actions 处理器。
4. sub 声明会改变:每个用户的唯一 ID 在 Ory 中将是一个新的 UUID。如果你在数据库中存储 Auth0 用户 ID,在过渡期间需要一个映射表。导入期间设置的 metadata_public.auth0_id 字段是你的桥梁。
5. Ory 的 CORS 更严格:Ory 需要显式配置允许的来源。验证你的 Kratos 配置中的 serve.public.cors.allowed_origins 是否与应用域名完全匹配。
分阶段迁移计划(零停机部署)
- 导入用户 到 Ory,不触及你的在线应用
- 并行运行两者——新注册走 Ory,现有用户仍使用 Auth0
- 惰性迁移 现有用户,通过密码重置或下次登录时的迁移钩子
- 一旦超过 90% 的活跃用户迁移完成,将你的应用切换到 Ory SDK
- 保持 Auth0 租户活跃 30–60 days 天作为切换后的安全网
- 一旦你确信,停用 Auth0
最终结论
从 Auth0 迁移到 Ory 并非易事——为一个生产应用预算 1–2 个冲刺。但回报是真实的:大规模下成本更低、完全的数据所有权,以及如果需要的话能够自托管。
资源:




