You are currently viewing documentation for v1.8 (Beta)

System for Cross-domain Identity Management (SCIM)

Provision users and groups from a directory into Better Auth.

The SCIM plugin adds an inbound System for Cross-domain Identity Management (SCIM) 2.0 service to Better Auth. A directory can use this service to create, update, deactivate, and delete users and groups in your application.

SCIM defines identity resources in RFC 7643 and the HTTP protocol in RFC 7644. Better Auth supports the operations and attributes listed in the SCIM reference.

Each SCIM User links to a Better Auth User. Provisioning does not create a sign-in method or grant application access. Configure authentication separately, and use the optional identity and role callbacks when SCIM should affect your application.

Installation

Install the plugin

npm install @better-auth/scim

Enable database transactions

SCIM resource requests require a database adapter with native interactive transaction support. Enable transactions only when your database driver can run an interactive transaction callback.

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

Cloudflare D1 does not support the interactive transactions required by the SCIM plugin.

Configure a connection

Create a high-entropy bearer token in your secret manager. Configure the same value in Better Auth and your directory.

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,
            },
          ],
        },
      ],
    }),
  ],
});

The connection ID owns the resources provisioned with this credential. The optional provisioningDomainId identifies the workspace, tenant, project, or other application boundary that receives lifecycle and role updates. It defaults to the connection ID.

For runtime tenant onboarding and credential rotation, configure the optional managedConnections catalog. It persists plugin-owned connection metadata and token digests while your server authorizes the customer-administrator UI. Applications with an existing catalog can instead resolve the connection atomically in authentication.verifyBearerToken.

Expose the SCIM methods

Your Better Auth route must forward GET, POST, PUT, PATCH, and DELETE. For Next.js, export every method from the handler.

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);

Create the database tables

Run the migration, or generate the schema if your application manages migrations.

npx auth migrate

Configure your directory

Use the following values in your directory's SCIM connection settings:

SettingValue
Base URLYour Better Auth base URL followed by /scim/v2, such as https://app.example.com/api/auth/scim/v2
AuthenticationBearer token
TokenA static token from connections[].credentials or the one-time token returned by a managed create or rotate call

The directory must send the token as Authorization: Bearer <token>. User and Group requests use application/scim+json or application/json. Discovery endpoints are public so the directory can inspect the supported resources and operations before provisioning.

How provisioning maps to your application

The plugin keeps directory resources and application access separate:

  • A connection owns an isolated set of SCIM Users, Groups, and direct Group memberships.
  • A SCIM User links to one Better Auth User.
  • A provisioning domain identifies where your application applies lifecycle and access changes.
  • A SCIM Group grants no application permission unless you configure a role projection.

You can configure several connections. Their credentials remain isolated, while multiple connections may contribute to the same provisioning domain.

Provision users

When a directory creates a SCIM User, Better Auth creates a User by default, or links one when identity.resolveUser returns link. The directory can then update the profile, set active, or delete the SCIM resource. A SCIM User does not create an authentication account, so the user still needs SSO, a passkey, credentials, or another sign-in method.

By default, the SCIM source manages the Better Auth User's email and name. The plugin never links an incoming resource to an existing user by email.

Use identity.resolveUser when your application already has a stable mapping from the directory subject to a Better Auth User. Prefer a directory-owned externalId when the directory keeps it immutable. Do not use an unverified email address as the link key.

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" };
    },
  },
});

Choose the profile behavior when you link a user:

ResultBehavior
{ action: "create" }Creates a Better Auth User and lets the SCIM source manage its email and name.
{ action: "link", userId, profile: "manage" }Links an existing User and lets this source manage its email and name.
{ action: "link", userId, profile: "preserve" }Links an existing User without changing its Better Auth email or name.

Only one source can manage a Better Auth User's profile. Separate connections may link preserved sources to the same User, but one connection cannot create two SCIM resources for the same linked User.

Authenticate provisioned users with SSO

Use acquireActiveSCIMUserLink inside the SSO plugin's resolveUser callback when one SCIM connection controls who can sign in through a paired OIDC provider. The helper finds an active SCIM User by its exact connection ID and externalId, returning { scimUserId, userId } for the active link. It returns null for missing, inactive, deleted, tombstoned, orphaned, or decommissioned links.

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",
            };
      },
    }),
  ],
});

Configure the directory to send the OIDC provider's immutable, case-exact subject as the SCIM User's externalId. The example uses the validated OIDC sub from input.accountKey.accountId. The helper never falls back to userName, email, another connection, or a deleted-resource tombstone.

Call the helper with the context supplied by resolveUser; its database is the same native transaction adapter used for account linking and session creation. The helper fences the link against concurrent subject changes, source deactivation or deletion, and connection decommissioning. A direct helper caller can retry its entire transaction after a conflict.

During SSO, a lifecycle conflict aborts the current authentication attempt and is returned as SSO_USER_RESOLUTION_FAILED; no Account or Session is created. SSO does not retry the callback automatically, and the original OIDC callback cannot be replayed. The user or client must start a fresh SSO authentication attempt, which re-evaluates the current SCIM state.

Handle activation and deletion

Use identity.reconcileUser to apply the combined SCIM lifecycle state to your application. The callback receives every source linked to the Better Auth User and an active value that is true while at least one source remains active.

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

The callback runs in the same database transaction as the SCIM change. Make it idempotent and use the supplied database transaction for writes. Throwing from the callback rejects the request and rolls back the SCIM change.

When the final active source is deactivated or deleted, Better Auth deletes the User's sessions. Reactivating a source updates the lifecycle state but does not create a session.

Deleting sessions does not prevent a user from signing in again. Persist the disabled state in identity.reconcileUser, then enforce it in your authentication or authorization policy when deactivation must block future sign-in.

Deleting a SCIM resource preserves its Better Auth User. If the resource has an externalId, recreating the same externalId through the same connection links the new SCIM resource to that User. Without an externalId, the next create follows your resolver or creates another Better Auth User.

Next steps