This page describes the Geneva implementation of the OIDC auto-provisioning. The
general framework is documented at
OIDC Auto-Provisioning (Single Sign-On).
Implemented in backend/logic/GeOidcUserProvisioner.php.
The identity provider GINA authenticates the Geneva users. However, GINA delivers to BISO only the list of application roles (no fine-grained permissions) – and not in the front-channel claims, but via a userinfo endpoint. BISO fetches the roles there and maps them to fully configured BISO users.
The login/userinfo provides:
sub → login.benutzername (e.g. PIERRER; read from the front-channel claim)firstName, lastName, email → linked Person (from the userinfo response,
therefore cleanly UTF-8 encoded)roles → e.g. DIP.BISO.ADMINISTRATEUR-APPLICATIONThe roles are mapped as full strings (including the DIP.BISO. prefix); the config
values accordingly carry the full role names. The generic role DIP.BISO.UTILISATEUR
is ignored (on its own it grants no access).
Example of a userinfo response:
{
"sub": "BRESSONJ",
"firstName": "Julien",
"lastName": "Bresson",
"email": "julien.bresson@etat.ge.ch",
"roles": [
"DIP.BISO.ADMINISTRATEUR-APPLICATION",
"DIP.BISO.GESTIONNAIRE-SECRETAIRE",
"DIP.BISO.UTILISATEUR"
]
}
The French form labels of the BISO user form correspond to the following user fields (in Geneva, Regionalstelle is labeled Prestation):
| Form Label (FR) | User Field |
|---|---|
| Personne | person_id (linked Person: last name/first name/e-mail) |
| Prestation | institution_id (an institution) |
| Groupe comptes rendus | protokoll_gruppe_id (a group) |
| Groupe Propriétaire | owner_group_id (a group) |
| Autorisation du groupe = Écrire | owner_group_initial_mode = 6 (write) |
| Autorisation d'accès par d'autres = Lire | world_initial_mode = 4 (read) |
| Langue = Français | sprache_id = French |
| Conseiller/ère | ist_berater |
| Secrétariat | sekretariat |
| Chef d'atelier | workshopleiter |
| Planificateur d'atelier | workshopplaner |
| Modifier des modules et des prestations | produkte |
| Modifier des personnes et des institutions | stammdaten |
| Montrer Meta-Infos | show_meta_infos |
| Créer des utilisateurs | darf_benutzer_anlegen |
Common to all profiles: language = French, owner_group_initial_mode = 6 (write),
world_initial_mode = 4 (read), linked Person. In addition, for each seat the person
is recorded as an active employee (Mitarbeiter) of the prestation/institution
(find-or-create, deactivated ones are reactivated) — the person field in the user form
only shows employees of the institution.
| Profile (Application Role) | Users/Seats | Groups | Roles/Permissions | Prestation / Groupe comptes rendus / Groupe Propriétaire |
|---|---|---|---|---|
ADMINISTRATEUR-APPLICATION |
1 | all groups | all role flags; all permissions except darf_benutzer_anlegen |
OSP |
GESTIONNAIRE-SECRETAIRE |
1 | OSP + the 8 service groups | Conseiller, Secrétariat, Chef d'atelier, Planificateur; permissions produkte/stammdaten/show_meta_infos | OSP |
GESTIONNAIRE-CONSEILLER |
one user per service | OSP + service group | Conseiller, Chef d'atelier, Planificateur (no Secrétariat); permissions produkte/stammdaten/show_meta_infos | the respective service |
Combination: The profiles are additive — one OSP seat plus one Conseiller seat per
service. ADMINISTRATEUR-APPLICATION and GESTIONNAIRE-SECRETAIRE share the OSP seat
(both point to the OSP institution; the admin profile is the superset and wins there).
A GESTIONNAIRE-CONSEILLER with several PRESTATION-* roles gets a dedicated user
(seat) per service on the same login. On login, BISO then shows the existing
multi-user selection window in which the user picks the desired service
(distinguished by the institution/prestation). See
Seat Concept.
On every login (when oidc.sync_existing = true):
syncBenutzer / applySeat).Person are updated
(the already linked person is reused, no duplicate).aktiv=false); it no longer grants access, but its data is preserved.UTILISATEUR remains) →
all seats are deactivated and the login is rejected; no active user remains,
only the Login record.mod_auth_openidc must perform the handshake and pass the access token on to PHP
(so that OIDC_access_token is available for the userinfo call):
OIDCPassClaimsAs both
OIDCPassAccessToken On
Template: docker/dev/apache-000-oidc.conf.
config.php – BasicsConfig::set('oidc.auto_provision', true); // enables the provisioning (remote_user_auth not needed)
Config::set('oidc.sync_existing', true);
Config::set('oidc.userinfo_endpoint', 'https://ssorec.geneveid.ch/ginasso/oauth2/userinfo');
Config::set('oidc.roles_claim', 'roles');
config.php – Group/Institution Mapping (by ID)Roles are mapped to BISO groups and institutions by ID. The IDs come from the GE production database. Every service and OSP must exist there both as a group and as an institution.
The IDs required for this are produced by the following two queries. The
anzahl_benutzer column shows how many users reference the entry — this helps to
identify the entries actually in use. Both queries are also available as runnable scripts
in the sql/ folder.
Groups (for osp_gruppe_id and the per-service gruppe_id):
SELECT g.id, g.name, count(bg.benutzer_id) AS anzahl_benutzer
FROM gruppe g
LEFT JOIN benutzer_gruppe bg ON bg.gruppe_id = g.id
GROUP BY g.id, g.name
ORDER BY g.name;
Institutions / prestations (for osp_institution_id and the per-service
institution_id; the ist_bsb filter narrows down to the OSP prestations — without it,
all schools appear as well):
SELECT i.id, i.name, i.aktiv,
count(b.id) FILTER (WHERE b.deleted IS NULL AND b.aktiv) AS anzahl_benutzer
FROM v_institution i
LEFT JOIN benutzer b ON b.institution_id = i.id
WHERE i.ist_bsb
GROUP BY i.id, i.name, i.aktiv
ORDER BY i.name;
Config::set('oidc.ge.osp_gruppe_id', 0); // OSP: group
Config::set('oidc.ge.osp_institution_id', 0); // OSP: institution (prestation)
Config::set('oidc.ge.services', [
// '<full ROLE, UPPERCASE>' => ['gruppe_id' => <int>, 'institution_id' => <int>],
'DIP.BISO.PRESTATION-CYCLE-D-ORIENTATION' => ['gruppe_id' => 0, 'institution_id' => 0],
'DIP.BISO.PRESTATION-ACCESSII' => ['gruppe_id' => 0, 'institution_id' => 0],
'DIP.BISO.PRESTATION-PREQUALIFIANT' => ['gruppe_id' => 0, 'institution_id' => 0],
'DIP.BISO.PRESTATION-CFP-COMMERCE' => ['gruppe_id' => 0, 'institution_id' => 0],
'DIP.BISO.PRESTATION-ECG' => ['gruppe_id' => 0, 'institution_id' => 0],
'DIP.BISO.PRESTATION-COLLEGE' => ['gruppe_id' => 0, 'institution_id' => 0],
'DIP.BISO.ENSEIGNEMENT-SPECIALISE' => ['gruppe_id' => 0, 'institution_id' => 0],
'DIP.BISO.PRESTATION-ETUDIANTS' => ['gruppe_id' => 0, 'institution_id' => 0],
'DIP.BISO.PRESTATION-TOUT-PUBLIC' => ['gruppe_id' => 0, 'institution_id' => 0],
'DIP.BISO.PRESTATION-VIAMIA' => ['gruppe_id' => 0, 'institution_id' => 0],
'DIP.BISO.PRESTATION-CHAMP-DOLLON' => ['gruppe_id' => 0, 'institution_id' => 0],
'DIP.BISO.PRESTATION-FEMME-ET-EMPLOI' => ['gruppe_id' => 0, 'institution_id' => 0],
'DIP.BISO.PRESTATION-TREMPLIN-JEUNES' => ['gruppe_id' => 0, 'institution_id' => 0],
'DIP.BISO.PRESTATION-EVASCOL' => ['gruppe_id' => 0, 'institution_id' => 0],
'DIP.BISO.PRESTATION-PRO-APPRENTIS' => ['gruppe_id' => 0, 'institution_id' => 0],
]);
// The 8 service roles whose groups the SECRETAIRE profile is a member of:
Config::set('oidc.ge.secretaire_service_roles', [
'DIP.BISO.PRESTATION-ETUDIANTS', 'DIP.BISO.PRESTATION-TOUT-PUBLIC', 'DIP.BISO.PRESTATION-VIAMIA',
'DIP.BISO.PRESTATION-CHAMP-DOLLON', 'DIP.BISO.PRESTATION-FEMME-ET-EMPLOI',
'DIP.BISO.PRESTATION-TREMPLIN-JEUNES', 'DIP.BISO.PRESTATION-EVASCOL', 'DIP.BISO.PRESTATION-PRO-APPRENTIS',
]);
Dedicated oidc log channel as described in the general framework, see
OIDC Auto-Provisioning – Logging.
Users can be created up front from the GINA role list, without every person having to log in once first:
php backend/tools/ge_oidc_user_import.php <file.xlsx> # dry run (default)
php backend/tools/ge_oidc_user_import.php <file.xlsx> --do-it # writes
Login name: by default the column Identifiant becomes login.benutzername; use
--login-column=<header> to change it. What matters is that the column carries the same
value GINA sends as sub — and that is the Identifiant (BRESSONJ), not the email
address. If the column misses the sub, the imported user is not recognized at the first
SSO login and a second account is created. To verify, the dry run reports for both the
chosen and the alternative column how many values already exist as logins; the column with
the overlap is the right one.
Letter case: nothing in the login path normalizes it — extractUsername() only trims,
findLogin() compares exactly, and the unique index on login.benutzername is
case-sensitive. The login name must therefore match the IdP's sub character for
character. The script warns, naming the row, when a value differs from an existing login
only in case — that would create a second account. Nothing is corrected automatically:
letter case belongs in the source sheet.
The script does not map the roles itself: it builds the claims GINA would deliver from
each row and hands them to the existing GeOidcUserProvisioner. Seats, groups, flags and
the linked Person / Mitarbeiter are therefore created exactly as they would be by an SSO
login. The userinfo call is switched off for the run (the roles come from the file) and
oidc.sync_existing is forced on.
Expected file layout (sheet Collaborateurs-GINA-BISO): a band row with the cells GINA
and — further right — BISO, below it a header row with Identifiant, Nom, Prénom,
Adresse email. Every column between GINA and BISO is a role and must match a profile
role or a key of oidc.ge.services. The marker is x. The optional column
Nb de sous-profils attendus dans BISO is checked against the computed seat count.
Before writing anything the script validates the whole file and aborts on any problem:
unknown role column (with a suggestion), duplicate Identifiant, missing identity, row
without a profile role. A service configured but missing a column only yields a warning.
For every person the run reports the detected profile and service roles, the resulting
Prestations (id and name) and whether an OSP seat is created. An OSP seat comes only
from ADMINISTRATEUR-APPLICATION or GESTIONNAIRE-SECRETAIRE; GESTIONNAIRE-CONSEILLER
grants the OSP group, but no OSP Prestation.
Because the import has to produce the same result as a real GINA login, the script also
warns when a row carries ADMINISTRATEUR-APPLICATION but not all service columns are
marked. GINA assigns all service roles to an administrator; if the same row also
carries GESTIONNAIRE-CONSEILLER, the first SSO login therefore creates one counsellor
account per service. The warning names both numbers.
The dry run wraps everything in one transaction that is rolled back at the end. With
--do-it each person is committed individually — a failure on one person leaves the
already processed ones in place. A second run creates nothing new, it only re-synchronizes.
Users not in the list are never touched.
This requires a complete configuration: oidc.ge.services must contain all services, and
groups and institutions must exist (see sql/).
Profile roles: ADMINISTRATEUR-APPLICATION, GESTIONNAIRE-SECRETAIRE,
GESTIONNAIRE-CONSEILLER (each with the prefix DIP.BISO.).
Service roles (PRESTATION-*): PRESTATION-CYCLE-D-ORIENTATION,
PRESTATION-ACCESSII, PRESTATION-PREQUALIFIANT, PRESTATION-CFP-COMMERCE,
PRESTATION-ECG, PRESTATION-COLLEGE, ENSEIGNEMENT-SPECIALISE,
PRESTATION-ETUDIANTS, PRESTATION-TOUT-PUBLIC, PRESTATION-VIAMIA,
PRESTATION-CHAMP-DOLLON, PRESTATION-FEMME-ET-EMPLOI, PRESTATION-TREMPLIN-JEUNES,
PRESTATION-EVASCOL, PRESTATION-PRO-APPRENTIS.
oidc.debug_claims = true: the normalized claims and the raw OIDC_* server
variables are logged to the oidc channel (dev/test only; token values are masked).biso-oidc.log: provisioning start, fetched roles, created/updated seats,
deactivations, errors.oidc.ge.services mapping – without a recognized role,
all seats are deactivated and access is denied.