Skip to content

feat(scim): add SCIM Users, Groups, and admin endpoints - #2747

Open
xlgmokha wants to merge 1 commit into
masterfrom
scim/3-users
Open

xlgmokha wants to merge 1 commit into
masterfrom
scim/3-users

Conversation

@xlgmokha

@xlgmokha xlgmokha commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

Feature. SCIM 2.0 provisioning for SSO providers: Users, Groups, per-provider tokens, admin endpoints, and audit events.

What is the current behavior?

/scim/v2 serves discovery metadata only, behind GOTRUE_EXPERIMENTAL_SCIM_ENABLED. There is no way to provision users or groups from an IdP.

What is the new behavior?

SCIM endpoints, authenticated with a bearer token that is scoped to one SSO provider:

Method Path Description
GET /scim/v2/Users List, filter (userName eq, externalId eq), sort, paginate
POST /scim/v2/Users Create, and create or link the auth user
GET, PUT, PATCH, DELETE /scim/v2/Users/{id} Read, replace, patch, soft delete
GET /scim/v2/Groups List, filter (displayName eq, externalId eq), paginate
POST /scim/v2/Groups Create
GET, PUT, PATCH, DELETE /scim/v2/Groups/{id} Read, replace, patch, delete
GET /scim/v2/ServiceProviderConfig, /scim/v2/ResourceTypes[/{id}], /scim/v2/Schemas[/{id}] Discovery, bearer token required (unauthenticated on master)

Admin endpoints under /admin/sso/providers/{idp_id}/scim:

Method Path Description
GET / Status: enabled, base_url, active tokens
DELETE / Disable: revoke all active tokens, return the revoked tokens
GET /tokens List tokens
POST /tokens Create a token (plaintext returned once), optional expires_at
DELETE /tokens/{prefix} Revoke a token

Audit events:

  • scim_user_created, scim_user_updated, scim_user_deactivated, scim_user_reactivated, scim_user_deleted
  • scim_group_created, scim_group_updated, scim_group_deleted, scim_group_member_added, scim_group_member_removed
  • scim_enabled, scim_disabled, scim_token_created, scim_token_revoked, scim_users_banned

Configuration:

Env Default Note
GOTRUE_SSO_SCIM_ENABLED false Replaces GOTRUE_EXPERIMENTAL_SCIM_ENABLED, which is removed
GOTRUE_RATE_LIMIT_SCIM 3000 Requests per 5 minutes, burst 30

Additional context

Extracted from #2731

@xlgmokha
xlgmokha changed the base branch from master to scim/2-core August 26, 2026 00:59
@xlgmokha xlgmokha self-assigned this Aug 26, 2026
@xlgmokha
xlgmokha force-pushed the scim/3-users branch 2 times, most recently from 6e4416d to edff202 Compare August 26, 2026 22:33
@xlgmokha
xlgmokha force-pushed the scim/2-core branch 2 times, most recently from f689fc8 to fec7a7d Compare August 26, 2026 23:29
@xlgmokha
xlgmokha force-pushed the scim/3-users branch 2 times, most recently from 3538a0b to 77782ab Compare August 26, 2026 23:31
@xlgmokha
xlgmokha force-pushed the scim/3-users branch 2 times, most recently from 4709682 to e114c8e Compare August 27, 2026 16:17
@xlgmokha
xlgmokha force-pushed the scim/2-core branch 2 times, most recently from a9ac782 to 316aa74 Compare August 27, 2026 17:11
@xlgmokha
xlgmokha force-pushed the scim/3-users branch 3 times, most recently from eb969a8 to 86c6ef9 Compare August 28, 2026 15:45
@xlgmokha
xlgmokha marked this pull request as ready for review August 28, 2026 16:39
@xlgmokha
xlgmokha requested a review from a team as a code owner August 28, 2026 16:39
Comment thread internal/api/scim/user_repository.go Outdated
Comment thread internal/api/scim/user_repository.go Outdated
@xlgmokha
xlgmokha force-pushed the scim/3-users branch 2 times, most recently from 7294ee5 to e6690b1 Compare August 28, 2026 16:58
@xlgmokha
xlgmokha marked this pull request as ready for review September 1, 2026 17:26
Comment thread internal/models/scim_token.go Outdated

@annabkr annabkr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: could we break this up for easier review? I find Claude is pretty good at doing that, if it feels tedious to do yourself

@hf hf left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, some minor clarifications not blocking from my POV.

