跨域身份管理系统(SCIM)

从目录中向 Better Auth 配置用户和组。

SCIM 插件为 Better Auth 添加了一个入站跨域身份管理系统(SCIM)2.0 服务。目录可以使用此服务在你的应用程序中创建、更新、停用和删除用户及组。

SCIM 在 RFC 7643 中定义身份资源,并在 RFC 7644 中定义 HTTP 协议。Better Auth 支持 SCIM 参考中列出的操作和属性。

每个 SCIM 用户都链接到一个 Better Auth 用户。配置不会创建登录方式,也不会授予应用程序访问权限。请单独配置身份验证;当 SCIM 应影响你的应用程序时,可以使用可选的身份和角色回调。

安装

安装插件

npm install @better-auth/scim

启用数据库事务

SCIM 资源请求需要使用支持原生交互式事务的数据库适配器。仅当你的数据库驱动程序可以运行交互式事务回调时,才启用事务。

auth.ts
const auth = betterAuth({
  database: kyselyAdapter(db, {
    type: "sqlite",
    transaction: true,
  }),
});

Cloudflare D1 不支持 SCIM 插件所需的交互式事务。

配置连接

在你的密钥管理器中创建一个高熵 bearer token。在 Better Auth 和你的目录中配置相同的值。

auth.ts
import { scim } from "@better-auth/scim";
import { betterAuth } from "better-auth";

const workforceToken = process.env.SCIM_WORKFORCE_TOKEN;

if (!workforceToken) {
  throw new Error("SCIM_WORKFORCE_TOKEN is required");
}

export const auth = betterAuth({
  baseURL: "https://app.example.com/api/auth",
  plugins: [
    scim({
      connections: [
        {
          id: "workforce-acme",
          provisioningDomainId: "workspace-acme",
          credentials: [
            {
              type: "bearer",
              id: "workforce-primary",
              token: workforceToken,
            },
          ],
        },
      ],
    }),
  ],
});

连接 ID 负责管理使用此凭据配置的资源。可选的 provisioningDomainId 用于标识接收生命周期和角色更新的工作区、租户、项目或其他应用边界。默认值为连接 ID。

对于运行时租户接入和凭据轮换,请配置可选的managedConnections 目录。它会持久化由插件拥有的连接元数据和令牌摘要,同时由你的服务器为客户管理员 UI 执行授权。已有目录的应用程序也可以在 authentication.verifyBearerToken 中原子地解析连接。

暴露 SCIM 方法

你的 Better Auth 路由必须转发 GET、POST、PUT、PATCH 和 DELETE。对于 Next.js,请从处理程序中导出每个方法。

app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";

export const { GET, POST, PUT, PATCH, DELETE } = toNextJsHandler(auth);

创建数据库表

运行迁移;如果你的应用程序自行管理迁移,则生成架构。

npx auth migrate

配置你的目录

在目录的 SCIM 连接设置中使用以下值:

设置值
基础 URL你的 Better Auth 基础 URL 后接 /scim/v2,例如 https://app.example.com/api/auth/scim/v2
身份验证Bearer token
令牌来自 connections[].credentials 的静态令牌,或 managed create 或 rotate 调用返回的一次性令牌

目录必须将令牌作为 Authorization: Bearer <token> 发送。用户和组请求使用 application/scim+json 或 application/json。发现端点是公开的,因此目录可以在配置之前检查支持的资源和操作。

配置如何映射到你的应用程序

该插件将目录资源与应用程序访问权限分开管理:

  • 一个连接负责管理一组隔离的 SCIM 用户、组和直接组成员关系。
  • 一个 SCIM 用户链接到一个 Better Auth 用户。
  • 一个配置域用于标识应用程序应用生命周期和访问权限变更的位置。
  • 除非你配置角色投影,否则 SCIM 组不会授予任何应用程序权限。

你可以配置多个连接。它们的凭据彼此隔离,但多个连接可以共同作用于同一个配置域。

配置用户

当目录创建 SCIM 用户时,Better Auth 默认会创建一个用户;如果 identity.resolveUser 返回 link,则会链接现有用户。之后,目录可以更新配置文件、设置 active 或删除 SCIM 资源。SCIM 用户不会创建身份验证账户,因此用户仍然需要 SSO、passkey、凭据或其他登录方式。

默认情况下,SCIM 源管理 Better Auth 用户的电子邮件和姓名。插件绝不会通过电子邮件将传入资源链接到现有用户。

链接现有 Better Auth 用户

当你的应用程序已经具有从目录主体到 Better Auth 用户的稳定映射时,请使用 identity.resolveUser。如果目录会保持 externalId 不变,优先使用由目录管理的 externalId。不要使用未经验证的电子邮件地址作为链接键。

auth.ts
scim({
  connections,
  identity: {
    async resolveUser(input, { database }) {
      const userId = input.resource.externalId
        ? await findUserIdByDirectorySubject(
            database,
            input.connectionId,
            input.resource.externalId,
          )
        : undefined;

      return userId
        ? { action: "link", userId, profile: "preserve" }
        : { action: "create" };
    },
  },
});

链接用户时选择配置文件行为:

