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
- Variables
- type Address
- type BearerRequest
- type Connection
- type ConnectionStatus
- type CreateManagedRequest
- type Credential
- type CredentialStatus
- type DecommissionRequest
- type DecommissionResult
- type DecommissionStatus
- type EnterpriseUser
- type Error
- type ErrorType
- type Event
- type EventKind
- type Group
- type GroupMapper
- type GroupState
- type IssuedCredential
- type ManagedConnection
- type ManagedCredential
- type ManagedOption
- type Manager
- type Member
- type Meta
- type MultiValue
- type Name
- type Option
- func AcceptEntraLegacyGroupSchema() Option
- func Connections(cs ...Connection) Option
- func DeleteUsers() Option
- func ManagedConnections(opts ...ManagedOption) Option
- func ProjectRoles(rp RoleProjection) Option
- func ReconcileUser(fn func(ctx context.Context, in ReconcileInput) error) Option
- func ResolveUser(fn func(ctx context.Context, in ResolveInput) (Resolution, error)) Option
- func TrustEmails() Option
- func VerifyBearerToken(fn func(ctx context.Context, r BearerRequest) (Principal, error)) Option
- func WithGroupMapper(m GroupMapper) Option
- type PatchOp
- type PatchRequest
- type Plugin
- func (p *Plugin) ActiveUser(ctx context.Context, connectionID, externalID string) (scimUserID, userID string, err error)
- func (p *Plugin) Columns() []auth.ColumnExtension
- func (p *Plugin) ConnectionEvents(ctx context.Context, id, domain string) ([]Event, error)
- func (p *Plugin) CreateManagedConnection(ctx context.Context, in CreateManagedRequest) (IssuedCredential, error)
- func (p *Plugin) DataHooks() auth.DataHooks
- func (p *Plugin) DecommissionConnection(ctx context.Context, in DecommissionRequest) (DecommissionResult, error)
- func (p *Plugin) Handler() http.Handler
- func (p *Plugin) Init(a *auth.Auth) error
- func (p *Plugin) ManagedConnection(ctx context.Context, id, domain string) (ManagedConnection, error)
- func (p *Plugin) ManagedConnections(ctx context.Context, domain string) ([]ManagedConnection, error)
- func (p *Plugin) Name() string
- func (p *Plugin) Operations() []auth.Operation
- func (p *Plugin) OptionErr() error
- func (p *Plugin) RateLimits() []auth.RateRule
- func (p *Plugin) ReconcileProjection(ctx context.Context, domain string) (ProjectionResult, error)
- func (p *Plugin) Requires() []string
- func (p *Plugin) RevokeCredential(ctx context.Context, id, domain, credentialID, actor string) error
- func (p *Plugin) RotateCredential(ctx context.Context, id, domain, actor string, scopes []Scope, ...) (IssuedCredential, error)
- func (p *Plugin) SessionPredicates() []sqlb.Predicate
- func (p *Plugin) Tables() []sqlb.Table
- func (p *Plugin) TypedMigrations(engine string, t auth.SQLTypes) ([]auth.Migration, error)
- func (p *Plugin) UserTables() []auth.UserTable
- func (p *Plugin) Validate(c *auth.Config) error
- type Principal
- type Profile
- type ProjectedUser
- type ProjectionResult
- type ReconcileInput
- type Resolution
- type ResolveInput
- type RoleGrant
- type RoleProjection
- type RoleSource
- type Scope
- type User
Constants ¶
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).
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).
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.
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.
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.
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.
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.
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.
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, ...}).
const EntraLegacyGroupSchema = "http://schemas.microsoft.com/2006/11/ResourceManagement/ADSCIM/2.0/Group"
EntraLegacyGroupSchema is the marker AcceptEntraLegacyGroupSchema drops.
const (
MediaType = "application/scim+json"
)
Media types. A request body must be one of them; every answer is SCIM's.
const PluginName = "scim"
PluginName is the plugin's permanent identity.
Variables ¶
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.
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.
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).
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.
var ( TableUser = sqlb.Table{Name: TableNameUser, Alias: TableAliasUser} TableGroup = sqlb.Table{Name: TableNameGroup, Alias: TableAliasGroup} TableMember = sqlb.Table{Name: TableNameMember, Alias: TableAliasMember} )
Tables.
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.
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.
var ( TableTombstone = sqlb.Table{Name: TableNameTombstone, Alias: TableAliasTombstone} TombConnection = sqlb.Col[string](TableTombstone, ColConnectionID) TombExternalKey = sqlb.Col[string](TableTombstone, ColExternalIDKey) TombUserID = sqlb.Col[string](TableTombstone, ColUserID) TombIdentityID = sqlb.Col[*string](TableTombstone, ColIdentityID) TombDeletedAt = sqlb.Col[time.Time](TableTombstone, ColDeletedAt) )
TableTombstone and its typed columns.
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.
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.
var ErrNoActiveUser = errors.New("scim: no active provisioned user")
ErrNoActiveUser is ActiveUser's answer for no active provisioned user.
var ErrNoGroupMapper = errors.New("scim: ReconcileProjection needs WithGroupMapper or ProjectRoles")
ErrNoGroupMapper answers ReconcileProjection with neither WithGroupMapper nor ProjectRoles.
ErrUnauthorized is VerifyBearerToken's answer for a token it does not know.
var ScopeValues = []Scope{ScopeUsersRead, ScopeUsersWrite, ScopeGroupsRead, ScopeGroupsWrite}
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 ¶
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 ¶
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 ¶
Error is a SCIM error answer. It is returned by the plugin's Go methods and written by Handler in SCIM's error schema.
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 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 ¶
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 (*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 ¶
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 ¶
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 ¶
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) 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) Operations ¶
Operations lists EndpointRequest.
func (*Plugin) OptionErr ¶
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 ¶
RateLimits bounds SCIM requests per IP and per connection.
func (*Plugin) ReconcileProjection ¶
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 ¶
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 ¶
SessionPredicates refuses every session of a deactivated user.
func (*Plugin) TypedMigrations ¶
TypedMigrations creates the three tables.
func (*Plugin) UserTables ¶
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 ¶
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.
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).
type ProjectedUser ¶
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 ¶
ProjectionResult reports a ReconcileProjection.
type ReconcileInput ¶
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 ¶
ResolveInput is what ResolveUser is asked about.
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 ¶
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) PrimaryEmail ¶
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.