Comment thread internal/api/scim/core/attribute.go Outdated
Comment on lines +5 to +16
Name string `json:"name"`
Type AttributeType `json:"type"`
MultiValued bool `json:"multiValued"`
Description string `json:"description"`
Required bool `json:"required"`
CanonicalValues []string `json:"canonicalValues,omitempty"`
CaseExact bool `json:"caseExact"`
Mutability Mutability `json:"mutability"`
Returned Returned `json:"returned"`
Uniqueness Uniqueness `json:"uniqueness"`
ReferenceTypes []ReferenceType `json:"referenceTypes,omitempty"`
SubAttributes []*Attribute `json:"subAttributes,omitempty"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't all of these have omitempty?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't all of these have omitempty?

RFC 7643 says:

Unlike other core resources, the "Schema" resource MAY contain a complex object within a sub-attribute, and all attributes are REQUIRED unless otherwise specified.

So I opted to not add the omitempty so that they get the default zero values which would be false for all booleans.

field required
name Y
name Y
type Y
multiValued Y
description Y
required Y
caseExact Y
mutability Y
returned Y
uniqueness Y
canonicalValues N
referenceTypes N
subAttributes N

Comment thread internal/api/scim/core/kind.go Outdated
Comment on lines +85 to +87
if values.Get("sortBy") != "" {
return SortAscending, nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is ?sortBy (without =true) handled here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is ?sortBy (without =true) handled here?

I think this will return as a default sort order and then the default sorting will kick in. I'll double check though.

Comment thread internal/models/sso.go Outdated
Comment thread internal/api/scim/server.go Outdated
@xlgmokha
xlgmokha force-pushed the scim/3-users branch 2 times, most recently from 7f512ea to 3c58fe8 Compare September 24, 2026 18:14
Comment thread internal/api/scim/users_repository.go Outdated
Comment thread internal/api/scim_provisioner.go Outdated
@blacksmith-sh

This comment has been minimized.

Comment thread internal/api/external.go
Comment thread internal/tokens/service.go Outdated
Comment on lines +211 to +212
case models.LinkAccount:
if _, err = p.api.createNewIdentity(tx, user, providerType, identityData(input)); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Severity: MEDIUM

SCIM's LinkAccount branch attaches an sso:<provider> identity to an existing password user but leaves user.IsSSOUser false. That user passes the SSO-only passkey-registration guard, can register a credential while provisioned, and later use passkey login after SCIM deactivation to obtain a fresh session, bypassing deprovisioning.
Helpful? Add 👍 / 👎

💡 Fix Suggestion

Suggestion: In the models.LinkAccount case within the link function, after attaching the SSO identity to an existing password user, explicitly set user.IsSSOUser = true and persist it to the database using tx.UpdateOnly(user, "is_sso_user"). This ensures that: (1) the passkey-registration guard in passkey_registration.go correctly blocks the now-SSO-linked user from registering passkeys, and (2) the SCIM deprovisioning check in tokens/service.go (if user.IsSSOUser { ... IsSCIMUserDeprovisionedForUpdate ... }) is evaluated for this user after a SCIM deactivation, preventing bypass of deprovisioning via passkey login.

⚠️ Experimental Feature: This code suggestion is automatically generated. Please review carefully.

Suggested change
case models.LinkAccount:
if _, err = p.api.createNewIdentity(tx, user, providerType, identityData(input)); err != nil {
case models.LinkAccount:
if !user.IsSSOUser {
user.IsSSOUser = true
if err = tx.UpdateOnly(user, "is_sso_user"); err != nil {
return nil, false, err
}
}
if _, err = p.api.createNewIdentity(tx, user, providerType, identityData(input)); err != nil {

Comment thread internal/api/external.go Outdated
@xlgmokha xlgmokha changed the title feat(scim): Add SCIM /Users endpoints feat(scim): add SCIM Users, Groups, and admin endpoints Sep 28, 2026
err := conn.Transaction(func(tx *storage.Connection) error {
var terr error

if config.SSO.SCIM.Enabled && user.IsSSOUser {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Severity: MEDIUM

When SCIM links a provisioned record to an existing password account, user.IsSSOUser remains false. After the IdP sets active:false, this guard skips the SCIM deprovision check; password or refresh-token authentication can then issue a new session for the offboarded user.
Helpful? Add 👍 / 👎

💡 Fix Suggestion

Suggestion: Remove the user.IsSSOUser condition from the SCIM deprovision guard. The IsSCIMUserDeprovisionedForUpdate function already safely handles users who have no SCIM records by returning false when len(rows) == 0 (line 301 of scim_user.go). By keeping user.IsSSOUser in the guard, users whose password accounts were linked to a SCIM provisioned record (where IsSSOUser remains false) bypass the deprovisioning check entirely. Changing the condition to if config.SSO.SCIM.Enabled { ensures all users are checked against SCIM deprovisioning status when SCIM is active, regardless of whether they are flagged as SSO users.

⚠️ Experimental Feature: This code suggestion is automatically generated. Please review carefully.

Suggested change
if config.SSO.SCIM.Enabled && user.IsSSOUser {
if config.SSO.SCIM.Enabled {

Comment thread internal/api/admin.go Outdated
Comment thread internal/api/external.go

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants