scim

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 14, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

README

scim

A SCIM 2.0 service provider (RFC 7643, RFC 7644), better-auth's scim plugin: an identity provider such as Okta, Microsoft Entra ID, Google Workspace or JumpCloud creates, updates, deactivates and deletes users and groups by calling your app, so a leaver loses access the moment IT disables them.

It is its own module, github.com/ubgo/auth/plugin/scim, so an app without enterprise customers never compiles it. It needs the email plugin: a provisioned user's primary email becomes an email identity.

sp := scim.New(
	scim.Connections(scim.Connection{ID: "acme", Credentials: []scim.Credential{{ID: "2026-10", Token: os.Getenv("SCIM_ACME_TOKEN")}}}),
	scim.WithGroupMapper(myOrgSync), // optional
)
a, err := auth.New(auth.WithDB(db, dialect), auth.WithBaseURL("https://app.example.com"),
	auth.WithPlugins(email.New(...), password.New(), sp))

mux.Handle("/scim/v2/", http.StripPrefix("/scim/v2", sp.Handler()))

Give the IdP the base URL https://app.example.com/scim/v2 and the token.

Endpoints

Route Scope What it does
POST /Users scim.users.write Creates a SCIM user and a new library user (or links one, ResolveUser); 201
GET /Users scim.users.read ListResponse with filter, startIndex, count, attributes, excludedAttributes
GET /Users/{id} scim.users.read One user; another connection's id is 404
PUT /Users/{id} scim.users.write Replaces the user
PATCH /Users/{id} scim.users.write Ordered, atomic operations
DELETE /Users/{id} scim.users.write Deletes the SCIM user and its memberships; 204
POST /Groups, GET /Groups, GET, PUT, PATCH, DELETE /Groups/{id} scim.groups.* The same for groups
GET /ServiceProviderConfig, /Schemas, /Schemas/{id}, /ResourceTypes, /ResourceTypes/{id} public Discovery: PATCH and filters yes; bulk, sort, ETags and password change no

Every request passes the core's rate rules first, as the operation scim.EndpointRequest (scim.request, self-served: never mounted by a transport, and absent from the OpenAPI document and the generated TypeScript client): by default 600 a minute per IP and 600 a minute per connection, answered with 429 and Retry-After; replace them with auth.WithRateRules(auth.RateRule{Match: scim.EndpointRequest, ...}). A refused token is counted by its address, so guessing is limited too.

Every write to a user is conditional on the revision it read: meta.version is W/"n", a PUT, PATCH or DELETE with If-Match naming an older revision answers 412, and a write that loses a race with another answers 409, rolling back everything it changed. ServiceProviderConfig advertises etag as supported.

Requests must be application/scim+json or application/json; every answer is application/scim+json, errors in SCIM's error schema with scimType (invalidFilter, uniqueness, mutability, invalidSyntax, invalidPath, noTarget, invalidValue, tooMany).

Authentication

Each Connection is one IdP tenant and scopes every resource it creates: a connection never sees another's users or groups. A connection holds one or more Credentials (bearer tokens), so a token is rotated by adding the new credential, switching the IdP, then removing the old one. A credential's Scopes default to all four (scim.users.read, scim.users.write, scim.groups.read, scim.groups.write); ExpiresAt refuses it after a date. Tokens are compared in constant time over their SHA-256. VerifyBearerToken(fn) authenticates tokens the app stores itself (a per-customer token in its own table), asked only after every static credential missed; it returns a Principal or scim.ErrUnauthorized. A Principal with a blank or over-long connection id, an unknown scope, the managed connections' prefix, or the id of a connection given to Connections is refused (401), so a verifier can never speak for a configured connection. A missing or wrong token answers 401 with WWW-Authenticate: Bearer realm="SCIM"; a missing scope 403.

Managed connections

ManagedConnections(opts...) lets the app create connections at run time, better-auth's managed mode, for a self-serve "connect your IdP" page: CreateManagedConnection(ctx, CreateManagedRequest{CreationRequestID, ProvisioningDomainID, ActorID, Scopes, ExpiresAt}) returns the connection, its first credential and the raw token, once (a CreationRequestID of 16 to 255 characters is never reused: scim.creation_request_conflict). ManagedConnections(ctx, domain) lists a provisioning domain's connections newest first, ManagedConnection(ctx, id, domain) reads one (another tenant's is not found), RotateCredential adds an overlapping credential (at most MaxActiveCredentials, default 5), RevokeCredential revokes one, and ConnectionEvents returns the latest 100 events (connection.created, credential.issued, credential.rotated, credential.revoked, connection.decommissioning, connection.decommissioned). Tokens start with scimt_, are stored as HMAC-SHA256 digests (CredentialHashSecret, or a key derived from auth.WithSecret), carry their credential id, and never fall through to VerifyBearerToken. last_used_at is written at most every LastUsedWriteInterval (5 minutes).

Every connection, static, verified or managed, gets a row on its first request with its ProvisioningDomainID (default: the connection id), which cannot change afterwards (409).

Decommissioning

DecommissionConnection(ctx, DecommissionRequest{ConnectionID, ProvisioningDomainID, ActorID, MaxBatches}) retires a connection for good: its tokens answer 401 "SCIM connection is decommissioned" at once (static tokens, and managed credentials that were active, which are marked decommissioned; a credential revoked before keeps answering "Invalid SCIM bearer token"), its SCIM users are made inactive in batches of 50, each batch in its own transaction, and each linked library user is reconciled, so a user with no other active source is deactivated and signed out. At the end the GroupMapper is told every group is deleted. The SCIM rows are kept. MaxBatches bounds one call, which then answers reconciling; the next call resumes at the stored cursor. Run it from one worker.

Users

Supported attributes: userName (required, unique per connection ignoring case), externalId (unique per connection), name, displayName, title, userType, preferredLanguage, locale, timezone, active, emails (up to 20), phoneNumbers, addresses, roles, entitlements (up to 10 each; one primary, distinct types), and the enterprise extension (employeeNumber, costCenter, organization, division, department, manager). A user is at most 65,535 bytes serialized. Entra's "True"/"False" strings for active and primary are accepted.

A new SCIM user gets a new library user named after displayName (or the formatted, or given and family name) with its primary email (the one marked primary, else the first work, else the first, else userName when it is an address) as an unverified email identity. The address must not be held by any identity already (409 uniqueness): the plugin never links by email, because an IdP can assert any address. ResolveUser(fn) links a new SCIM user to an existing library user instead, and a connection may hold one SCIM user per library user. The link's Resolution.Profile is better-auth's profile mode: ProfilePreserve (the default) leaves the linked user's name and email alone; ProfileManage makes the SCIM user that user's profile source, which writes the name and makes the IdP's address the primary email now (adopting the user's identity that already holds it, otherwise replacing the old primary email) and follows it on every PUT and PATCH. A library user has one profile source across every connection: a user the plugin created is managed by its creator, and a second ProfileManage link, or a tombstone revival while another source holds the profile, is 409 uniqueness. Deleting the source frees the profile for the next ProfileManage link (the hand-over). Another value is ErrBadProfile (500). TrustEmails() creates the address verified; use it only when the IdP verifies addresses in domains the customer owns. On PUT and PATCH the name follows, and a changed primary email replaces the identity with a new unverified one.