结果行为
{ action: "create" }创建 Better Auth 用户,并允许 SCIM 源管理其电子邮件和姓名
{ action: "link", userId, profile: "manage" }链接现有用户,并允许此源管理其电子邮件和姓名
{ action: "link", userId, profile: "preserve" }链接现有用户,但不更改其 Better Auth 电子邮件或姓名

只有一个源可以管理 Better Auth 用户的配置文件。不同的连接可以将保留配置文件的源链接到同一个用户,但一个连接不能为同一个已链接用户创建两个 SCIM 资源。

使用 SSO 对已配置的用户进行身份验证

当一个 SCIM 连接控制谁可以通过配对的 OIDC provider 登录时,在 SSO 插件的 resolveUser 回调中使用 acquireActiveSCIMUserLink。该辅助函数通过精确的连接 ID 和 externalId 查找活跃的 SCIM 用户,并为活跃链接返回 { scimUserId, userId }。对于缺失、非活跃、已删除、已标记删除、孤立或已停用的链接,它会返回 null。

auth.ts
import { acquireActiveSCIMUserLink, scim } from "@better-auth/scim";
import { sso } from "@better-auth/sso";
import { betterAuth } from "better-auth";

const workforceProviderId = "acme-workforce-oidc";
const workforceConnectionId = "workforce-acme";

export const auth = betterAuth({
  plugins: [
    scim({ connections }),
    sso({
      defaultSSO: [
        {
          providerId: workforceProviderId,
          domain: "acme.example",
          oidcConfig: {
            issuer: "https://idp.acme.example",
            clientId: "acme-workforce-client-id",
            clientSecret: "acme-workforce-client-secret",
            pkce: true,
            discoveryEndpoint:
              "https://idp.acme.example/.well-known/openid-configuration",
          },
        },
      ],
      async resolveUser(input, context) {
        if (input.providerId !== workforceProviderId) {
          return { action: "continue" };
        }

        const link = await acquireActiveSCIMUserLink(
          {
            connectionId: workforceConnectionId,
            externalId: input.accountKey.accountId,
          },
          context,
        );

        return link
          ? {
              action: "link",
              userId: link.userId,
              profile: "preserve",
            }
          : {
              action: "reject",
              code: "SCIM_USER_NOT_ACTIVE",
            };
      },
    }),
  ],
});

配置目录,将 OIDC provider 的不可变且大小写精确的主体作为 SCIM 用户的 externalId 发送。示例使用了经过验证的 OIDC sub,其值来自 input.accountKey.accountId。该辅助函数绝不会回退到 userName、电子邮件、其他连接或已删除资源的 tombstone。

使用 resolveUser 提供的 context 调用该辅助函数;其 database 与用于账户链接和会话创建的原生事务适配器相同。该辅助函数会防止链接受到并发主体变更、源停用或删除以及连接停用的影响。直接调用辅助函数的调用方可以在冲突后重试整个事务。

在 SSO 期间,生命周期冲突会中止当前身份验证尝试,并作为 SSO_USER_RESOLUTION_FAILED 返回;不会创建 Account 或 Session。SSO 不会自动重试回调,原始 OIDC 回调也无法重放。用户或客户端必须发起全新的 SSO 身份验证尝试,该尝试会重新评估当前 SCIM 状态。

处理激活和删除

使用 identity.reconcileUser 将组合后的 SCIM 生命周期状态应用到你的应用程序。该回调会接收与 Better Auth 用户关联的每个源,以及一个 active 值;只要至少有一个源仍处于活跃状态,该值就是 true。

auth.ts
scim({
  connections,
  identity: {
    async reconcileUser(state, { database }) {
      await setDirectoryAccessState(database, state.userId, state.active);
    },
  },
});

该回调与 SCIM 变更在同一个数据库事务中运行。请确保它具有幂等性,并使用提供的 database 事务进行写入。从回调中抛出错误会拒绝请求并回滚 SCIM 变更。

当最后一个活跃源被停用或删除时,Better Auth 会删除用户的会话。重新激活源会更新生命周期状态,但不会创建会话。

删除会话并不能阻止用户再次登录。请将禁用状态持久化在 identity.reconcileUser 中,然后在身份验证或授权策略中执行该状态,以便在停用时阻止未来的登录。

目录可以在停用或删除用户后重新创建该用户。例如,Okta 在通过 userName 查找不到匹配项时会再次创建用户。若创建请求通过同一连接携带相同的 externalId,Better Auth 会保留已有的 Better Auth User:

  • 如果之前的 SCIM User 已停用,创建操作会重新配置该用户。资源保留其 SCIM ID,采用提交的资料和 active 值,并返回 201。
  • 如果之前的 SCIM User 已删除,创建操作会将新的 SCIM 资源关联到同一个 Better Auth User。

这两种情况都不会调用 identity.resolveUser,因为 externalId 已足以标识用户。若创建操作复用仍处于活动状态的 SCIM User 的 externalId,或复用另一个 SCIM User 的 userName,仍会返回 409。Better Auth 不会通过 userName 或电子邮件重新配置用户。未提供 externalId 时,后续创建操作会遵循你的解析器,或创建另一个 Better Auth User。

后续步骤