Back to Better Auth

System for Cross-domain Identity Management (SCIM)

docs/content/docs/plugins/scim.mdx

1.6.1120.5 KB
Original Source

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

<Callout> Need a self-service SCIM setup where your customers can configure their own identity provider sync? [Contact us for enterprise](/enterprise). </Callout>

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

Self-Service Directory Sync

If you're using Better Auth Infrastructure, you get self-service directory sync in the dashboard. Organization admins can create and manage SCIM directory connections and rotate bearer tokens without calling the SCIM APIs directly.

The dashboard is available at:

https://dash.better-auth.com/[project]/organization/[orgId]/enterprise

From the dashboard you can:

  • Create and remove directory connections scoped to an organization
  • Regenerate SCIM bearer tokens when your identity provider requires rotation

This eliminates the back-and-forth typically required when setting up SCIM, reducing onboarding time from days to minutes.

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 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
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
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 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: "[email protected]", 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: "[email protected]", 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
scim({
    requiredRole: ["owner"],
})
  • providerOwnership: { enabled: boolean } — When enabled, links each personal provider connection to the user who generated its token. See Connection ownership for details. Default is { enabled: false }.
ts
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
scim({
    storeSCIMToken: { 
        encrypt: async (scimToken) => {
            return myCustomEncryptor(scimToken);
        },
        decrypt: async (scimToken) => {
            return myCustomDecryptor(scimToken);
        },
    }
})

Custom hasher

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>