Skip to content

feat: implement scim and oidc integration - #2674

Merged
cristianscheid merged 5 commits into
masterfrom
feat/1861/scim-oidc-integration
Aug 11, 2026
Merged

cristianscheid merged 5 commits into
masterfrom
feat/1861/scim-oidc-integration

Conversation

@cristianscheid

@cristianscheid cristianscheid commented Jul 23, 2026 •

Copy link
Copy Markdown
Member
  • Resolves: #

Summary

Implement support for SCIM and OIDC integration.

This is the first iteration of development of this feature, and covers the following scenarios:

  • Single instance environment:

    • connects to a SCIM server, fetches groups, and creates the corresponding circles
    • connects to an OIDC server, fetches users' membership information, and adds them to the corresponding circles
  • Federated environment:

    • 1 master instance: connects to a SCIM server, fetches groups, and creates the corresponding circles
    • N slave instances: connect to an OIDC server, fetch users' membership information, and add them to the corresponding circles, whether local or on master, depending on where the SCIM integration is running

Configurations

On the instance running SCIM integration (master, in a federated setup, or the single instance otherwise):

occ config:app:set circles scim_enabled --type=boolean --value=true
occ config:app:set circles scim_endpoint --value="https://scim.example.com/oauth2"
occ config:app:set circles scim_token --value="TOKEN"

On the instance(s) running OIDC integration (slaves or local, in a federated setup, or the single instance otherwise):

occ config:app:set circles oidc_enabled --type=boolean --value=true
occ config:app:set circles oidc_issuer --value="https://oidc.example.com/oauth2"
occ config:app:set circles oidc_client_id --value="CLIENT_ID"
occ config:app:set circles oidc_client_secret --value="CLIENT_SECRET"
occ config:app:set circles oidc_authorization_endpoint --value="https://oidc.example.com/oauth2-as/oauth2-authz"
occ config:app:set circles oidc_token_endpoint --value="https://oidc.example.com/oauth2/token"
occ config:app:set circles oidc_userinfo_endpoint --value="https://oidc.example.com/oauth2/userinfo"
occ config:app:set circles oidc_scope --value="openid entitlements offline_access"
occ config:app:set circles oidc_membership_claim --value="entitlements"

In a federated setup, on the instance running SCIM, set the list of remote instances whose federation agent is trusted as a moderator on this instance's SCIM circles:

occ config:app:set circles scim_federated_moderator_instances --type=array --value='["instance-two","instance-three"]'

On each slave, enable its own federation agent, so it can act as moderator on the master's SCIM circles:

occ config:app:set circles federation_agent_enabled --type=boolean --value=true

How a circle representing a SCIM group is identified across instances

If the instance running SCIM creates a circle from a SCIM group, how does an instance running OIDC know which circle corresponds to a membership fetched from OIDC?

A new method, TStringTools::generateCircleIdFromString(), generates a circle ID (hash) from a string. Unlike token() (also in this trait), which generates a hash randomly, this method always returns the same generated value for the same input.

