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.
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.
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).AuthService::authCheck() calls
remoteUserAuth() (as soon as oidc.auto_provision = true – or remote_user_auth = true
for the separate REMOTE_USER/Kerberos path).oidc.auto_provision = true, then:OidcClaims object is built from $_SERVER,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,setupSession() (or setupSessionPartial() if the
login has several users/seats – in that case the SPA shows the existing
multi-user selection window).SESSION.benutzer populated –
without a separate login screen.| 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):
extractUsername() – which claim becomes login.benutzername (the sub claim).findLogin() / createLogin() – find/create the login.enrichClaimsFromUserInfo() – userinfo call + merge (generic, enabled via config).provisionSeats() – creates the user "seats" (default: exactly one user).createBenutzer() / syncBenutzer() – create the user / sync the identity (Person).syncLogin() – sync login data (default: nothing).mapRolesToGroups() / mapRolesToFlags() – roles → groups/flags.findOrCreatePerson() / syncPersonIdentity() – find/create the linked Person
and keep its last name/first name/e-mail up to date.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.
setupSessionPartial()); the SPA shows
the existing multi-user selection window in which the user picks the seat
(distinguished, among other things, by the institution). No additional custom UI
is needed.Some IdPs do not deliver the roles in the front-channel claims. In this case
enrichClaimsFromUserInfo() fetches them via HTTP:
oidc.userinfo_endpoint with the user's bearer token. This token is reused
from the OIDC_access_token provided by Apache (OidcClaims::accessToken()), i.e.
no additional client secret is needed.roles) is merged into the claims; the roles end up under
oidc.roles_claim.oidc.userinfo_endpoint is empty, the call is skipped and the roles are read from
the claim oidc.roles_claim.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).
On every login (when oidc.sync_existing = true):
Benutzer is deactivated (aktiv=false) – it no longer grants access, but the
data is preserved.Login record.oidc.sync_existing = false means "create only": existing users are no longer modified
after the first login. New users are always fully provisioned.
Param::set('kunde', 'xy').backend/logic/XyOidcUserProvisioner.php:class XyOidcUserProvisioner extends OidcUserProvisioner {
protected function mapRolesToGroups(OidcClaims $claims): array { /* ... */ }
protected function mapRolesToFlags(OidcClaims $claims): array { /* ... */ }
}
case for the customer in the BisoDIProvider:case Param::KUNDE_XY:
return new XyOidcUserProvisioner();
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.
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.
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.