Skip to content

feat: implement scim and oidc integration - #2674

Open
cristianscheid wants to merge 1 commit into
masterfrom
feat/1861/scim-oidc-integration
Open

feat: implement scim and oidc integration#2674
cristianscheid wants to merge 1 commit 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 scenario in a federated environment:

  • 1 master instance: connects to a SCIM server, fetches groups, and creates the corresponding circles
  • N slave instances: connect to a OIDC server, fetch user's membership information, and add them to the corresponding circles on master

on master, SCIM server integration can be configured like:

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

on slave, OIDC server integration can be configured like:

occ config:app:set circles oidc_enabled --type=boolean --value=true
occ config:app:set circles oidc_issuer --value="https://login-dev.helmholtz.de/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://login-dev.helmholtz.de/oauth2-as/oauth2-authz"
occ config:app:set circles oidc_token_endpoint --value="https://login-dev.helmholtz.de/oauth2/token"
occ config:app:set circles oidc_userinfo_endpoint --value="https://login-dev.helmholtz.de/oauth2/userinfo"
occ config:app:set circles oidc_scope --value="openid entitlements offline_access"
occ config:app:set circles oidc_membership_claim --value="entitlements"

How a circle representing a SCIM group is identified across instances

If master creates a circle from a SCIM group, how does a slave know which circle on the master 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 hash randomly, this method always returns the same generated value for the same input

This means:

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

New circle config flag

A new circle config flag, Circle::CFG_THIRD_PARTY, was introduced to mark circles managed by an external system (in this case, a SCIM server). Every circle created from a SCIM group gets this flag.

How groups from SCIM are synced

On the master instance, 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_THIRD_PARTY flag, if it doesn't exist yet.

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

Triggered via:

  • command: occ circles:scim:sync
  • cron: OCA\Circles\Cron\ScimSync

The "remote moderator" mechanism

For a slave to be able to add one of its own users to a third-party circle on master, two things are needed:

  1. the slave must be aware that the circle exists
  2. whoever performs the "add member" action on slave needs moderator rights on master's circle

To achieve this, the master instance should set a config, remote_mod_circle_instances, listing which remote instances are allowed to add members to its third-party circles, e.g.:

["instance-one","instance-two"]

The master then makes a request to each configured remote_mod_circle_instances on the /index.php/apps/circles/moderator/ endpoint, which creates a circle called remote-mod-circle on slave if it doesn't exist yet, and returns its circle ID. the master stores the results in remote_mod_circle_mapping, e.g.:

{
  "instance-one": "qH6ucMyBtQgo4HiBNyDqR33KXAqAaIV",
  "instance-two": "wmqsbzRKPHhE5vlC7XDOgJLYP9KwspX"
}

This tells the master which circle on each slave acts as the moderator, allowed to add members to its third-party circles.

Triggered via:

  • command: occ circles:remotemod:discover
  • cron: OCA\Circles\Cron\RemoteModDiscover

Once the mapping is done, the master iterates through every third-party circle and adds each moderator circle from remote_mod_circle_mapping as a member with moderator level.

From this point on, every slave is aware of third-party circles on the master and can add members to them, using its own remote-mod-circle as the initiator, since it has moderator level.

Triggered via:

  • command: occ circles:remotemod:sync
  • queued job: OCA\Circles\BackgroundJob\RemoteModSync

Connecting with OIDC

On a slave instance, 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 user's memberships from OIDC are synced

On slave instances, 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 third-party circle on master.

It's also checked whether the user is a member of any third-party circle they no longer belong to, and, if so, they are removed accordingly.

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

  • manually, via occ circles:oidc:sync
  • periodically, via cron: OCA\Circles\Cron\OidcSync
  • right after the user connects/authenticates to OIDC, if an access token was returned
  • on login:
    • the UserLoggedIn listener adds an OidcSyncUser 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 master fetch circles from SCIM server and create the corresponding circles if missing
circles:remotemod:discover master discover the remote moderator circle id for each configured remote instance
circles:remotemod:sync master ensure every discovered remote moderator circle is a member of every third-party circle
circles:oidc:sync slave fetch memberships from OIDC server and add users to corresponding circles if not a member

Jobs

job runs on type interval / trigger
OCA\Circles\Cron\ScimSync master TimedJob every 12h
OCA\Circles\Cron\RemoteModDiscover master TimedJob every 12h
OCA\Circles\BackgroundJob\RemoteModSync master QueuedJob triggered by RemoteModDiscover
OCA\Circles\Cron\OidcSync slave TimedJob every 24h
OCA\Circles\BackgroundJob\OidcSyncUser slave QueuedJob triggered by UserLoggedIn listener

TODO

  • SCIM server integration (OCA\Circles\Service\ScimService::fetchCircles()) still need to be tested against a real SCIM server, and adjusted as needed once access is available.

  • Currently, this implementation only covers a federated setup where the master fetches groups from SCIM and creates circles, while slaves only fetch membership data from OIDC. A future iteration should also support a single instance configured with both SCIM and OIDC, so it can fetch memberships from OIDC and add users directly to its own SCIM circles, without needing a separate master/slave split.

  • When a remote instance is removed from remote_mod_circle_instances, there's no cleanup mechanism in place, meaning the slave's remote moderator circle and its members are still kept on the master's third-party circles.

    • A dedicated occ command for adding/removing entries from remote_mod_circle_instances could be implemented, as it would be a good place to trigger this cleanup, removing the remote moderator circle and its members from the master's third-party circles upon removal.
  • When a circle is removed on the master, it doesn't seem to be propagated to slaves (federated instances). This appears to be existing behavior in Circles app, a listener to propagate circle removal to slaves may be needed.

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 from 3eb9581 to 3b7daf7 Compare August 6, 2026 14:16
Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
@cristianscheid
cristianscheid force-pushed the feat/1861/scim-oidc-integration branch 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
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.

1 participant