This means:

  • the SCIM side receives a group identifier (e.g. urn:geant:company.co:group:my_group#login.company.co) and generates a circle ID from it when creating the circle
  • the OIDC side sees a user is a member of that same identifier, generates a circle ID from it using the exact same method, and can correctly identify the corresponding circle, whether it's local or on another instance

New circle config flag

A new circle config flag, Circle::CFG_SCIM, was introduced to mark circles managed by a SCIM server. Every circle created from a SCIM group gets this flag, and CircleRequest::getScim() fetches all of them.

How groups from SCIM are synced

On the instance running SCIM, a request is made to the SCIM server to retrieve groups. For each group, a circle ID is generated (using TStringTools::generateCircleIdFromString(), as mentioned above), and a circle is created with the Circle::CFG_SCIM flag, if it doesn't exist yet.

It's also checked whether any existing SCIM circle no longer has a corresponding group on the SCIM server, and it's destroyed accordingly.

Triggered via:

  • command: occ circles:scim:sync-circles
  • cron: OCA\Circles\BackgroundJob\ScimSyncCircles

The federation agent

In a federated setup, for a slave to add its own users to a SCIM circle owned by another instance, two things are needed:

  1. the slave must know the circle exists
  2. whoever adds the member needs moderator rights on that circle

To solve this, a new entity was introduced: the app:federation_agent:{singleId} circle. It represents an instance on federated/remote circles.

On this specific SCIM/OIDC scenario, the slave's federation agent is added as a moderator on the master's SCIM circles. Once added, the slave knows those circles exist, and, having moderator rights, can add its own users to them.

It's meant to be a generic agent representing the instance, reusable by future features that might need an entity to act on remote circles, not just this SCIM/OIDC scenario.

It's created lazily, the first time RemoteStreamService::getAppSignatory() runs, for example during occ circles:remote or occ circles:scim:sync-federated-moderators (more on this command below), and only if federation_agent_enabled is set. Its singleId is included in the discovery payload already exchanged between trusted instances, so no extra endpoint or request is needed to look it up.

On the instance running SCIM, scim_federated_moderator_instances lists which remote instances' federation agents are trusted as moderators on its SCIM circles, e.g.:

["instance-two","instance-three"]

For each configured instance, its federation agent's singleId is read from its (already cached, or freshly fetched if not yet known) RemoteInstance record, and added as a moderator to every SCIM circle.

Triggered via:

  • command: occ circles:scim:sync-federated-moderators
  • cron: OCA\Circles\BackgroundJob\ScimSyncFederatedModerators

Resolving the initiator

Whenever a member needs to be added to or removed from a circle, OidcService::setInitiatorForCircle() resolves the correct initiator based on whether the circle is local or remote:

  • local circle is managed by 'app:circles:{singleId}'
  • remote circle is managed by 'app:federation_agent:{singleId}'

Connecting with OIDC

On an instance running OIDC, a user can go to "Personal settings > Teams" and click "Connect" under "Connect to OIDC provider":

image

This redirects the user to the OIDC provider to authenticate. Once authenticated, the user is redirected back and their refresh token is stored.

How a user's memberships from OIDC are synced

On instances running OIDC, a request is made to the OIDC server for a given user, retrieving the group identifiers from the oidc_membership_claim (e.g. entitlements). For each identifier, a circle ID is generated (using TStringTools::generateCircleIdFromString(), as mentioned above), and the user is added to the corresponding SCIM circle.

Memberships added this way are marked with Member::setManagedBy('oidc'). This is also used to detect and remove memberships the user no longer has according to OIDC.

A user's OIDC memberships are synced in any of the following situations:

  • manually, via occ circles:oidc:sync-memberships
  • periodically, via cron: OCA\Circles\BackgroundJob\OidcSyncMemberships
  • right after the user connects/authenticates to OIDC, if an access token was returned
  • on login:
    • the UserLoggedIn listener adds an OidcSyncMembershipsUser queued job
    • a queued job is used instead of syncing directly during login, to avoid delaying the login itself while waiting on the OIDC server's response

OCC commands

command runs on description
circles:scim:sync-circles SCIM instance sync circles from SCIM groups
circles:scim:sync-federated-moderators SCIM instance sync federated moderators for SCIM circles
circles:oidc:sync-memberships OIDC instance sync circle memberships from OIDC

Jobs

job runs on type interval / trigger
OCA\Circles\BackgroundJob\ScimSyncCircles SCIM instance TimedJob every 12h
OCA\Circles\BackgroundJob\ScimSyncFederatedModerators SCIM instance TimedJob every 12h
OCA\Circles\BackgroundJob\OidcSyncMemberships OIDC instance TimedJob every 24h
OCA\Circles\BackgroundJob\OidcSyncMembershipsUser OIDC instance QueuedJob triggered by UserLoggedIn listener

TODO

  • SCIM server integration (lib/Service/ScimService::fetchCircles()) still needs to be tested against a real SCIM server, and adjusted as needed once access is available.
  • When a remote instance is removed from scim_federated_moderator_instances, there's no cleanup mechanism in place: its federation agent and any memberships it added remain on the SCIM circles.
    • A dedicated occ command for adding/removing entries from scim_federated_moderator_instances could be a good place to trigger this cleanup.
  • When a circle is removed on one instance, deletion is not propagated to other instances. This seems to be a existing behavior in the Circles app, a listener to propagate circle removal may be needed.
  • Add mechanism to allow user to disconnect from OIDC server.
  • Store the SCIM group ID on the circle, to know which SCIM group a given circle represents.
  • Consider setting the source of SCIM-created circles to "SCIM" instead of the current value.
  • Consider creating SCIM circles with CFG_APP + CFG_SYSTEM instead of the current config.
  • In lib/Service/FederationAgentService::ensureModerator(), if the federation agent is already a member but not a moderator, its level should be updated instead of being skipped.

Checklist

AI (if applicable)

  • The content of this PR was partly or fully generated using AI

@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cristianscheid
cristianscheid force-pushed the feat/1861/scim-oidc-integration branch from af7cec3 to 3eb9581 Compare July 24, 2026 14:30
@cristianscheid
cristianscheid force-pushed the feat/1861/scim-oidc-integration branch 2 times, most recently from 2727221 to bd9972c Compare August 7, 2026 11:42
@cristianscheid cristianscheid self-assigned this Aug 7, 2026
@cristianscheid cristianscheid added this to the Nextcloud 35 milestone Aug 7, 2026
@cristianscheid
cristianscheid marked this pull request as ready for review August 7, 2026 13:16
@cristianscheid
cristianscheid force-pushed the feat/1861/scim-oidc-integration branch 13 times, most recently from a82249a to 33d6170 Compare August 9, 2026 15:52
Comment thread lib/BackgroundJob/OidcSyncMembershipsUser.php Outdated
Comment thread lib/Command/CirclesOidcSyncMemberships.php Outdated
Comment thread lib/Command/CirclesScimSyncCircles.php Outdated
Comment thread lib/Command/CirclesScimSyncFederatedModerators.php Outdated
Comment thread lib/Cron/OidcSyncMemberships.php Outdated
Comment thread lib/Cron/ScimSyncCircles.php Outdated
Comment thread lib/Cron/ScimSyncFederatedModerators.php Outdated
Comment thread lib/BackgroundJob/OidcSyncMemberships.php
@cristianscheid
cristianscheid force-pushed the feat/1861/scim-oidc-integration branch from 33d6170 to 6ddcf96 Compare August 10, 2026 15:24
Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
…ge CFG_THIRD_PARTY to CFG_SCIM

Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
…dency

Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
@cristianscheid
cristianscheid force-pushed the feat/1861/scim-oidc-integration branch from 6ddcf96 to 1b702ec Compare August 11, 2026 10:33
@cristianscheid

Copy link
Copy Markdown
Member Author

/compile

Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
@cristianscheid
cristianscheid merged commit a70f5bc into master Aug 11, 2026
44 of 45 checks passed
@cristianscheid
cristianscheid deleted the feat/1861/scim-oidc-integration branch August 11, 2026 13:01
@ser

ser commented Sep 22, 2026 •

Copy link
Copy Markdown

Hello, a few questions:

  1. why Teams can't just use RFC 7591 to register itself on OIDC
  2. are you planning to allow teams to be provisioned to teams, not just individuals?
  3. why so many OIDC configuration parameters are in use instead of simply providing OpenID Configuration URL
  4. do you plan to integrate Teams oidc login with user_oidc to avoid forcing users to do logging into the same OIDC server twice
  5. SCIM has sources or providers. SCIM Provider pushes groups and users to SCIM Source. I'm not familiar with a concept of a SCIM Source pulling data as SCIM Providers usually do not have endpoints to fetch anything. It also eliminates a requirement to use any internal trigger, as cron, to get accurate data as it's the Providers role to update information

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants