System for Cross-domain Identity Management (SCIM)
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.
Installation
Install the plugin
npm install @better-auth/scimAdd Plugin to the server
import { betterAuth } from "better-auth"
import { scim } from "@better-auth/scim";
const auth = betterAuth({
plugins: [
scim()
]
})Enable HTTP methods
SCIM requires the POST, 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:
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET, PUT, PATCH, DELETE } = toNextJsHandler(auth); import { auth } from "~/lib/auth";
import { toSolidStartHandler } from "better-auth/solid-start";
export const { GET, POST, PUT, PATCH, DELETE } = toSolidStartHandler(auth); Migrate the database
Run the migration or generate the schema to add the necessary fields and tables to the database.
npx @better-auth/cli migratenpx @better-auth/cli generateSee the Schema section to add the fields manually.
Usage
Upon registration, this plugin will expose compliant SCIM 2.0 server. The following subset of the specification is currently supported:
SCIM endpoints
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.
Returns the provisioned SCIM user details. See https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.1
const data = await auth.api.listSCIMUsers({ query: { filter: 'userName eq "user-a"', }, // This endpoint requires a bearer authentication token. headers: { authorization: 'Bearer <token>' },});| Prop | Description | Type |
|---|---|---|
filter? | SCIM compliant filter expression | string |
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.
Returns the provisioned SCIM user details. See https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.1
const data = await auth.api.getSCIMUser({ params: { userId: "user id", // required }, // This endpoint requires a bearer authentication token. headers: { authorization: 'Bearer <token>' },});| Prop | Description | Type |
|---|---|---|
userId | Unique user identifier | string |
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.
Provision a new user via SCIM. See https://datatracker.ietf.org/doc/html/rfc7644#section-3.3
const data = await auth.api.createSCIMUser({ body: { externalId: "third party id", name: { formatted: "Daniel Perez", givenName: "Daniel", familyName: "Perez", }, emails: [{ value: "daniel@email.com", primary: true }], }, // This endpoint requires a bearer authentication token. headers: { authorization: 'Bearer <token>' },});| Prop | Description | Type |
|---|---|---|
externalId? | Unique external (third party) identifier | string |
name? | User name details | Object |
name.formatted? | Formatted name (takes priority over given and family name) | string |
name.givenName? | Given name | string |
name.familyName? | Family name | string |
emails? | List of emails associated to the user, only a single email can be primary | Array<{ value: string, primary?: boolean }> |
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.
Updates an existing user via SCIM. See https://datatracker.ietf.org/doc/html/rfc7644#section-3.3
const data = await auth.api.updateSCIMUser({ body: { externalId: "third party id", name: { formatted: "Daniel Perez", givenName: "Daniel", familyName: "Perez", }, emails: [{ value: "daniel@email.com", primary: true }], }, // This endpoint requires a bearer authentication token. headers: { authorization: 'Bearer <token>' },});| Prop | Description | Type |
|---|---|---|
externalId? | Unique external (third party) identifier | string |
name? | User name details | Object |
name.formatted? | Formatted name (takes priority over given and family name) | string |
name.givenName? | Given name | string |
name.familyName? | Family name | string |
emails? | List of emails associated to the user, only a single email can be primary | Array<{ value: string, primary?: boolean }> |
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.
Partially updates a user resource. See https://datatracker.ietf.org/doc/html/rfc7644#section-3.5.2
const data = await auth.api.patchSCIMUser({ body: { schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], // required Operations: [{ op: "replace", path: "/userName", value: "any value" }], // required }, // This endpoint requires a bearer authentication token. headers: { authorization: 'Bearer <token>' },});| Prop | Description | Type |
|---|---|---|
schemas | Mandatory schema declaration | string[] |
Operations | List of JSON patch operations | Array<{ op: "replace" | "add" | "remove", path: string, value: any }> |
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.
Deletes an existing user resource. See https://datatracker.ietf.org/doc/html/rfc7644#section-3.6
const data = await auth.api.deleteSCIMUser({ params: { userId, // required }, // This endpoint requires a bearer authentication token. headers: { authorization: 'Bearer <token>' },});| Prop | Description | Type |
|---|---|---|
userId | string |
Get service provider config
Get SCIM metadata describing supported features of this server.
Standard SCIM metadata endpoint used by identity providers. See https://datatracker.ietf.org/doc/html/rfc7644#section-4
const data = await auth.api.getSCIMServiceProviderConfig();Get SCIM schemas
Get the list of supported SCIM schemas.
Standard SCIM metadata endpoint used by identity providers to acquire information about supported schemas. See https://datatracker.ietf.org/doc/html/rfc7644#section-4
const data = await auth.api.getSCIMSchemas();Get SCIM schema
Get the details of a supported SCIM schema.
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
const data = await auth.api.getSCIMSchema();Get SCIM resource types
Get the list of supported SCIM types.
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
const data = await auth.api.getSCIMResourceTypes();Get SCIM resource type
Get the details of a supported SCIM resource type.
Standard SCIM metadata endpoint used by identity providers to get a server supported type. See https://datatracker.ietf.org/doc/html/rfc7644#section-4
const data = await auth.api.getSCIMResourceType();Generally, this server is meant to be consumed by a third-party (your identity provider), which will require:
- SCIM base url: This should be the fully qualified URL to be SCIM server (e.g
http://your-app.com/api/auth/scim/v2) - SCIM bearer token: See 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:
const { data, error } = await authClient.scim.generateToken({ providerId: "acme-corp", // required organizationId: "the-org",});| Prop | Description | Type |
|---|---|---|
providerId | The provider id | string |
organizationId? | Optional organization id. When specified, the organizations plugin must also be enabled | string |
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 oneuser.name: Derived fromname(name.formattedorname.givenName+name.familyName) and fallbacks to the user primary emailaccount.providerId: Provider associated to theSCIMtokenaccount.accountId: Defaults toexternalIdand fallbacks touserNamemember.organizationId: Organization associated to the provider
Schema
The plugin requires additional fields in the scimProvider table to store the provider's configuration.
| Field Name | Type | Key | Description |
|---|---|---|---|
| id | string | A database identifier | |
| providerId | string | - | The provider ID. Used to identify a provider and to generate a redirect URL. |
| scimToken | string | - | The SCIM bearer token. Used by your identity provider to authenticate against your server |
| organizationId | string | - | The organization Id. If provider is linked to an organization. |
Options
Server
storeSCIMToken: The method to store the SCIM token in your database, whetherencrypted,hashedorplaintext. Default isplaintext.
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);
},
}
})Hooks
The following hooks allow to intercept the lifecycle of the SCIM token generation:
scim({
beforeSCIMTokenGenerated: async ({ user, member, scimToken }) => {
// Callback called before the scim token is persisted
// can be useful to intercept the generation
if (member?.role !== "admin") {
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);
},
})Note: All hooks support error handling. Throwing an error in a before hook will prevent the operation from proceeding.