Projekt BISO 3 - Handbuch
[inhalt]

OIDC Auto-Provisioning (Single Sign-On)

German Version

This page describes BISO's general OIDC auto-provisioning. The canton-specific Geneva implementation (GINA), including the complete configuration, is documented at Canton of Geneva: OIDC/GINA User Provisioning.

Purpose

With single sign-on (SSO), an external identity provider (IdP) authenticates the user. BISO should then create the user automatically and synchronize them on every login (identity, groups, roles/permissions), so that no separate BISO login screen and no manual user administration are needed. The behavior is implemented as a general framework and is specialized per customer via a delegate class.

Flow

  1. Apache mod_auth_openidc completes the OIDC handshake (see docker/dev/apache-000-oidc.conf) and passes the user's claims to PHP as $_SERVER variables (OIDC_CLAIM_*, after internal redirects also REDIRECT_OIDC_CLAIM_*) together with the access token (OIDC_access_token).
  2. On the first BISO request without a session, AuthService::authCheck() calls remoteUserAuth() (as soon as oidc.auto_provision = true – or remote_user_auth = true for the separate REMOTE_USER/Kerberos path).
  3. If oidc.auto_provision = true, then:
    • an OidcClaims object is built from $_SERVER,
    • the DI container provides the OidcUserProvisioner instance — the BisoDIProvider selects the customer-specific subclass based on the kunde param (e.g. GeOidcUserProvisioner), otherwise the base class,
    • provision($claims) is called. If oidc.userinfo_endpoint is set, this method first enriches the claims via the userinfo endpoint (roles + clean identity), then finds or creates Login + Benutzer (user) and maps the roles to BISO groups/flags,
    • the session is established via setupSession() (or setupSessionPartial() if the login has several users/seats – in that case the SPA shows the existing multi-user selection window).
  4. The SPA then loads directly into the main panel with SESSION.benutzer populated – without a separate login screen.

Core Classes and Hooks

Class Responsibility
backend/logic/OidcClaims.php Normalizes the OIDC_CLAIM_* variables; get()/getList(); accessToken() returns the bearer token; merge() adds userinfo claims.
backend/logic/OidcUserInfoClient.php Small Guzzle client: GET userinfo with Authorization: Bearer <token>, JSON parsing.
backend/logic/OidcUserProvisioner.php Base delegate with the generic find-or-create/sync flow and overridable hooks.
backend/service/AuthService.php remoteUserAuth() – entry point, session setup.

Overridable hooks on OidcUserProvisioner (per customer):

The "Seat" Concept (One User per Role/Service)

In BISO, a Login can own several Benutzer (users). The framework uses this for the concept of a seat: for each role/service, a dedicated user with its own institution, its own groups and default values can be created.

Roles via the userinfo Endpoint

Some IdPs do not deliver the roles in the front-channel claims. In this case enrichClaimsFromUserInfo() fetches them via HTTP:

Error policy: If the userinfo call fails, an existing user is left untouched (the login succeeds with the current permissions – never "wipe permissions" during an outage), while a new login is rejected (roles cannot be determined).

Synchronization and Deactivation

On every login (when oidc.sync_existing = true):

oidc.sync_existing = false means "create only": existing users are no longer modified after the first login. New users are always fully provisioned.

Customer-Specific Extension

  1. Param::set('kunde', 'xy').
  2. Create backend/logic/XyOidcUserProvisioner.php:
    class XyOidcUserProvisioner extends OidcUserProvisioner {
        protected function mapRolesToGroups(OidcClaims $claims): array { /* ... */ }
        protected function mapRolesToFlags(OidcClaims $claims): array { /* ... */ }
    }
    
  3. Add a case for the customer in the BisoDIProvider:
    case Param::KUNDE_XY:
        return new XyOidcUserProvisioner();
    
  4. In this installation's config.php, set oidc.auto_provision = true (and oidc.userinfo_endpoint if needed).

A complete example is the Geneva implementation, see Canton of Geneva: OIDC/GINA User Provisioning.

Configuration (General)

In the config.php of the respective installation:

Config::set('oidc.auto_provision', true);  // enables OIDC auto-provisioning (find-or-create from the claims)
Config::set('oidc.sync_existing', true);   // re-synchronize existing users on every login
Config::set('oidc.debug_claims', false);   // log the normalized claims (dev/test only)
Config::set('oidc.roles_claim', 'roles');  // name of the claim/userinfo field containing the roles

// Optional: fetch roles from a userinfo endpoint (if not in the claims):
Config::set('oidc.userinfo_endpoint', ''); // e.g. 'https://.../oauth2/userinfo'; empty = disabled

oidc.auto_provision = true is sufficient to enable the provisioning path. remote_user_auth belongs to a separate SSO mechanism (REMOTE_USER/Kerberos via config remote_user) and is not required for OIDC.

The active customer is selected as usual via Param::set('kunde', '<kunde>'); based on this, the BisoDIProvider provides the matching OidcUserProvisioner instance.

Logging

The provisioning logs to a dedicated oidc log channel (start, created Login/Benutzer, sync vs. create-only, mapped groups/flags, errors, established session). Use a dedicated file via an oidc entry in gaia.logging (config.php):

'oidc' => [
    'type' => 'file',
    'level' => 'DEBUG',
    'logfile' => Config::get('tmpdir') . "/biso-oidc.log",
],

If the oidc channel is missing, the messages automatically end up in the main log.

See also: Login, Benutzer, Berater, Institutionen, Mitarbeiter and Checking Roles.