---
title: System for Cross-domain Identity Management (SCIM)
description: Integrate SCIM with your application.
---

System for Cross-domain Identity Management ([SCIM](https://simplecloud.info/#Specification)) makes managing identities in multi-domain scenarios easier to support via a standardized protocol.
This plugin exposes a [SCIM](https://simplecloud.info/#Specification) server that allows third party identity providers to sync identities to your service.

## Installation

<Steps>
  <Step>
    ### Install the plugin

    ```package-install
    @better-auth/scim
    ```
  </Step>

  <Step>
    ### Add Plugin to the server

    ```ts title="auth.ts"
    import { betterAuth } from "better-auth"
    import { scim } from "@better-auth/scim"; // [!code highlight]

    const auth = betterAuth({
        plugins: [
            scim() // [!code highlight]
        ]
    })
    ```
  </Step>

  <Step>
    ### Enable HTTP methods

    SCIM requires the `POST`, `GET`, `PUT`, `PATCH` and `DELETE` HTTP methods to be supported by your server.
    For most frameworks, this will work out of the box, but some frameworks may require additional configuration:

    <Tabs items={["Next.js", "Solid Start"]}>
      <Tab value="Next.js">
        ```ts title="api/auth/[...all]/route.ts"
        import { auth } from "@/lib/auth";
        import { toNextJsHandler } from "better-auth/next-js";

        export const { POST, GET, PUT, PATCH, DELETE } = toNextJsHandler(auth); // [!code highlight]
        ```
      </Tab>

      <Tab value="Solid Start">
        ```ts title="routes/api/auth/*auth.ts"
        import { auth } from "~/lib/auth";
        import { toSolidStartHandler } from "better-auth/solid-start";

        export const { GET, POST, PUT, PATCH, DELETE } = toSolidStartHandler(auth); // [!code highlight]
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step>
    ### Migrate the database

    Run the migration or generate the schema to add the necessary fields and tables to the database.

    <Tabs items={["migrate", "generate"]}>
      <Tab value="migrate">
        ```package-install
        npx auth migrate
        ```
      </Tab>

      <Tab value="generate">
        ```package-install
        npx auth generate
        ```
      </Tab>
    </Tabs>

    See the [Schema](#schema) section to add the fields manually.
  </Step>
</Steps>

## Usage

Upon registration, this plugin will expose compliant [SCIM 2.0](https://simplecloud.info/#Specification) server. Generally, this server is meant to be consumed by a third-party (your identity provider), and will require a:

* **SCIM base URL**: This should be the fully qualified URL to the SCIM server (e.g `http://your-app.com/api/auth/scim/v2`)
* **SCIM bearer token**: See [generating a SCIM token](#generating-a-scim-token)

### Generating a SCIM token

Before your identity provider can start syncing information to your SCIM server,
you need to generate a SCIM token that your identity provider will use to authenticate against it.

A SCIM token is a simple bearer token that you can generate:

<APIMethod path="/scim/generate-token" method="POST" requireSession>
  ```ts
  type generateSCIMToken = {
    /**
    * The provider id
    */
    providerId: string = "acme-corp"
    /**
     * Optional organization id. When specified, the organizations plugin must also be enabled
    */
    organizationId?: string = "the-org"
  }
  ```
</APIMethod>

A `SCIM` token is always restricted to a provider, thus you are required to specify a `providerId`. This can be any provider your instance supports (e.g one of the built-in providers such as `credentials` or an external provider registered through an external plugin such as `@better-auth/sso`).
Additionally, when the `organization` plugin is registered, you can optionally restrict the token to an organization via the `organizationId`.

<Callout>
  **Important:** Personal SCIM connections can still be generated by any authenticated user. Organization-scoped connections are restricted by default to users with the `admin` role or the organization creator role (`organization.creatorRole`, which defaults to `owner`). If you need a different policy, configure [`requiredRole`](#options) and/or add stricter checks in [hooks](#hooks).
</Callout>

#### Organization-scoped authorization

When `organizationId` is provided, Better Auth requires the current user to be a member of that organization and to have at least one of the configured `requiredRole` values.

By default, `requiredRole` resolves to:

* `admin`
* `organization.creatorRole` or `owner`

The same role requirement is also used by the SCIM management endpoints for organization-scoped connections:

* `GET /scim/list-provider-connections`
* `GET /scim/get-provider-connection`
* `POST /scim/delete-provider-connection`

```ts
const approvedScimOperators = new Set(["some-admin-user-id"]);

scim({
    beforeSCIMTokenGenerated: async ({ user }) => {
        // Add stricter rules on top of the built-in organization role checks.
        if (!approvedScimOperators.has(user.id)) {
            throw new APIError("FORBIDDEN", { message: "User does not have enough permissions" });
        }
    },
})
```

See the [hooks](#hooks) documentation for more details about supported hooks.

#### Default SCIM token

We also provide a way for you to specify a `SCIM` token to use by default. This allows you to test a SCIM connection without setting up providers in the database:

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

const auth = betterAuth({
    plugins: [
        scim({
            defaultSCIM: [
                {
                    providerId: "default-scim", // ID of the existing provider you want to provision
                    scimToken: "some-scim-token", // SCIM plain token
                    organizationId: "the-org" // Optional organization id
                }
            ]
        })
    ]
});
```

<Callout type="info">
  **Important**: Please note that you must base64 encode your `scimToken` before you try to use as follows: `base64(scimToken:providerId[:organizationId])`.

  In our example above, you would need to encode the `some-scim-token:default-scim:the-org` text to base64, resulting in the following scimToken: `c29tZS1zY2ltLXRva2VuOmRlZmF1bHQtc2NpbTp0aGUtb3Jn`
</Callout>

### SCIM provider connection ownership

SCIM provider connection ownership applies to personal (non-organization) SCIM connections. It lets your application track who generated a connection and restricts later management operations for that connection to the same user.

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

const auth = betterAuth({
    plugins: [
        scim({ // [!code highlight]
            providerOwnership: { // [!code highlight]
                enabled: true // [!code highlight]
            } // [!code highlight]
        }) // [!code highlight]
    ]
});
```

When enabled:

* Personal connections store the creating user's `userId`
* Only the owner can regenerate, list, inspect, or delete those personal connections later
* Organization-scoped connections continue to use the organization role checks configured by `requiredRole`

Once enabled, make sure you migrate the database schema (again).

<Tabs items={["migrate", "generate"]}>
  <Tab value="migrate">
    ```bash
    npx @better-auth/cli migrate
    ```
  </Tab>

  <Tab value="generate">
    ```bash
    npx @better-auth/cli generate
    ```
  </Tab>
</Tabs>

See the [Schema](#if-you-have-provider-ownership-enabled-via-providerownershipenabled) section to add the fields manually.

### Managing SCIM provider connections

You can manage SCIM provider connections from your application using the following endpoints:

#### List SCIM provider connections

List existing connections the current user can manage. For organization-scoped connections, the user must have one of the configured `requiredRole` roles for that organization. For personal connections, access is based on ownership when `providerOwnership.enabled` is turned on.

<APIMethod path="/scim/list-provider-connections" method="GET" requireSession>
  ```ts
  type listSCIMProviderConnections = {
  }
  ```
</APIMethod>

#### Get SCIM provider connection details

Get a single connection by provider id. Access is allowed only if the user can manage that connection: either because they satisfy the configured organization role requirement, or because they own the personal connection.

<APIMethod path="/scim/get-provider-connection" method="GET" requireSession>
  ```ts
  type getSCIMProviderConnection = {
    /**
     * Unique provider identifier
     */
    providerId: string = "acme-corp"
  }
  ```
</APIMethod>

#### Delete SCIM provider connection

Delete an existing connection. This will immediately invalidate the connection's associated token.

<APIMethod path="/scim/delete-provider-connection" method="POST" requireSession>
  ```ts
  type deleteSCIMProviderConnection = {
    /**
     * Unique provider identifier
     */
    providerId: string = "acme-corp"
  }
  ```
</APIMethod>

### SCIM endpoints

The following subset of the specification is currently supported:

#### List users

Get a list of available users in the database. This is restricted to list only users associated to the same provider and organization than your SCIM token.

<APIMethod path="/scim/v2/Users" method="GET" requireBearerToken isExternalOnly note="Returns the provisioned SCIM user details. See https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.1">
  ```ts
  type listSCIMUsers = {
      /**
       * SCIM compliant filter expression
      */
      filter?: string = 'userName eq "user-a"'
  }
  ```
</APIMethod>

#### Get user

Get an user from the database. The user will be only returned if it belongs to the same provider and organization than the SCIM token.

<APIMethod path="/scim/v2/Users/:userId" method="GET" forceAsParam requireBearerToken isExternalOnly note="Returns the provisioned SCIM user details. See https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.1">
  ```ts
  type getSCIMUser = {
      /**
       * Unique user identifier
      */
      userId: string = "user id"
  }
  ```
</APIMethod>

#### Create new user

Provisions a new user to the database. The user will have an account associated to the same provider and will be member of the same org than the SCIM token.

<APIMethod path="/scim/v2/Users" method="POST" requireBearerToken isExternalOnly note="Provision a new user via SCIM. See https://datatracker.ietf.org/doc/html/rfc7644#section-3.3">
  ```ts
  type createSCIMUser = {
      /*
       * Unique external (third party) identifier
      */
      externalId?: string = "third party id"
      /**
       * User name details
      */
      name?: {
          /**
           * Formatted name (takes priority over given and family name)
          */
          formatted?: string = "Daniel Perez"
          /**
           * Given name
          */
          givenName?: string = "Daniel"
          /**
           * Family name
          */
          familyName?: string = "Perez"
      }
      /**
       * List of emails associated to the user, only a single email can be primary
      */
      emails?: Array<{ value: string, primary?: boolean }> = [{ value: "daniel@email.com", primary: true }]
  }
  ```
</APIMethod>

#### Update an existing user

Replaces an existing user details in the database. This operation can only update users that belong to the same provider and organization than the SCIM token.

<APIMethod path="/scim/v2/Users/:userId" method="PUT" requireBearerToken isExternalOnly note="Updates an existing user via SCIM. See https://datatracker.ietf.org/doc/html/rfc7644#section-3.3">
  ```ts
  type updateSCIMUser = {
      /*
       * Unique external (third party) identifier
      */
      externalId?: string = "third party id"
      /**
       * User name details
      */
      name?: {
          /**
           * Formatted name (takes priority over given and family name)
          */
          formatted?: string = "Daniel Perez"
          /**
           * Given name
          */
          givenName?: string = "Daniel"
          /**
           * Family name
          */
          familyName?: string = "Perez"
      }
      /**
       * List of emails associated to the user, only a single email can be primary
      */
      emails?: Array<{ value: string, primary?: boolean }> = [{ value: "daniel@email.com", primary: true }]
  }
  ```
</APIMethod>

#### Partial update an existing user

Allows to apply a partial update to the user details. This operation can only update users that belong to the same provider and organization than the SCIM token.

<APIMethod path="/scim/v2/Users/:userId" method="PATCH" requireBearerToken isExternalOnly note="Partially updates a user resource. See https://datatracker.ietf.org/doc/html/rfc7644#section-3.5.2">
  ```ts
  type patchSCIMUser = {
      /**
       * Mandatory schema declaration
      */
      schemas: string[] = ["urn:ietf:params:scim:api:messages:2.0:PatchOp"]
      /**
       * List of JSON patch operations
      */
      Operations: Array<{ op: "replace" | "add" | "remove", path?: string, value: any }> = [{ op: "replace", path: "/userName", value: "any value" }]
  }
  ```
</APIMethod>

#### Deletes a user resource

Completely deletes a user resource from the database. This operation can only delete users that belong to the same provider and organization than the SCIM token.

<APIMethod path="/scim/v2/Users/:userId" method="DELETE" forceAsParam requireBearerToken isExternalOnly note="Deletes an existing user resource. See https://datatracker.ietf.org/doc/html/rfc7644#section-3.6">
  ```ts
  type deleteSCIMUser = {
      userId: string
  }
  ```
</APIMethod>

#### Get service provider config

Get SCIM metadata describing supported features of this server.

<APIMethod path="/scim/v2/ServiceProviderConfig" method="GET" isExternalOnly note="Standard SCIM metadata endpoint used by identity providers. See https://datatracker.ietf.org/doc/html/rfc7644#section-4">
  ```ts
  type getSCIMServiceProviderConfig = {
  }
  ```
</APIMethod>

#### Get SCIM schemas

Get the list of supported SCIM schemas.

<APIMethod path="/scim/v2/Schemas" method="GET" isExternalOnly note="Standard SCIM metadata endpoint used by identity providers to acquire information about supported schemas. See https://datatracker.ietf.org/doc/html/rfc7644#section-4">
  ```ts
  type getSCIMSchemas = {
  }
  ```
</APIMethod>

#### Get SCIM schema

Get the details of a supported SCIM schema.

<APIMethod path="/scim/v2/Schemas/:schemaId" method="GET" isExternalOnly note="Standard SCIM metadata endpoint used by identity providers to acquire information about a given schema. See https://datatracker.ietf.org/doc/html/rfc7644#section-4">
  ```ts
  type getSCIMSchema = {
  }
  ```
</APIMethod>

#### Get SCIM resource types

Get the list of supported SCIM types.

<APIMethod path="/scim/v2/ResourceTypes" method="GET" isExternalOnly note="Standard SCIM metadata endpoint used by identity providers to get a list of server supported types. See https://datatracker.ietf.org/doc/html/rfc7644#section-4">
  ```ts
  type getSCIMResourceTypes = {
  }
  ```
</APIMethod>

#### Get SCIM resource type

Get the details of a supported SCIM resource type.

<APIMethod path="/scim/v2/ResourceTypes/:resourceTypeId" method="GET" isExternalOnly note="Standard SCIM metadata endpoint used by identity providers to get a server supported type. See https://datatracker.ietf.org/doc/html/rfc7644#section-4">
  ```ts
  type getSCIMResourceType = {
  }
  ```
</APIMethod>

#### SCIM attribute mapping

By default, the SCIM provisioning will automatically map the following fields:

* `user.email`: User primary email or the first available email if there is not a primary one
* `user.name`: Derived from `name` (`name.formatted` or `name.givenName` + `name.familyName`) and fallbacks to the user primary email
* `account.providerId`: Provider associated to the `SCIM` token
* `account.accountId`: Defaults to `externalId` and fallbacks to `userName`
* `member.organizationId`: Organization associated to the provider

## Schema

The plugin requires additional fields in the `scimProvider` table to store the provider's configuration.

export const scimProviderTableFields = [
	{
		name: "id",
		type: "string",
		description: "A database identifier",
		isPrimaryKey: true,
	},
	{
		name: "providerId",
		type: "string",
		description:
			"The provider ID. Used to identify a provider and to generate a redirect URL.",
		isUnique: true,
	},
	{
		name: "scimToken",
		type: "string",
		description:
			"The SCIM bearer token. Used by your identity provider to authenticate against your server",
		isUnique: true,
	},
	{
		name: "organizationId",
		type: "string",
		description:
			"The organization Id. If provider is linked to an organization.",
		isOptional: true,
	},
];

<DatabaseTable name="scimProvider" fields={scimProviderTableFields} />

### If you have provider ownership enabled via `providerOwnership.enabled`:

The `scimProvider` schema is extended as follows:

export const scimProviderOwnershipFields = [
	{
		name: "userId",
		type: "string",
		description:
			"The user id of the connection owner. Set automatically when generating a token via the API.",
		isOptional: true,
	},
];

<DatabaseTable name="scimProvider" fields={scimProviderOwnershipFields} />

## Options

### Server

* `requiredRole`: `string[]` — Minimum organization role(s) allowed to generate organization-scoped tokens and manage organization-scoped connections. Defaults to `["admin", organization.creatorRole ?? "owner"]`.

```ts title="Allow only owners to manage organization-scoped SCIM connections"
scim({
    requiredRole: ["owner"],
})
```

* `providerOwnership`: `{ enabled: boolean }` — When enabled, links each personal provider connection to the user who generated its token. See [Connection ownership](#scim-provider-connection-ownership) for details. Default is `{ enabled: false }`.

```ts title="Enable connection ownership (requires migration)"
scim({
    providerOwnership: { enabled: true },
})
```

* `defaultSCIM`: Default list of SCIM tokens for testing.
* `storeSCIMToken`: The method to store the SCIM token in your database, whether `encrypted`, `hashed` or `plain` text. Default is `plain` text.

Alternatively, you can pass a custom encryptor or hasher to store the SCIM token in your database.

**Custom encryptor**

```ts title="auth.ts"
scim({
    storeSCIMToken: { 
        encrypt: async (scimToken) => {
            return myCustomEncryptor(scimToken);
        },
        decrypt: async (scimToken) => {
            return myCustomDecryptor(scimToken);
        },
    }
})
```

**Custom hasher**

```ts title="auth.ts"
scim({
    storeSCIMToken: {
        hash: async (scimToken) => {
            return myCustomHasher(scimToken);
        },
    }
})
```

### Hooks

The following hooks allow to intercept the lifecycle of the `SCIM` token generation:

<Callout>
  **Note:** The built-in organization role check runs before these hooks. Use hooks to add stricter rules, not to bypass `requiredRole`.
</Callout>

```ts
const approvedScimOperators = new Set(["some-admin-user-id"]);

scim({
    beforeSCIMTokenGenerated: async ({ user, member, scimToken }) => {
        // `member` is null for personal connections.
        // Add any extra restrictions you need before the token is persisted.
        if (!approvedScimOperators.has(user.id)) {
            throw new APIError("FORBIDDEN", { message: "User does not have enough permissions" });
        }
    },
    afterSCIMTokenGenerated: async ({ user, member, scimToken, scimProvider }) => {
        // Callback called after the scim token has been persisted
        // can be useful to send a notification or otherwise share the token
        await shareSCIMTokenWithInterestedParty(scimToken);
    },
})
```

<Callout>
  **Note**: All hooks support error handling. Throwing an error in a before hook will prevent the operation from proceeding.
</Callout>
