System for Cross-domain Identity Management (SCIM)
Integrate SCIM with your application.
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, 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:
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 auth migratenpx auth generateSee the Schema section to add the fields manually.
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
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",});providerIdstringrequiredThe provider id
organizationIdstringOptional organization id. When specified, the organizations plugin must also be enabled
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.
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 and/or add stricter checks in hooks.
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:
adminorganization.creatorRoleorowner
The 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-connection
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:
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
}
]
})
]
});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
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.
import { betterAuth } from "better-auth";
import { scim } from "@better-auth/scim";
const auth = betterAuth({
plugins: [
scim({
providerOwnership: {
enabled: true
}
})
]
});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).
npx @better-auth/cli migratenpx @better-auth/cli generateSee 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.
const { data, error } = await authClient.scim.listProviderConnections();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.
const { data, error } = await authClient.scim.getProviderConnection({ query: { providerId: "acme-corp", // required },});providerIdstringrequiredUnique provider identifier
Delete SCIM provider connection
Delete an existing connection. This will immediately invalidate the connection's associated token.
const { data, error } = await authClient.scim.deleteProviderConnection({ providerId: "acme-corp", // required});providerIdstringrequiredUnique provider identifier
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.
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>' },});filterstringSCIM compliant filter expression
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>' },});userIdstringrequiredUnique user identifier
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>' },});externalIdstringUnique external (third party) identifier
nameObjectUser name details
formattedstringFormatted name (takes priority over given and family name)
givenNamestringGiven name
familyNamestringFamily name
emailsArray<{ value: string, primary?: boolean }>List of emails associated to the user, only a single email can be primary
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>' },});externalIdstringUnique external (third party) identifier
nameObjectUser name details
formattedstringFormatted name (takes priority over given and family name)
givenNamestringGiven name
familyNamestringFamily name
emailsArray<{ value: string, primary?: boolean }>List of emails associated to the user, only a single email can be primary
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>' },});schemasstring[]requiredMandatory schema declaration
OperationsArray<{ op: "replace" | "add" | "remove", path: string, value: any }>requiredList of JSON patch operations
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>' },});userIdstringrequiredGet 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();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.
If you have provider ownership enabled via providerOwnership.enabled:
The scimProvider schema is extended as follows:
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"].
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, 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:
Note: The built-in organization role check runs before these hooks. Use hooks to add stricter rules, not to bypass requiredRole.
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);
},
})Note: All hooks support error handling. Throwing an error in a before hook will prevent the operation from proceeding.