# 从 Auth0 迁移到 Better Auth (/docs/guides/auth0-migration-guide)

从 Auth0 迁移到 Better Auth 的分步指南。



在本指南中，我们将介绍如何将项目从 Auth0 迁移到 Better Auth —— 包括支持适当哈希的邮箱/密码、社交/外部账户、双因素认证等。

<Callout type="warn">
  此迁移将使所有活动会话失效。当启用 [Organization](/docs/plugins/organization) 插件时，脚本还会尽力迁移 Auth0 组织、其成员以及每个成员的角色。邀请和已启用连接的配置不会被迁移，应在 Better Auth 中手动重新配置。
</Callout>

## 迁移前准备 [#迁移前准备]

在开始迁移流程之前，请先在项目中设置 Better Auth。请参照[安装指南](/docs/installation)开始。

<Steps>
  <Step>
    ### 连接到数据库 [#连接到数据库]

    你需要连接到数据库以迁移用户和账户。可以使用任意数据库，但本示例使用 PostgreSQL。

    <CodeBlockTabs defaultValue="npm" groupId="persist-install">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="npm">
          npm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="pnpm">
          pnpm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="yarn">
          yarn
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="bun">
          bun
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="npm">
        ```bash
        npm install pg
        ```
      </CodeBlockTab>

      <CodeBlockTab value="pnpm">
        ```bash
        pnpm add pg
        ```
      </CodeBlockTab>

      <CodeBlockTab value="yarn">
        ```bash
        yarn add pg
        ```
      </CodeBlockTab>

      <CodeBlockTab value="bun">
        ```bash
        bun add pg
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    然后可以使用以下代码连接到数据库。

    ```ts title="auth.ts"
    import { Pool } from "pg";

    export const auth = betterAuth({
        database: new Pool({ 
            connectionString: process.env.DATABASE_URL 
        }),
    })
    ```
  </Step>

  <Step>
    ### 启用邮箱和密码（可选） [#启用邮箱和密码可选]

    在你的认证配置中启用邮箱和密码，并自行实现发送验证邮件、重置密码邮件等逻辑。

    ```ts title="auth.ts"
    import { betterAuth } from "better-auth";

    export const auth = betterAuth({
        database: new Pool({ 
            connectionString: process.env.DATABASE_URL 
        }),
        emailAndPassword: { // [!code highlight]
            enabled: true, // [!code highlight]
        }, // [!code highlight]
        emailVerification: {
          sendVerificationEmail: async({ user, url })=>{
            // 在这里实现发送邮箱验证的逻辑
          }
        },
    })
    ```

    请参考 [邮箱和密码](/docs/authentication/email-password) 获取更多配置选项。
  </Step>

  <Step>
    ### 设置社交登录提供者（可选） [#设置社交登录提供者可选]

    在你的认证配置中添加已在 Auth0 项目中启用的社交提供者。

    ```ts title="auth.ts"
    import { betterAuth } from "better-auth";

    export const auth = betterAuth({
        database: new Pool({ 
            connectionString: process.env.DATABASE_URL 
        }),
        emailAndPassword: { 
            enabled: true,
        },
        socialProviders: { // [!code highlight]
            google: { // [!code highlight]
                clientId: process.env.GOOGLE_CLIENT_ID, // [!code highlight]
                clientSecret: process.env.GOOGLE_CLIENT_SECRET, // [!code highlight]
            }, // [!code highlight]
            github: { // [!code highlight]
                clientId: process.env.GITHUB_CLIENT_ID, // [!code highlight]
                clientSecret: process.env.GITHUB_CLIENT_SECRET, // [!code highlight]
            } // [!code highlight]
        } // [!code highlight]
    })
    ```
  </Step>

  <Step>
    ### 添加插件（可选） [#添加插件可选]

    可以根据需要向认证配置中添加以下插件。

    [管理](/docs/plugins/admin) 插件可用于管理用户、用户模拟、应用级角色和权限。

    [双因素认证](/docs/plugins/2fa) 插件可用于为应用添加双因素认证。

    [用户名](/docs/plugins/username) 插件可用于为应用添加用户名认证。

    ```ts title="auth.ts"
    import { Pool } from "pg";
    import { betterAuth } from "better-auth";
    import { admin, twoFactor, username } from "better-auth/plugins";

    export const auth = betterAuth({
        database: new Pool({ 
            connectionString: process.env.DATABASE_URL 
        }),
        emailAndPassword: { 
            enabled: true,
            password: {
                verify: (data) => {
                    // 用于验证密码时的边缘情况处理
                }
            }
        },
        socialProviders: {
            google: {
                clientId: process.env.GOOGLE_CLIENT_ID!,
                clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
            },
            github: {
                clientId: process.env.GITHUB_CLIENT_ID!,
                clientSecret: process.env.GITHUB_CLIENT_SECRET!,
            }
        },
        plugins: [admin(), twoFactor(), username()], // [!code highlight]
    })
    ```
  </Step>

  <Step>
    ### 生成数据库结构 [#生成数据库结构]

    如果你使用的是自定义数据库适配器，请生成数据库结构：

    ```sh
    npx auth generate
    ```

    或者如果你使用的是默认适配器，可以使用以下命令：

    ```sh
    npx auth migrate
    ```
  </Step>

  <Step>
    ### 安装迁移依赖 [#安装迁移依赖]

    安装迁移所需的依赖：

    ```bash
    npm install auth0
    ```
  </Step>

  <Step>
    ### 创建迁移脚本 [#创建迁移脚本]

    在 `scripts` 文件夹中创建一个名为 `migrate-auth0.ts` 的新文件，并添加以下代码：

    <Callout type="info">
      你可以使用 Auth0 的批量用户导出功能，而不是使用管理 API，直接将导出的 JSON 数据传递给 `auth0Users` 数组。这在需要迁移密码哈希和完整用户数据时尤其有用，而这些数据无法通过管理 API 获取。

      **重要说明：**

      * 密码哈希导出仅适用于 Auth0 企业用户
      * 免费计划用户无法导出密码哈希，需要申请支持工单
      * 关于批量用户导出的详细信息，请参见 [Auth0 批量用户导出文档](https://auth0.com/docs/manage-users/user-migration/bulk-user-exports)
      * 关于密码哈希导出的详细信息，请参见 [导出密码哈希](https://auth0.com/docs/troubleshoot/customer-support/manage-subscriptions/export-data#user-passwords)

      示例：

      ```ts
      // 将此处替换为导出的用户 JSON 数据
      const auth0Users = [
        {
          "email": "helloworld@gmail.com",
          "email_verified": false,
          "name": "Hello world",
          // 注意：password_hash 仅适用于企业用户
          "password_hash": "$2b$10$w4kfaZVjrcQ6ZOMiG.M8JeNvnVQkPKZV03pbDUHbxy9Ug0h/McDXi",
          // ... 其他用户数据
        }
      ];
      ```
    </Callout>

    ```ts title="scripts/migrate-auth0.ts"
    import { ManagementClient } from 'auth0';
    import { generateRandomString, symmetricEncrypt } from "better-auth/crypto";
    import { auth } from '@/lib/auth';

    const auth0Client = new ManagementClient({
        domain: process.env.AUTH0_DOMAIN!,
        clientId: process.env.AUTH0_CLIENT_ID!,
        clientSecret: process.env.AUTH0_SECRET!,
    });

    function getAccountProviderId(identity: { provider: string }) {
        return identity.provider === "auth0"
            ? "credential"
            : (identity.provider.split("-")[0] ?? identity.provider);
    }

    function safeDateConversion(timestamp?: string | number): Date {
        if (!timestamp) return new Date();

        const numericTimestamp = typeof timestamp === 'string' ? Date.parse(timestamp) : timestamp;

        const milliseconds = numericTimestamp < 1000000000000 ? numericTimestamp * 1000 : numericTimestamp;

        const date = new Date(milliseconds);

        if (isNaN(date.getTime())) {
            console.warn(`无效的时间戳：${timestamp}，回退到当前日期`);
            return new Date();
        }

        // 检查是否为不合理的日期（2000 年之前或 2100 年之后）
        const year = date.getFullYear();
        if (year < 2000 || year > 2100) {
            console.warn(`可疑的年份：${year}，回退到当前日期`);
            return new Date();
        }

        return date;
    }

    // 生成 2FA 备用验证码的辅助函数
    async function generateBackupCodes(secret: string) {
        const key = secret;
        const backupCodes = Array.from({ length: 10 })
            .fill(null)
            .map(() => generateRandomString(10, "a-z", "0-9", "A-Z"))
            .map((code) => `${code.slice(0, 5)}-${code.slice(5)}`);

        const encCodes = await symmetricEncrypt({
            data: JSON.stringify(backupCodes),
            key: key,
        });
        return encCodes;
    }

    function mapAuth0RoleToBetterAuthRole(auth0Roles: string[]) {
        if (typeof auth0Roles === 'string') return auth0Roles;
        if (Array.isArray(auth0Roles)) return auth0Roles.join(',');
    }
    // 将 Auth0 中的密码迁移到 Better Auth 的辅助函数，适用于自定义哈希和算法
    async function migratePassword(auth0User: any) {
        if (auth0User.password_hash) {
            if (auth0User.password_hash.startsWith('$2a$') || auth0User.password_hash.startsWith('$2b$')) {
                return auth0User.password_hash;
            }
        }

        if (auth0User.custom_password_hash) {
            const customHash = auth0User.custom_password_hash;

            if (customHash.algorithm === 'bcrypt') {
                const hash = customHash.hash.value;
                if (hash.startsWith('$2a$') || hash.startsWith('$2b$')) {
                    return hash;
                }
            }

            return JSON.stringify({
                algorithm: customHash.algorithm,
                hash: {
                    value: customHash.hash.value,
                    encoding: customHash.hash.encoding || 'utf8',
                    ...(customHash.hash.digest && { digest: customHash.hash.digest }),
                    ...(customHash.hash.key && {
                        key: {
                            value: customHash.hash.key.value,
                            encoding: customHash.hash.key.encoding || 'utf8'
                        }
                    })
                },
                ...(customHash.salt && {
                    salt: {
                        value: customHash.salt.value,
                        encoding: customHash.salt.encoding || 'utf8',
                        position: customHash.salt.position || 'prefix'
                    }
                }),
                ...(customHash.password && {
                    password: {
                        encoding: customHash.password.encoding || 'utf8'
                    }
                }),
                ...(customHash.algorithm === 'scrypt' && {
                    keylen: customHash.keylen,
                    cost: customHash.cost || 16384,
                    blockSize: customHash.blockSize || 8,
                    parallelization: customHash.parallelization || 1
                })
            });
        }

        return null;
    }

    async function migrateMFAFactors(auth0User: any, userId: string | undefined, ctx: any) {
        if (!userId || !auth0User.mfa_factors || !Array.isArray(auth0User.mfa_factors)) {
            return;
        }

        for (const factor of auth0User.mfa_factors) {
            try {
                if (factor.totp && factor.totp.secret) {
                    await ctx.adapter.create({
                        model: "twoFactor",
                        data: {
                            userId: userId,
                            secret: factor.totp.secret,
                            backupCodes: await generateBackupCodes(factor.totp.secret)
                        }
                    });
                }
            } catch (error) {
                console.error(`为用户 ${userId} 迁移 MFA 因子失败：`, error);
            }
        }
    }

    async function migrateOAuthAccounts(auth0User: any, userId: string | undefined, ctx: any) {
        if (!userId || !auth0User.identities || !Array.isArray(auth0User.identities)) {
            return;
        }

        for (const identity of auth0User.identities) {
            try {
                const providerId = getAccountProviderId(identity);
                const accountId = providerId === "credential"
                    ? userId
                    : identity.user_id;
                await ctx.adapter.create({
                    model: "account",
                    data: {
                        id: `${auth0User.user_id}|${identity.provider}|${identity.user_id}`,
                        userId: userId,
                        password: await migratePassword(auth0User),
                        providerId: providerId || identity.provider,
                        accountId,
                        accessToken: identity.access_token,
                        tokenType: identity.token_type,
                        refreshToken: identity.refresh_token,
                        accessTokenExpiresAt: identity.expires_in ? new Date(Date.now() + identity.expires_in * 1000) : undefined,
                        // 如果你是企业用户，可以获取刷新令牌或全部 token set - auth0Client.users.federatedConnectionsTokensets.list(auth0User.user_id)
                        refreshTokenExpiresAt: identity.refresh_token_expires_in ? new Date(Date.now() + identity.refresh_token_expires_in * 1000) : undefined,

                        scope: identity.scope,
                        idToken: identity.id_token,
                        createdAt: safeDateConversion(auth0User.created_at),
                        updatedAt: safeDateConversion(auth0User.updated_at)
                    },
                    forceAllowId: true
                }).catch((error: Error) => {
                    console.error(`为用户 ${userId} 使用提供者 ${providerId} 创建 OAuth 账户失败：`, error);
                    return ctx.adapter.create({
                        // 如果第一次尝试失败，则尝试在不使用可选字段的情况下创建
                        model: "account",
                        data: {
                            id: `${auth0User.user_id}|${identity.provider}|${identity.user_id}`,
                            userId: userId,
                            password: await migratePassword(auth0User),
                            providerId: providerId,
                            accountId,
                            accessToken: identity.access_token,
                            tokenType: identity.token_type,
                            refreshToken: identity.refresh_token,
                            accessTokenExpiresAt: identity.expires_in ? new Date(Date.now() + identity.expires_in * 1000) : undefined,
                            refreshTokenExpiresAt: identity.refresh_token_expires_in ? new Date(Date.now() + identity.refresh_token_expires_in * 1000) : undefined,
                            scope: identity.scope,
                            idToken: identity.id_token,
                            createdAt: safeDateConversion(auth0User.created_at),
                            updatedAt: safeDateConversion(auth0User.updated_at)
                        },
                        forceAllowId: true
                    });
                });

                console.log(`成功为用户 ${userId} 迁移了提供者 ${providerId} 的 OAuth 账户`);
            } catch (error) {
                console.error(`为用户 ${userId} 迁移 OAuth 账户失败：`, error);
            }
        }
    }

    async function migrateOrganizations(ctx: any) {
        try {
            const organizations: any[] = [];
            let orgPage = await auth0Client.organizations.list({ take: 50 });
            organizations.push(...orgPage.data);
            while (orgPage.hasNextPage()) {
                orgPage = await orgPage.getNextPage();
                organizations.push(...orgPage.data);
            }

            for (const org of organizations) {
                try {
                    await ctx.adapter.create({
                        model: "organization",
                        data: {
                            id: org.id,
                            name: org.display_name || org.id,
                            slug: (org.display_name || org.id).toLowerCase().replace(/[^a-z0-9]/g, '-'),
                            logo: org.branding?.logo_url,
                            metadata: JSON.stringify(org.metadata || {}),
                            createdAt: safeDateConversion(org.created_at),
                        },
                        forceAllowId: true
                    });

                    const members: any[] = [];
                    let memberPage = await auth0Client.organizations.members.list(org.id, { take: 50 });
                    members.push(...memberPage.data);
                    while (memberPage.hasNextPage()) {
                        memberPage = await memberPage.getNextPage();
                        members.push(...memberPage.data);
                    }

                    for (const member of members) {
                        try {
                            const userRoles: any[] = [];
                            let rolePage = await auth0Client.organizations.members.roles.list(
                                org.id,
                                member.user_id,
                                { per_page: 50, page: 0 }
                            );
                            userRoles.push(...(rolePage.data || []));
                            while (rolePage.hasNextPage()) {
                                rolePage = await rolePage.getNextPage();
                                userRoles.push(...(rolePage.data || []));
                            }
                            const role = mapAuth0RoleToBetterAuthRole(userRoles.map(r => r.name));
                            await ctx.adapter.create({
                                model: "member",
                                data: {
                                    id: `${org.id}|${member.user_id}`,
                                    organizationId: org.id,
                                    userId: member.user_id,
                                    role: role,
                                    createdAt: new Date()
                                },
                                forceAllowId: true
                            });

                            console.log(`成功迁移组织 ${org.display_name || org.id} 的成员 ${member.user_id}`);
                        } catch (error) {
                            console.error(`迁移组织 ${org.display_name || org.id} 的成员 ${member.user_id} 失败：`, error);
                        }
                    }

                    console.log(`成功迁移组织：${org.display_name || org.id}`);
                } catch (error) {
                    console.error(`迁移组织 ${org.display_name || org.id} 失败：`, error);
                }
            }
            console.log('组织迁移完成');
        } catch (error) {
            console.error('迁移组织失败：', error);
        }
    }

    async function migrateFromAuth0() {
        try {
            const ctx = await auth.$context;
            const isAdminEnabled = ctx.options?.plugins?.find(plugin => plugin.id === "admin");
            const isUsernameEnabled = ctx.options?.plugins?.find(plugin => plugin.id === "username");
            const isOrganizationEnabled = ctx.options?.plugins?.find(plugin => plugin.id === "organization");
            const auth0Users: any[] = [];

            try {
                let userPage = await auth0Client.users.list({
                    per_page: 100,
                    page: 0,
                    include_totals: true,
                });
                auth0Users.push(...userPage.data);
                while (userPage.hasNextPage()) {
                    userPage = await userPage.getNextPage();
                    auth0Users.push(...userPage.data);
                }
            } catch (error) {
                console.error('获取用户时出错：', error);
            }


            console.log(`Found ${auth0Users.length} users to migrate`);

            for (const auth0User of auth0Users) {
                try {
                    // 确定这是基于密码的用户还是 OAuth 用户
                    const isOAuthUser = auth0User.identities?.some((identity: any) => identity.provider !== 'auth0');
                    // 两种类型通用的基础用户数据
                    const baseUserData = {
                        id: auth0User.user_id,
                        email: auth0User.email,
                        emailVerified: auth0User.email_verified || false,
                        name: auth0User.name || auth0User.nickname,
                        image: auth0User.picture,
                        createdAt: safeDateConversion(auth0User.created_at),
                        updatedAt: safeDateConversion(auth0User.updated_at),
                        ...(isAdminEnabled ? {
                            banned: auth0User.blocked || false,
                            role: mapAuth0RoleToBetterAuthRole(auth0User.roles || []),
                        } : {}),

                        ...(isUsernameEnabled ? {
                            username: auth0User.username || auth0User.nickname,
                        } : {}),

                    };

                    const createdUser = await ctx.adapter.create({
                        model: "user",
                        data: {
                            ...baseUserData,
                        },
                        forceAllowId: true
                    });

                    if (!createdUser?.id) {
                        throw new Error('创建用户失败');
                    }


                    await migrateOAuthAccounts(auth0User, createdUser.id, ctx)
                    console.log(`成功迁移用户：${auth0User.email}`);
                } catch (error) {
                    console.error(`迁移用户 ${auth0User.email} 失败：`, error);
                }
            }
            if (isOrganizationEnabled) {
                await migrateOrganizations(ctx);
            }
            console.log('迁移成功完成');
        } catch (error) {
            console.error('迁移失败：', error);
            throw error;
        }
    }

    migrateFromAuth0()
        .then(() => {
            console.log('迁移完成');
            process.exit(0);
        })
        .catch((error) => {
            console.error('迁移失败：', error);
            process.exit(1);
        }); 
    ```

    请确保将 Auth0 环境变量替换为你自己的值：

    * `AUTH0_DOMAIN`
    * `AUTH0_CLIENT_ID`
    * `AUTH0_SECRET`
  </Step>

  <Step>
    ### 运行迁移 [#运行迁移]

    运行迁移脚本：

    ```sh
    bun run scripts/migrate-auth0.ts # or use your preferred runtime
    ```

    <Callout type="warning">
      重要注意事项：

      1. 请先在开发环境中测试迁移
      2. 监控迁移过程中的错误
      3. 在迁移完成之前验证 Better Auth 中的数据
      4. 在迁移完成之前保持 Auth0 安装和配置
      5. 默认情况下该脚本会处理 bcrypt 密码哈希。对于自定义密码哈希算法，你需要修改 `migratePassword` 函数
    </Callout>
  </Step>

  <Step>
    ### 更改密码哈希算法 [#更改密码哈希算法]

    默认情况下，Better Auth 使用 `scrypt` 算法来哈希密码。由于 Auth0 使用 `bcrypt`，你需要配置 Better Auth 使用 bcrypt 进行密码验证。

    首先安装 bcrypt：

    ```bash
    npm install bcrypt
    npm install -D @types/bcrypt
    ```

    然后更新你的认证配置：

    ```ts title="auth.ts"
    import { betterAuth } from "better-auth";
    import bcrypt from "bcrypt";

    export const auth = betterAuth({
       emailAndPassword: {
           password: {
               hash: async (password) => {
                   return await bcrypt.hash(password, 10);
               },
               verify: async ({ hash, password }) => {
                   return await bcrypt.compare(password, hash);
               }
           }
       }
    })
    ```
  </Step>

  <Step>
    ### 验证迁移 [#验证迁移]

    迁移完成后，请验证：

    1. 所有用户都已正确迁移
    2. 社交登录功能正常
    3. 基于密码的登录功能正常
    4. 双因素认证设置已保留（如已启用）
    5. 用户角色和权限已正确映射
  </Step>

  <Step>
    ### 更新组件 [#更新组件]

    现在数据已迁移，请更新你的组件以使用 Better Auth。以下是一个登录组件的示例：

    ```tsx title="components/auth/sign-in.tsx"
    import { authClient } from "@/lib/auth-client";

    export const SignIn = () => {
      const handleSignIn = async () => {
        const { data, error } = await authClient.signIn.email({
          email: "helloworld@gmail.com",
          password: "helloworld",
        });
        
        if (error) {
          console.error(error);
          return;
        }
        // 处理成功登录
      };

      return (
        <form onSubmit={handleSignIn}>
          <button type="submit">登录</button>
        </form>
      );
    };
    ```
  </Step>

  <Step>
    ### 更新中间件 [#更新中间件]

    将你的 Auth0 中间件替换为 Better Auth 的中间件：

    ```ts title="middleware.ts"
    import { NextRequest, NextResponse } from "next/server";
    import { getSessionCookie } from "better-auth/cookies";

    export async function middleware(request: NextRequest) {
      const sessionCookie = getSessionCookie(request);
      const { pathname } = request.nextUrl;

      if (sessionCookie && ["/login", "/signup"].includes(pathname)) {
        return NextResponse.redirect(new URL("/dashboard", request.url));
      }

      if (!sessionCookie && pathname.startsWith("/dashboard")) {
        return NextResponse.redirect(new URL("/login", request.url));
      }

      return NextResponse.next();
    }

    export const config = {
      matcher: ["/dashboard", "/login", "/signup"],
    };
    ```
  </Step>

  <Step>
    ### 移除 Auth0 依赖 [#移除-auth0-依赖]

    一旦你验证一切在 Better Auth 中正常工作，请移除 Auth0 依赖：

    ```bash
    npm remove @auth0/auth0-react @auth0/auth0-spa-js @auth0/nextjs-auth0
    ```
  </Step>
</Steps>

## 额外注意事项 [#额外注意事项]

### 密码迁移 [#密码迁移]

迁移脚本默认处理 bcrypt 密码哈希。如果你在 Auth0 中使用了自定义密码哈希算法，需要修改迁移脚本中的 `migratePassword` 函数以处理你的具体情况。

### 角色映射 [#角色映射]

脚本包含一个基本的角色映射函数（`mapAuth0RoleToBetterAuthRole`）。请根据你的 Auth0 角色和 Better Auth 角色需求自定义此函数。

### 速率限制 [#速率限制]

迁移脚本包含分页处理以应对大量用户。请根据你的需求和 Auth0 的速率限制，调整传递给 Auth0 `list()` 调用的 `per_page`（偏移分页）和 `take`（检查点分页）值。

## 总结 [#总结]

现在！你已成功从 Auth0 迁移到了 Better Auth。

Better Auth 提供更大的灵活性和更多功能——请务必探索[文档](/docs)，以充分发挥其潜力。

