docs/content/docs/plugins/scim.mdx
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>```package-install
@better-auth/scim
```
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { scim } from "@better-auth/scim"; // [!code highlight]
const auth = betterAuth({
plugins: [
scim() // [!code highlight]
]
})
```
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>
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.
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:
http://your-app.com/api/auth/scim/v2)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:
This eliminates the back-and-forth typically required when setting up SCIM, reducing onboarding time from days to minutes.
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.
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:
adminorganization.creatorRole or ownerThe same role requirement is also used by the SCIM management endpoints for organization-scoped connections:
GET /scim/list-provider-connectionsGET /scim/get-provider-connectionPOST /scim/delete-provider-connectionconst 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.
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:
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
}
]
})
]
});
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 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.
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:
userIdrequiredRoleOnce enabled, make sure you migrate the database schema (again).
<Tabs items={["migrate", "generate"]}>
<Tab value="migrate">
bash npx @better-auth/cli migrate
</Tab>
See the Schema section to add the fields manually.
You can manage SCIM provider connections from your application using the following endpoints:
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.
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 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>The following subset of the specification is currently supported:
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 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>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>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>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>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 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 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 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 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 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>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 oneuser.name: Derived from name (name.formatted or name.givenName + name.familyName) and fallbacks to the user primary emailaccount.providerId: Provider associated to the SCIM tokenaccount.accountId: Defaults to externalId and fallbacks to userNamemember.organizationId: Organization associated to the providerThe 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} />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} />requiredRole: string[] — Minimum organization role(s) allowed to generate organization-scoped tokens and manage organization-scoped connections. Defaults to ["admin", organization.creatorRole ?? "owner"].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 }.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
scim({
storeSCIMToken: {
encrypt: async (scimToken) => {
return myCustomEncryptor(scimToken);
},
decrypt: async (scimToken) => {
return myCustomDecryptor(scimToken);
},
}
})
Custom hasher
scim({
storeSCIMToken: {
hash: async (scimToken) => {
return myCustomHasher(scimToken);
},
}
})
The following hooks allow to intercept the lifecycle of the SCIM token generation:
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);
},
})