Deleting a SCIM user with an externalId leaves a tombstone; when the same connection creates a user with that externalId again, it gets the same library user (and its identity) back, reactivated, instead of a second account; if another source took the profile over meanwhile, the revival is 409.

active: false deactivates: when every SCIM user linked to a library user is inactive, the user's sessions end and the scim_deactivated_at column on auth_user refuses them until a connection sets active: true again: every later session lookup fails, and a new sign-in by any method is refused with scim.deactivated (403, ErrDeactivated) once the credential is proven, as admin refuses a banned user; a wrong credential still gets the ordinary answer, so the refusal tells a stranger nothing. Deleting the last SCIM user keeps the library user deactivated; DeleteUsers() deletes it instead. ReconcileUser(fn) is told the recomputed state (UserID, Active, Sources) inside the request's transaction, to mirror it into the app's own records. p.ActiveUser(ctx, connectionID, externalID) resolves the IdP's user id to the provisioned account while it is active, for an SSO sign-in that must never resolve by email (better-auth's acquireActiveSCIMUserLink).

PATCH

op is add, replace or remove in any case (replace when missing); operations apply in order to a copy and the result is validated before anything is written, so a failing operation changes nothing. Paths: an attribute, attr.sub, attr[type eq "work"], attr[type eq "work"].sub, attr[primary eq true]…, the core schema URN prefix, the enterprise URN and its attributes. A pathless add/replace takes an object of paths; inside the enterprise URN's object, manager.value names the sub-attribute as the path form does. A replace or add whose type eq filter matches nothing creates the entry (never primary), as Entra expects; other misses answer noTarget. id, schemas, meta and manager.displayName are read-only, and userName cannot be removed (mutability). members add skips values already present, remove with a value list removes those members, members[value eq "…"] removes one.

Filters, paging, projection

filter is one to ten attribute eq "value" terms joined by and: users by id, userName, externalId, emails.value, emails[type eq "work"].value; groups by id, displayName, externalId. userName, emails and displayName compare ignoring case. Anything else is invalidFilter. startIndex is 1-based; count defaults to and is capped at 100. attributes or excludedAttributes (not both) project every answer, id and schemas always kept; giving both is invalidValue before anything is written, so a refused create leaves no user behind for the retry to collide with.

Groups

displayName is required and unique per connection ignoring case, externalId unique; members are users of the same connection (type User; nested groups and other connections' users are refused), duplicates collapsed, at most 1,000. AcceptEntraLegacyGroupSchema() accepts Entra's legacy group schema marker once on POST /Groups. Groups map to nothing in the core: a GroupMapper receives SyncGroup with the group's full membership as library user ids after every change (including a member user's deletion), and DeleteGroup, inside the request's transaction, so an organization or role system follows the IdP. An error from it fails the request and the IdP retries. ProjectRoles(RoleProjection{Map, Exists, Reconcile}) is better-auth's role projection: Map turns a group into roles, Exists drops roles the app does not know, one grant row per member and role is kept in auth_scim_grant, and Reconcile receives each affected user's full grant list and active state after every change, in the same transaction. ReconcileProjection(ctx, domain) replays every group of a provisioning domain's active connections into the mapper, 50 per transaction, after a mapper fix.

Tables

auth_scim_user (scu), auth_scim_group (scg), auth_scim_member (scm), auth_scim_tombstone (sct), auth_scim_grant (scx), auth_scim_connection (scn), auth_scim_credential (scr) and auth_scim_event (sce), every row with its connection_id; unique keys are SHA-256 hashes of the compared forms so long values fit MySQL's key length. No foreign keys (ADR-0002): UserTables lists the user_id of auth_scim_user, auth_scim_member, auth_scim_tombstone and auth_scim_grant, so DeleteUser removes a user's SCIM rows. The plugin adds scim_deactivated_at to auth_user.

Not built

nothing of better-auth's SCIM plugin beyond what it lists as unsupported itself. The Entra request shapes (capitalised ops, string booleans, a bare manager id, member add and remove by value list, the legacy group marker) are replayed by TestSCIM_EntraValidatorSequence; Microsoft's validator is a hosted tool run against a deployed, publicly reachable endpoint from an Entra tenant, so it is outside the library's test suite. Every list is filtered in SQL for id, userName, externalId and displayName, but email filters and paging run over the connection's matching users in memory, which suits directories of thousands, not millions.

Documentation

Overview

Package scim is a SCIM 2.0 service provider (RFC 7643, RFC 7644), better-auth's scim plugin: an identity provider such as Okta, Microsoft Entra or JumpCloud creates, updates, deactivates and deletes users and groups by calling the app, so a leaver loses access when IT disables them.

The plugin serves /Users, /Groups and the discovery documents under the prefix the app mounts Handler at (conventionally /scim/v2), in SCIM's own media type and error schema. A caller is one connection (one IdP tenant) holding a bearer credential with scopes; every resource belongs to the connection that created it and is invisible to every other.

A SCIM user is linked to one library user: created for it by default (ResolveUser decides otherwise), never matched to an existing user by email, because an IdP can assert any address. When every SCIM user linked to a library user is inactive or deleted, that user's sessions end and every later session lookup refuses them (the scim_deactivated_at column), until a connection reactivates them.

Groups map to nothing in the core. A GroupMapper, if given, receives each group with its full membership as library user ids after every change, in the same transaction, so an organization or role system can follow the IdP without this package knowing its shape.

Index

Constants

View Source
const (
	SchemaUser           = "urn:ietf:params:scim:schemas:core:2.0:User"
	SchemaEnterpriseUser = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
	SchemaGroup          = "urn:ietf:params:scim:schemas:core:2.0:Group"
	SchemaListResponse   = "urn:ietf:params:scim:api:messages:2.0:ListResponse"
	SchemaPatchOp        = "urn:ietf:params:scim:api:messages:2.0:PatchOp"
	SchemaError          = "urn:ietf:params:scim:api:messages:2.0:Error"
	SchemaSPConfig       = "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"
	SchemaResourceType   = "urn:ietf:params:scim:schemas:core:2.0:ResourceType"
	SchemaSchema         = "urn:ietf:params:scim:schemas:core:2.0:Schema"
)

Schema URNs (RFC 7643, RFC 7644).

View Source
const (
	TableNameConnection = "auth_scim_connection"
	TableNameCredential = "auth_scim_credential"
	TableNameEvent      = "auth_scim_event"

	TableAliasConnection = "scn"
	TableAliasCredential = "scr"
	TableAliasEvent      = "sce"

	ColProvisioningDomain = "provisioning_domain"
	ColManaged            = "managed"
	ColStatus             = "status"
	ColCreationRequestID  = "creation_request_id"
	ColCreatedBy          = "created_by"
	ColDecomStartedAt     = "decommission_started_at"
	ColDecommissionedAt   = "decommissioned_at"
	ColDecomCursor        = "decommission_cursor"
	ColReconciledUsers    = "reconciled_users"
	ColBatches            = "batches"
	ColDigest             = "digest"
	ColScopes             = "scopes"
	ColExpiresAt          = "expires_at"
	ColLastUsedAt         = "last_used_at"
	ColRevokedAt          = "revoked_at"
	ColRevokedBy          = "revoked_by"
	ColSequence           = "sequence"
	ColEventType          = "event_type"
	ColActorID            = "actor_id"
	ColCredentialID       = "credential_id"
)

Connection state: every connection that ever authenticated has a row, static, verified and managed alike, so a connection can be decommissioned and its provisioning domain cannot change under it (better-auth's scimConnectionBinding). Managed connections also keep their credentials and an event log (scimManagedConnection, scimManagedCredential, scimManagedConnectionEvent).

View Source
const (
	// ManagedTokenPrefix starts every managed token; a token with it is
	// never handed to VerifyBearerToken.
	//ds:def id=scim-managedtokenprefix-8v3kq6wn owner=@khanakia stability=stable
	ManagedTokenPrefix = "scimt_"

	// DefaultMaxActiveCredentials bounds overlapping credentials per
	// managed connection.
	//ds:def id=scim-defaultmaxactivecredentials-4n9wq2xt owner=@khanakia stability=stable
	DefaultMaxActiveCredentials = 5

	// DefaultLastUsedWriteInterval spaces last_used_at writes, so a busy
	// IdP does not write the row on every request.
	DefaultLastUsedWriteInterval = 5 * time.Minute
)

Managed-connection limits, better-auth's.

View Source
const (
	TableNameGrant  = "auth_scim_grant"
	TableAliasGrant = "scx"
	ColRole         = "role"
)

Role projection, better-auth's projection.roles and projection.reconcileUser: the app maps each group to roles, the plugin keeps one grant row per member and role (scimProjectionGrant), and tells the app each affected user's full grant list after every change, inside the request's transaction. It sits beside GroupMapper: the mapper sees groups, the projection sees users and roles.

View Source
const (
	TableNameUser   = "auth_scim_user"
	TableNameGroup  = "auth_scim_group"
	TableNameMember = "auth_scim_member"

	TableAliasUser   = "scu"
	TableAliasGroup  = "scg"
	TableAliasMember = "scm"

	ColID           = "id"
	ColConnectionID = "connection_id"
	ColUserID       = "user_id"
	ColIdentityID   = "identity_id"
	ColUserName     = "user_name"
	// ColUserNameKey and the other *_key columns hold the SHA-256 of the
	// compared form (lower-cased where SCIM compares case-insensitively), so
	// a 512-character userName fits a unique index on MySQL.
	ColUserNameKey    = "user_name_key"
	ColExternalID     = "external_id"
	ColExternalIDKey  = "external_id_key"
	ColActive         = "active"
	ColPrimaryEmail   = "primary_email"
	ColResource       = "resource"
	ColDisplayName    = "display_name"
	ColDisplayNameKey = "display_name_key"
	ColGroupID        = "group_id"
	ColSCIMUserID     = "scim_user_id"
	ColCreatedAt      = "created_at"
	ColUpdatedAt      = "updated_at"
	// ColRevision counts a SCIM user's writes: every write is conditional on
	// the revision it read, so a stale write fails instead of overwriting a
	// newer one, and meta.version (W/"n") lets an IdP send If-Match.
	ColRevision = "revision"

	// ColDeactivatedAt is the column this plugin adds to auth_user: set while
	// every SCIM user linked to the user is inactive or deleted.
	ColDeactivatedAt = "scim_deactivated_at"
)

Table, alias and column names. Every table is scoped by connection_id.

View Source
const (
	// MaxPageSize is the most resources one list answers (SCIM_MAX_PAGE_SIZE).
	//ds:def id=scim-maxpagesize-6k2wq9mv owner=@khanakia stability=stable
	MaxPageSize = 100
	// MaxGroupMembers bounds one group (SCIM_MAX_GROUP_MEMBERS).
	//ds:def id=scim-maxgroupmembers-3n8xk5tq owner=@khanakia stability=stable
	MaxGroupMembers = 1000
	// MaxResourceSize bounds one serialized user, in bytes.
	//ds:def id=scim-maxresourcesize-9v4mz2kw owner=@khanakia stability=stable
	MaxResourceSize = 65535
)

Limits, better-auth's.

View Source
const (
	//ds:def id=scim-defaultrequestlimit-7w3qn8kv owner=@khanakia stability=stable
	DefaultRequestLimit = 600
	//ds:def id=scim-defaultrequestwindow-2k9xm4wq owner=@khanakia stability=stable
	DefaultRequestWindow = time.Minute
)

Default limits of EndpointRequest: an IdP's full sync of a few thousand users and their groups fits in a few minutes, and a token guesser waits.

View Source
const (
	TableNameTombstone  = "auth_scim_tombstone"
	TableAliasTombstone = "sct"
	ColDeletedAt        = "deleted_at"
)

Identity tombstones, better-auth's scimIdentityTombstone: deleting a SCIM user with an externalId leaves a row naming the library user it was linked to, so an IdP that deletes and later re-creates the same person (same connection, same externalId) gets the same account back, not a second one beside a deactivated first.

View Source
const EndpointRequest auth.EndpointID = "scim.request"

EndpointRequest is the operation every SCIM request passes through before it is served, so the core's rate rules apply to SCIM: by IP, and by connection (ByIdentifier). It is SelfServed, so no transport mounts it and no generated client or API reference lists it, and a call that did not come from Handler answers auth.ErrUnknownOperation; only Handler calls it. Override its limits with auth.WithRateRules(auth.RateRule{Match: scim.EndpointRequest, ...}).

View Source
const EntraLegacyGroupSchema = "http://schemas.microsoft.com/2006/11/ResourceManagement/ADSCIM/2.0/Group"

EntraLegacyGroupSchema is the marker AcceptEntraLegacyGroupSchema drops.

View Source
const (
	MediaType = "application/scim+json"
)

Media types. A request body must be one of them; every answer is SCIM's.

View Source
const PluginName = "scim"

PluginName is the plugin's permanent identity.

Variables

View Source
var (
	TableConnection = sqlb.Table{Name: TableNameConnection, Alias: TableAliasConnection}
	TableCredential = sqlb.Table{Name: TableNameCredential, Alias: TableAliasCredential}
	TableEvent      = sqlb.Table{Name: TableNameEvent, Alias: TableAliasEvent}
)

Tables of connection state.

View Source
var (
	ConnID           = sqlb.Col[string](TableConnection, ColID)
	ConnDomain       = sqlb.Col[string](TableConnection, ColProvisioningDomain)
	ConnManaged      = sqlb.Col[bool](TableConnection, ColManaged)
	ConnStatus       = sqlb.Col[ConnectionStatus](TableConnection, ColStatus)
	ConnCreationReq  = sqlb.Col[*string](TableConnection, ColCreationRequestID)
	ConnCreatedBy    = sqlb.Col[string](TableConnection, ColCreatedBy)
	ConnCreatedAt    = sqlb.Col[time.Time](TableConnection, ColCreatedAt)
	ConnDecomStarted = sqlb.Col[*time.Time](TableConnection, ColDecomStartedAt)
	ConnDecommission = sqlb.Col[*time.Time](TableConnection, ColDecommissionedAt)
	ConnDecomCursor  = sqlb.Col[string](TableConnection, ColDecomCursor)
	ConnReconciled   = sqlb.Col[int64](TableConnection, ColReconciledUsers)
	ConnBatches      = sqlb.Col[int64](TableConnection, ColBatches)

	CredID         = sqlb.Col[string](TableCredential, ColID)
	CredConnection = sqlb.Col[string](TableCredential, ColConnectionID)
	CredDigest     = sqlb.Col[[]byte](TableCredential, ColDigest)
	CredStatus     = sqlb.Col[CredentialStatus](TableCredential, ColStatus)
	CredScopes     = sqlb.Col[string](TableCredential, ColScopes)
	CredExpiresAt  = sqlb.Col[*time.Time](TableCredential, ColExpiresAt)
	CredCreatedAt  = sqlb.Col[time.Time](TableCredential, ColCreatedAt)
	CredCreatedBy  = sqlb.Col[string](TableCredential, ColCreatedBy)
	CredLastUsedAt = sqlb.Col[*time.Time](TableCredential, ColLastUsedAt)
	CredRevokedAt  = sqlb.Col[*time.Time](TableCredential, ColRevokedAt)
	CredRevokedBy  = sqlb.Col[*string](TableCredential, ColRevokedBy)

	EventID         = sqlb.Col[string](TableEvent, ColID)
	EventConnection = sqlb.Col[string](TableEvent, ColConnectionID)
	EventSequence   = sqlb.Col[int64](TableEvent, ColSequence)
	EventType       = sqlb.Col[EventKind](TableEvent, ColEventType)
	EventActor      = sqlb.Col[string](TableEvent, ColActorID)
	EventCredential = sqlb.Col[*string](TableEvent, ColCredentialID)
	EventCreatedAt  = sqlb.Col[time.Time](TableEvent, ColCreatedAt)
)

Typed columns of the connection state.

View Source
var (
	ErrConnectionNotFound      = auth.NewError("scim.connection_not_found", http.StatusNotFound, "No such SCIM connection.")
	ErrCreationRequestConflict = auth.NewError("scim.creation_request_conflict", http.StatusConflict, "This creation request was already used.")
	ErrTooManyCredentials      = auth.NewError("scim.too_many_credentials", http.StatusConflict, "The connection has the most active credentials it may have.")
	ErrCredentialNotFound      = auth.NewError("scim.credential_not_found", http.StatusNotFound, "No such active credential.")
	ErrConnectionDecommission  = auth.NewError("scim.connection_decommissioned", http.StatusConflict, "The SCIM connection is decommissioned.")
	ErrManagedDisabled         = errors.New("scim: managed connections are off; pass scim.ManagedConnections")
	ErrBadManaged              = errors.New("scim: managed credential hash secret must be at least 32 bytes and max active credentials 1 to 100")
)

Errors of the managed-connection API (Go only; never a SCIM answer).

View Source
var (
	TableGrant      = sqlb.Table{Name: TableNameGrant, Alias: TableAliasGrant}
	GrantConnection = sqlb.Col[string](TableGrant, ColConnectionID)
	GrantGroupID    = sqlb.Col[string](TableGrant, ColGroupID)
	GrantSCIMUserID = sqlb.Col[string](TableGrant, ColSCIMUserID)
	GrantUserID     = sqlb.Col[string](TableGrant, ColUserID)
	GrantRole       = sqlb.Col[string](TableGrant, ColRole)
	GrantCreatedAt  = sqlb.Col[time.Time](TableGrant, ColCreatedAt)
)

TableGrant and its typed columns.

View Source
var (
	TableUser   = sqlb.Table{Name: TableNameUser, Alias: TableAliasUser}
	TableGroup  = sqlb.Table{Name: TableNameGroup, Alias: TableAliasGroup}
	TableMember = sqlb.Table{Name: TableNameMember, Alias: TableAliasMember}
)

Tables.

View Source
var (
	UserID           = sqlb.Col[string](TableUser, ColID)
	UserConnectionID = sqlb.Col[string](TableUser, ColConnectionID)
	UserUserID       = sqlb.Col[string](TableUser, ColUserID)
	UserIdentityID   = sqlb.Col[*string](TableUser, ColIdentityID)
	UserUserName     = sqlb.Col[string](TableUser, ColUserName)
	UserUserNameKey  = sqlb.Col[string](TableUser, ColUserNameKey)
	UserExternalID   = sqlb.Col[*string](TableUser, ColExternalID)
	UserExternalKey  = sqlb.Col[*string](TableUser, ColExternalIDKey)
	UserActive       = sqlb.Col[bool](TableUser, ColActive)
	UserPrimaryEmail = sqlb.Col[string](TableUser, ColPrimaryEmail)
	UserResource     = sqlb.Col[string](TableUser, ColResource)
	UserCreatedAt    = sqlb.Col[time.Time](TableUser, ColCreatedAt)
	UserUpdatedAt    = sqlb.Col[time.Time](TableUser, ColUpdatedAt)
	UserRevision     = sqlb.Col[int64](TableUser, ColRevision)

	GroupID           = sqlb.Col[string](TableGroup, ColID)
	GroupConnectionID = sqlb.Col[string](TableGroup, ColConnectionID)
	GroupDisplayName  = sqlb.Col[string](TableGroup, ColDisplayName)
	GroupDisplayKey   = sqlb.Col[string](TableGroup, ColDisplayNameKey)
	GroupExternalID   = sqlb.Col[*string](TableGroup, ColExternalID)
	GroupExternalKey  = sqlb.Col[*string](TableGroup, ColExternalIDKey)
	GroupCreatedAt    = sqlb.Col[time.Time](TableGroup, ColCreatedAt)
	GroupUpdatedAt    = sqlb.Col[time.Time](TableGroup, ColUpdatedAt)

	MemberGroupID      = sqlb.Col[string](TableMember, ColGroupID)
	MemberSCIMUserID   = sqlb.Col[string](TableMember, ColSCIMUserID)
	MemberConnectionID = sqlb.Col[string](TableMember, ColConnectionID)
	MemberUserID       = sqlb.Col[string](TableMember, ColUserID)
	MemberCreatedAt    = sqlb.Col[time.Time](TableMember, ColCreatedAt)

	// DeactivatedAt is the auth_user column this plugin owns.
	DeactivatedAt = sqlb.Col[*time.Time](auth.TableUser, ColDeactivatedAt)
)

Typed columns, for an app that reads provisioning state; writes go through the SCIM endpoints.

View Source
var (
	ErrNoConnections  = errors.New("scim: add a Connection or VerifyBearerToken, or no caller can authenticate")
	ErrBadConnection  = errors.New("scim: a connection id must be 1 to 255 characters, unique, and its credentials need an id and a token without whitespace")
	ErrDuplicateToken = errors.New("scim: two credentials share one token")
)

Configuration errors from auth.New.

TableTombstone and its typed columns.

View Source
var ErrBadProfile = errors.New("scim: Resolution.Profile is ProfilePreserve, ProfileManage or empty")

ErrBadProfile is a Resolution whose Profile is neither mode: an app error, so the request fails (500) rather than guess which one was meant.

View Source
var ErrDeactivated = auth.NewError("scim.deactivated", http.StatusForbidden, "This account has been deactivated by your organization.")

ErrDeactivated refuses a session to a user the identity provider deactivated (every SCIM user linked to them is inactive), on every sign-in method, as admin's ErrBanned does for a banned user.

View Source
var ErrNoActiveUser = errors.New("scim: no active provisioned user")

ErrNoActiveUser is ActiveUser's answer for no active provisioned user.

View Source
var ErrNoGroupMapper = errors.New("scim: ReconcileProjection needs WithGroupMapper or ProjectRoles")

ErrNoGroupMapper answers ReconcileProjection with neither WithGroupMapper nor ProjectRoles.

View Source
var ErrUnauthorized = errors.New("scim: invalid bearer token")

ErrUnauthorized is VerifyBearerToken's answer for a token it does not know.

ScopeValues is every scope, the default of a credential that names none.

Functions

This section is empty.

Types

type Address

type Address struct {
	Formatted     string `json:"formatted,omitempty"`
	StreetAddress string `json:"streetAddress,omitempty"`
	Locality      string `json:"locality,omitempty"`
	Region        string `json:"region,omitempty"`
	PostalCode    string `json:"postalCode,omitempty"`
	Country       string `json:"country,omitempty"`
	Type          string `json:"type,omitempty"`
	Primary       bool   `json:"primary,omitempty"`
}

Address is one postal address.

type BearerRequest

type BearerRequest struct {
	Token  string
	Method string
	Path   string
	Header http.Header
}

BearerRequest is what VerifyBearerToken is asked to authenticate.

type Connection

type Connection struct {
	ID          string
	Credentials []Credential
	// ProvisioningDomainID groups connections of one tenant; it defaults to
	// ID and cannot change once the connection has been used.
	ProvisioningDomainID string
}

Connection is one identity provider tenant, better-auth's connection: its id scopes every resource it creates, and any of its credentials authenticates it, so a token can be rotated by adding the new credential before removing the old.

type ConnectionStatus

type ConnectionStatus string

ConnectionStatus is a connection's lifecycle state.

const (
	ConnectionActive          ConnectionStatus = "active"
	ConnectionDecommissioning ConnectionStatus = "decommissioning"
	ConnectionDecommissioned  ConnectionStatus = "decommissioned"
)

Connection statuses, better-auth's managed statuses.

type CreateManagedRequest

type CreateManagedRequest struct {
	// CreationRequestID makes the call idempotent-safe: 16 to 255
	// characters, never reused (ErrCreationRequestConflict).
	CreationRequestID    string
	ProvisioningDomainID string
	ActorID              string
	Scopes               []Scope
	ExpiresAt            *time.Time
}

CreateManagedRequest is the input of CreateManagedConnection.

type Credential

type Credential struct {
	ID        string
	Token     string
	Scopes    []Scope
	ExpiresAt *time.Time
}

Credential is one static bearer token of a connection. Scopes default to ScopeValues; an expired credential answers 401.

type CredentialStatus

type CredentialStatus string

CredentialStatus is a managed credential's state.

const (
	CredentialActive         CredentialStatus = "active"
	CredentialRevoked        CredentialStatus = "revoked"
	CredentialDecommissioned CredentialStatus = "decommissioned"
)

Credential statuses.

type DecommissionRequest

type DecommissionRequest struct {
	ConnectionID string
	// ProvisioningDomainID, when set, must be the connection's; a managed
	// connection is found only within its domain.
	ProvisioningDomainID string
	ActorID              string
	// MaxBatches bounds the work of one call (batches of 50 SCIM users);
	// zero runs to completion. A call that stops early answers
	// DecommissionReconciling, and the next call resumes at the cursor.
	MaxBatches int
}

DecommissionRequest names the connection to retire.

type DecommissionResult

type DecommissionResult struct {
	ConnectionID         string
	ProvisioningDomainID string
	Status               DecommissionStatus
	DecommissionedAt     *time.Time
	ReconciledUsers      int64
	Batches              int64
}

DecommissionResult reports a decommission's progress.

type DecommissionStatus

type DecommissionStatus string

DecommissionStatus is where a decommission stands.

const (
	DecommissionReconciling DecommissionStatus = "reconciling"
	DecommissionComplete    DecommissionStatus = "complete"
)

Decommission statuses, better-auth's.

type EnterpriseUser

type EnterpriseUser struct {
	EmployeeNumber string   `json:"employeeNumber,omitempty"`
	CostCenter     string   `json:"costCenter,omitempty"`
	Organization   string   `json:"organization,omitempty"`
	Division       string   `json:"division,omitempty"`
	Department     string   `json:"department,omitempty"`
	Manager        *Manager `json:"manager,omitempty"`
}

EnterpriseUser is the enterprise extension (RFC 7643 §4.3).

type Error

type Error struct {
	Status int
	Type   ErrorType
	Detail string
}

Error is a SCIM error answer. It is returned by the plugin's Go methods and written by Handler in SCIM's error schema.

func (*Error) Error

func (e *Error) Error() string

type ErrorType

type ErrorType string

ErrorType is RFC 7644 §3.12's scimType.

const (
	InvalidFilter ErrorType = "invalidFilter"
	TooMany       ErrorType = "tooMany"
	Uniqueness    ErrorType = "uniqueness"
	Mutability    ErrorType = "mutability"
	InvalidSyntax ErrorType = "invalidSyntax"
	InvalidPath   ErrorType = "invalidPath"
	NoTarget      ErrorType = "noTarget"
	InvalidValue  ErrorType = "invalidValue"
	InvalidVers   ErrorType = "invalidVers"
	Sensitive     ErrorType = "sensitive"
)

Error types.

type Event

type Event struct {
	Sequence     int64     `json:"sequence"`
	Type         EventKind `json:"type"`
	ActorID      string    `json:"actorId"`
	CredentialID string    `json:"credentialId,omitempty"`
	CreatedAt    time.Time `json:"createdAt"`
}

Event is one entry of a managed connection's log.

type EventKind

type EventKind string

EventKind is a managed connection event, better-auth's event types.

const (
	EventConnectionCreated         EventKind = "connection.created"
	EventCredentialIssued          EventKind = "credential.issued"
	EventCredentialRotated         EventKind = "credential.rotated"
	EventCredentialRevoked         EventKind = "credential.revoked"
	EventConnectionDecommissioning EventKind = "connection.decommissioning"
	EventConnectionDecommissioned  EventKind = "connection.decommissioned"
)

Event kinds.

type Group

type Group struct {
	Schemas     []string `json:"schemas"`
	ID          string   `json:"id,omitempty"`
	ExternalID  string   `json:"externalId,omitempty"`
	DisplayName string   `json:"displayName"`
	Members     []Member `json:"members,omitempty"`
	Meta        *Meta    `json:"meta,omitempty"`
}

Group is a SCIM group resource (RFC 7643 §4.2). Members are users of the same connection; nested groups are refused.

type GroupMapper

type GroupMapper interface {
	SyncGroup(ctx context.Context, g GroupState) error
	DeleteGroup(ctx context.Context, connectionID, groupID string) error
}

GroupMapper follows SCIM groups into the app's own grouping (an organization, a team, a role). Each method runs inside the SCIM request's transaction; an error fails the request and rolls the change back, so the IdP retries. Members are library user ids, the full current list, so an implementation can be idempotent.

type GroupState

type GroupState struct {
	ConnectionID string
	ID           string
	ExternalID   string
	DisplayName  string
	// UserIDs are the library users of the group's members.
	UserIDs []string
}

GroupState is one group as GroupMapper sees it.

type IssuedCredential

type IssuedCredential struct {
	Connection ManagedConnection
	Credential ManagedCredential
	Token      auth.Token
}

IssuedCredential is a new credential and its raw token, which is shown this once and never stored.

type ManagedConnection

type ManagedConnection struct {
	ID                   string              `json:"id"`
	ProvisioningDomainID string              `json:"provisioningDomainId"`
	Status               ConnectionStatus    `json:"status"`
	CreatedAt            time.Time           `json:"createdAt"`
	CreatedBy            string              `json:"createdBy"`
	Credentials          []ManagedCredential `json:"credentials"`
}

ManagedConnection is a managed connection as the API answers it.

type ManagedCredential

type ManagedCredential struct {
	ID         string           `json:"id"`
	Status     CredentialStatus `json:"status"`
	Scopes     []Scope          `json:"scopes"`
	ExpiresAt  *time.Time       `json:"expiresAt,omitempty"`
	CreatedAt  time.Time        `json:"createdAt"`
	CreatedBy  string           `json:"createdBy"`
	LastUsedAt *time.Time       `json:"lastUsedAt,omitempty"`
	RevokedAt  *time.Time       `json:"revokedAt,omitempty"`
}

ManagedCredential is a managed credential without its token.

type ManagedOption

type ManagedOption func(*managedConfig)

ManagedOption configures managed connections.

func CredentialHashSecret

func CredentialHashSecret(b []byte) ManagedOption

CredentialHashSecret is the HMAC key managed tokens are digested with, better-auth's credentialHashSecret; at least 32 bytes. Without it the key is derived from auth.WithSecret.

func LastUsedWriteInterval

func LastUsedWriteInterval(d time.Duration) ManagedOption

LastUsedWriteInterval overrides DefaultLastUsedWriteInterval; zero writes on every request.

func MaxActiveCredentials

func MaxActiveCredentials(n int) ManagedOption

MaxActiveCredentials overrides DefaultMaxActiveCredentials (1 to 100).

type Manager

type Manager struct {
	Value string `json:"value,omitempty"`
	Ref   string `json:"$ref,omitempty"`
}

Manager references another user. displayName is read-only and not kept.

type Member

type Member struct {
	Value   string `json:"value"`
	Display string `json:"display,omitempty"`
	Type    string `json:"type,omitempty"`
	Ref     string `json:"$ref,omitempty"`
}

Member is one group member: Value is the SCIM user id.

type Meta

type Meta struct {
	ResourceType string    `json:"resourceType"`
	Created      time.Time `json:"created"`
	LastModified time.Time `json:"lastModified"`
	Location     string    `json:"location,omitempty"`
	// Version is the weak ETag of a user's revision, W/"n"; an If-Match
	// with it makes a PUT, PATCH or DELETE conditional.
	Version string `json:"version,omitempty"`
}

Meta is a resource's metadata (RFC 7643 §3.1).

type MultiValue

type MultiValue struct {
	Value   string `json:"value,omitempty"`
	Display string `json:"display,omitempty"`
	Type    string `json:"type,omitempty"`
	Primary bool   `json:"primary,omitempty"`
}

MultiValue is one entry of emails, phoneNumbers, roles or entitlements.

type Name

type Name struct {
	Formatted       string `json:"formatted,omitempty"`
	FamilyName      string `json:"familyName,omitempty"`
	GivenName       string `json:"givenName,omitempty"`
	MiddleName      string `json:"middleName,omitempty"`
	HonorificPrefix string `json:"honorificPrefix,omitempty"`
	HonorificSuffix string `json:"honorificSuffix,omitempty"`
}

Name is a user's structured name.

type Option

type Option func(*Plugin)

Option configures the plugin.

func AcceptEntraLegacyGroupSchema

func AcceptEntraLegacyGroupSchema() Option

AcceptEntraLegacyGroupSchema accepts Microsoft Entra's legacy group schema marker on POST /Groups, better-auth's compatibility.microsoftEntra.acceptLegacyGroupSchema: exactly one EntraLegacyGroupSchema entry in schemas is dropped; a second one, or attributes under it, answer invalidValue. Off by default.

func Connections

func Connections(cs ...Connection) Option

Connections registers static connections, better-auth's connections[].

func DeleteUsers

func DeleteUsers() Option

DeleteUsers deletes the library user when the last SCIM user linked to it is deleted, instead of keeping the user deactivated. Off by default, as in better-auth, because the user's data in the app may outlive the account.

func ManagedConnections

func ManagedConnections(opts ...ManagedOption) Option

ManagedConnections turns on connections the app creates at run time with CreateManagedConnection, each with rotatable credentials and an event log, better-auth's managedConnections. Their tokens start with ManagedTokenPrefix and are stored as HMAC-SHA256 digests only.

func ProjectRoles

func ProjectRoles(rp RoleProjection) Option

ProjectRoles turns role projection on.

func ReconcileUser

func ReconcileUser(fn func(ctx context.Context, in ReconcileInput) error) Option

ReconcileUser runs fn whenever a library user's provisioning state is recomputed (after every write to a SCIM user linked to it), inside the request's transaction, better-auth's identity.reconcileUser; an error rolls the request back. Use it to mirror the state into the app's own records.

func ResolveUser

func ResolveUser(fn func(ctx context.Context, in ResolveInput) (Resolution, error)) Option

ResolveUser decides which library user a new SCIM user is linked to, better-auth's identity.resolveUser. Without it every new SCIM user gets a new library user. Linking to an existing user is the app's decision, made on evidence it trusts; the plugin never links by email itself.

func TrustEmails

func TrustEmails() Option

TrustEmails marks the primary email of a provisioned user verified. Off by default: the address is the IdP's assertion, and a verified address signs in by email and receives recovery mail. Turn it on only for connections whose IdP verifies addresses in domains the customer owns.

func VerifyBearerToken

func VerifyBearerToken(fn func(ctx context.Context, r BearerRequest) (Principal, error)) Option

VerifyBearerToken authenticates a token no static credential holds, better-auth's authentication.verifyBearerToken: the app looks the token up in its own store and returns the connection and scopes, or ErrUnauthorized. It is asked only after every static credential missed.

func WithGroupMapper

func WithGroupMapper(m GroupMapper) Option

WithGroupMapper receives every group change; see GroupMapper.

type PatchOp

type PatchOp struct {
	Op    string          `json:"op"`
	Path  string          `json:"path,omitempty"`
	Value json.RawMessage `json:"value,omitempty"`
}

PatchOp is one operation. Op is add, replace or remove in any case, and replace when empty.

type PatchRequest

type PatchRequest struct {
	Schemas    []string  `json:"schemas"`
	Operations []PatchOp `json:"Operations"`
}

PatchRequest is a PATCH body (RFC 7644 §3.5.2).

type Plugin

type Plugin struct {
	// contains filtered or unexported fields
}

Plugin is the SCIM plugin. Build it with New.

func New

func New(opts ...Option) *Plugin

New builds the plugin.

func (*Plugin) ActiveUser

func (p *Plugin) ActiveUser(ctx context.Context, connectionID, externalID string) (scimUserID, userID string, err error)

ActiveUser returns the SCIM user id and library user id of the active SCIM user a connection provisioned under externalID, better-auth's acquireActiveSCIMUserLink: an SSO sign-in that names the IdP's user can resolve it to the provisioned account, never by email. ErrNoActiveUser answers when there is none, or it is inactive.

func (*Plugin) Columns

func (p *Plugin) Columns() []auth.ColumnExtension

Columns adds scim_deactivated_at to auth_user.

func (*Plugin) ConnectionEvents

func (p *Plugin) ConnectionEvents(ctx context.Context, id, domain string) ([]Event, error)

ConnectionEvents returns the latest 100 events of a managed connection, ascending by sequence, better-auth's listSCIMManagedConnectionEvents.

func (*Plugin) CreateManagedConnection

func (p *Plugin) CreateManagedConnection(ctx context.Context, in CreateManagedRequest) (IssuedCredential, error)

CreateManagedConnection creates a connection and its first credential, better-auth's createSCIMManagedConnection. The token is returned once.

func (*Plugin) DataHooks

func (p *Plugin) DataHooks() auth.DataHooks

DataHooks refuse to create a session for a deactivated user. The hook sits on the session write, as admin's ban does, so it covers every sign-in method and a pending second-factor session, and it fires only after the method proved the credential: a stranger without the credential sees the ordinary wrong-credential answer, so the refusal reveals nothing to them. Without it a deactivated user's sign-in answered 200 with a session that SessionPredicates then refused on its first use.

func (*Plugin) DecommissionConnection

func (p *Plugin) DecommissionConnection(ctx context.Context, in DecommissionRequest) (DecommissionResult, error)

DecommissionConnection retires a connection for good, better-auth's decommissionSCIMConnection (and decommissionSCIMManagedConnection): its tokens stop working at once (every request with one answers 401 "SCIM connection is decommissioned": a static token, and a managed credential that was active, which is marked decommissioned; one revoked before keeps answering "Invalid SCIM bearer token"), then its SCIM users are made inactive in batches, each in its own transaction, and each linked library user is reconciled, so a user with no other active source is deactivated and signed out. Groups are told to the GroupMapper as deleted at the end. The SCIM rows are kept for the record. It is irreversible, resumable (the cursor survives a crash) and meant to be run by one worker at a time.

func (*Plugin) Handler

func (p *Plugin) Handler() http.Handler

Handler serves the SCIM endpoints. Mount it with the prefix stripped:

mux.Handle("/scim/v2/", http.StripPrefix("/scim/v2", p.Handler()))

It answers /Users, /Users/{id}, /Groups, /Groups/{id}, and the public /ServiceProviderConfig, /Schemas and /ResourceTypes documents.

func (*Plugin) Init

func (p *Plugin) Init(a *auth.Auth) error

Init captures the core handle.

func (*Plugin) ManagedConnection

func (p *Plugin) ManagedConnection(ctx context.Context, id, domain string) (ManagedConnection, error)

ManagedConnection returns one managed connection of domain, better-auth's getSCIMManagedConnection.

func (*Plugin) ManagedConnections

func (p *Plugin) ManagedConnections(ctx context.Context, domain string) ([]ManagedConnection, error)

ManagedConnections lists a provisioning domain's managed connections, newest first, better-auth's listSCIMManagedConnections.

func (*Plugin) Name

func (p *Plugin) Name() string

Name returns PluginName.

func (*Plugin) Operations

func (p *Plugin) Operations() []auth.Operation

Operations lists EndpointRequest.

func (*Plugin) OptionErr

func (p *Plugin) OptionErr() error

OptionErr is every error the options recorded, such as auth.ErrNilOption naming an option given nil; auth.New refuses to start while it is not nil (auth.OptionChecker).

func (*Plugin) RateLimits

func (p *Plugin) RateLimits() []auth.RateRule

RateLimits bounds SCIM requests per IP and per connection.

func (*Plugin) ReconcileProjection

func (p *Plugin) ReconcileProjection(ctx context.Context, domain string) (ProjectionResult, error)

ReconcileProjection replays every group of a provisioning domain's connections into the GroupMapper, in batches of 50 groups per transaction, better-auth's reconcileSCIMProjection: after a mapper bug is fixed, or the app's grouping was rebuilt, the app's state catches up with the IdP's without waiting for the next change.

func (*Plugin) Requires

func (p *Plugin) Requires() []string

Requires names the email plugin: a provisioned user's primary email is an email identity.

func (*Plugin) RevokeCredential

func (p *Plugin) RevokeCredential(ctx context.Context, id, domain, credentialID, actor string) error

RevokeCredential revokes one active credential, better-auth's revokeSCIMManagedCredential.

func (*Plugin) RotateCredential

func (p *Plugin) RotateCredential(ctx context.Context, id, domain, actor string, scopes []Scope, expires *time.Time) (IssuedCredential, error)

RotateCredential adds a credential beside the active ones, better-auth's rotateSCIMManagedCredential: the IdP switches to the new token, then the old credential is revoked. Scopes and expiry are the new credential's.

func (*Plugin) SessionPredicates

func (p *Plugin) SessionPredicates() []sqlb.Predicate

SessionPredicates refuses every session of a deactivated user.

func (*Plugin) Tables

func (p *Plugin) Tables() []sqlb.Table

Tables declares the three tables.

func (*Plugin) TypedMigrations

func (p *Plugin) TypedMigrations(engine string, t auth.SQLTypes) ([]auth.Migration, error)

TypedMigrations creates the three tables.

func (*Plugin) UserTables

func (p *Plugin) UserTables() []auth.UserTable

UserTables declares the rows that belong to a library user, so DeleteUser removes the SCIM user and its memberships (no foreign key does, ADR-0002).

func (*Plugin) Validate

func (p *Plugin) Validate(c *auth.Config) error

Validate refuses a configuration no caller could authenticate with, and credentials that are ambiguous.

The email identifier the provisioned users are addressed by needs no check here: Requires names the email plugin, which always registers it, and the core refuses a missing requirement before any Validate runs.

type Principal

type Principal struct {
	ConnectionID string
	CredentialID string
	Scopes       []Scope
	ExpiresAt    *time.Time
	// ProvisioningDomainID defaults to ConnectionID.
	ProvisioningDomainID string
	// contains filtered or unexported fields
}

Principal is an authenticated caller: the connection, the credential and its scopes (ScopeValues when empty), and an optional expiry.

func PrincipalFrom

func PrincipalFrom(ctx context.Context) (Principal, bool)

PrincipalFrom returns the caller of a SCIM request, for a ResolveUser or GroupMapper that needs more than the connection id.

type Profile

type Profile string

Profile is a linked SCIM user's profile mode. A library user has at most one profile source across every connection: the SCIM user that created it, or the one linked with ProfileManage. A second ProfileManage link, or the revival of a deleted source while another holds the profile, is 409 uniqueness, as better-auth's "Another SCIM source already manages this User profile". Deleting the source frees the profile for the next link (the hand-over).

const (
	ProfilePreserve Profile = "preserve"
	ProfileManage   Profile = "manage"
)

The profile modes.

type ProjectedUser

type ProjectedUser struct {
	UserID string
	Active bool
	Grants []RoleGrant
}

ProjectedUser is a user's projected state: Active while any linked SCIM user is active, and every role it holds through any group.

type ProjectionResult

type ProjectionResult struct {
	ProvisioningDomainID string
	Groups               int
	Batches              int
}

ProjectionResult reports a ReconcileProjection.

type ReconcileInput

type ReconcileInput struct {
	UserID  string
	Active  bool
	Sources int
}

ReconcileInput is a library user's provisioning state: Active while any linked SCIM user is active, Sources the number of linked SCIM users left.

type Resolution

type Resolution struct {
	UserID string
	// Profile is better-auth's link profile mode: ProfilePreserve (or "")
	// never writes the linked user's name or email; ProfileManage makes this
	// SCIM user the user's one profile source, which writes them now and on
	// every replace. Ignored when UserID is empty, since a user the plugin
	// creates is always managed.
	Profile Profile
}

Resolution is ResolveUser's answer: an empty UserID creates a new user; a UserID links the SCIM user to that existing library user, and Profile says whether the link writes that user's profile.

type ResolveInput

type ResolveInput struct {
	ConnectionID string
	User         User
}

ResolveInput is what ResolveUser is asked about.

type RoleGrant

type RoleGrant struct {
	ConnectionID string
	GroupID      string
	Role         string
}

RoleGrant is one role a user holds through one group.

type RoleProjection

type RoleProjection struct {
	// Map returns the roles a group grants its members (projection.roles.map).
	Map func(ctx context.Context, g RoleSource) ([]string, error)
	// Exists, when set, keeps only roles the app knows
	// (projection.roles.exists); a refused role grants nothing.
	Exists func(ctx context.Context, connectionID, role string) (bool, error)
	// Reconcile receives each affected user's full state, idempotent, in the
	// request's transaction (projection.reconcileUser); an error rolls back.
	Reconcile func(ctx context.Context, u ProjectedUser) error
}

RoleProjection configures ProjectRoles.

type RoleSource

type RoleSource struct {
	ConnectionID string
	GroupID      string
	ExternalID   string
	DisplayName  string
}

RoleSource is the group a role is mapped from.

type Scope

type Scope string

Scope is what a credential may do, better-auth's credential scopes.

const (
	ScopeUsersRead   Scope = "scim.users.read"
	ScopeUsersWrite  Scope = "scim.users.write"
	ScopeGroupsRead  Scope = "scim.groups.read"
	ScopeGroupsWrite Scope = "scim.groups.write"
)

Scopes. A GET or HEAD needs the resource's read scope, anything else its write scope; /Groups paths need the groups scopes, every other path the users scopes.

type User

type User struct {
	Schemas           []string        `json:"schemas"`
	ID                string          `json:"id,omitempty"`
	ExternalID        string          `json:"externalId,omitempty"`
	UserName          string          `json:"userName"`
	Name              *Name           `json:"name,omitempty"`
	DisplayName       string          `json:"displayName,omitempty"`
	Title             string          `json:"title,omitempty"`
	UserType          string          `json:"userType,omitempty"`
	PreferredLanguage string          `json:"preferredLanguage,omitempty"`
	Locale            string          `json:"locale,omitempty"`
	Timezone          string          `json:"timezone,omitempty"`
	Active            *bool           `json:"active,omitempty"`
	Emails            []MultiValue    `json:"emails,omitempty"`
	PhoneNumbers      []MultiValue    `json:"phoneNumbers,omitempty"`
	Addresses         []Address       `json:"addresses,omitempty"`
	Roles             []MultiValue    `json:"roles,omitempty"`
	Entitlements      []MultiValue    `json:"entitlements,omitempty"`
	Enterprise        *EnterpriseUser `json:"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User,omitempty"`
	Meta              *Meta           `json:"meta,omitempty"`
}

User is a SCIM user resource (RFC 7643 §4.1) as this plugin stores and answers it: the core schema, the enterprise extension, and the attributes better-auth supports. Passwords, ims, photos and x509Certificates are not accepted; an unknown attribute is ignored.

func (User) IsActive

func (u User) IsActive() bool

IsActive reports the user's active flag, true when absent.

func (User) PrimaryEmail

func (u User) PrimaryEmail() string

PrimaryEmail derives the address the library user gets: the one marked primary, else the first of type work, else the first; with no emails, userName when it is an address. Empty when none qualifies.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL