OIDC login (OpenID Connect)¶
This page is optional: the local accounts are enough to start. Use it to let people log in to the web interface with your identity provider.
The controller can delegate the login to the interface to an OpenID Connect identity provider
(Keycloak, Microsoft Entra ID, Okta, Authentik, Dex...). The login page then shows a
Log in with OIDC button above the form of the local accounts, which
stays available (initial admin account, fallback accounts).
The flow is authorization code with PKCE (S256), state and nonce:
GET /api/v1/auth/oidc/loginredirects the browser to the provider;- the provider sends back to
GET /api/v1/auth/oidc/callback; the controller exchanges the code (server-to-server call), checks the ID token (signature by the JWKS keys of the provider, issuer, audience, expiry, nonce), finds the OIDC account of the user (or creates it) and records the groups named by its groups claim; - it hands the interface a signed session token, valid
SECREVIEW_SESSION_HOURShours, that the tab uses like a Bearer token.
Declaring the client at the provider¶
Create a confidential OIDC client (or a public one, without a secret: PKCE is enough) with:
- Redirect URL:
https://<controller>/api/v1/auth/oidc/callback, the exact public URL seen by the browser (behind a reverse proxy, the proxy's); - a claim holding the groups or roles of the user in the ID token (or, failing that, in the userinfo answer, which the controller reads when the ID token does not contain it).
Keycloak example (realm roles):
SECREVIEW_OIDC_ENABLED=true
SECREVIEW_OIDC_ISSUER=https://sso.example.com/realms/acme
SECREVIEW_OIDC_CLIENT_ID=security-review
SECREVIEW_OIDC_CLIENT_SECRET=... # empty for a public client
SECREVIEW_OIDC_REDIRECT_URI=https://secreview.example.com/api/v1/auth/oidc/callback
SECREVIEW_OIDC_ROLES_CLAIM=realm_access.roles # "groups" by default
SECREVIEW_OIDC_ADMIN_GROUPS=secreview-admins # created with the admin role (first administrator)
SECREVIEW_OIDC_AUTO_CREATE_USERS=true # otherwise, create every OIDC account in Utilisateurs
SECREVIEW_SECRET_KEY=$(openssl rand -base64 48)
SECREVIEW_OIDC_DISPLAY_NAME=Keycloak
The issuer must use HTTPS (HTTP is only accepted on localhost, for development); its
/.well-known/openid-configuration document must announce the same issuer. If the provider
uses an internal CA, give it with SECREVIEW_OIDC_CAFILE. The controller reaches the provider
through its outgoing proxy (HTTPS_PROXY / NO_PROXY, or the Outgoing proxy screen).
Configuring OIDC in the interface¶
The same settings can be changed in the OIDC screen (users.manage permission: the OIDC
settings decide who joins which group), without restarting the controller. As for
the Outgoing proxy screen, a SECREVIEW_OIDC_* variable that is set always takes
precedence, field by field; a value stored in the database applies only to the fields whose
variable is not set:
- a field whose variable is set has the Variable badge (with a lock): it shows the variable and cannot be changed; a value stored before the variable was set is marked value set here ignored, with a Delete link;
- otherwise the field has the Web interface badge (stored value; Reset deletes it and the default applies again) or Default;
- the client secret is never shown again: left empty, the current secret is kept; Delete stores an empty secret (public client, PKCE only);
- Test the connection contacts the provider with the values of the
form, without storing them: reading of the
/.well-known/openid-configurationdocument (sameissuer, HTTPS endpoints), then of the signing keys (JWKS); - Save refuses (
422) a configuration that would enable an invalid login (missing or non-HTTPS issuer, scopes withoutopenid...): nothing is stored then. An incomplete configuration can be prepared while OIDC stays disabled.
The change applies to the next login, on every replica (the controller reads the configuration
again at every login and rebuilds the OIDC flow when it changes). Sessions already open stay
valid until they expire. Still variables only: SECREVIEW_OIDC_CAFILE (a file of the
deployment) and the session settings (SECREVIEW_SECRET_KEY, SECREVIEW_SESSION_HOURS).
If a variable changed later makes the combination with the stored values invalid, the
controller starts anyway: the OIDC login is disabled and the error logged, to be fixed in the
screen or in the variables.
Through the API (/api/v1/oidc/settings, users.manage permission): GET describes every setting (stored
value or null, env, env_set: the variable is set and takes precedence, effective; a
set secret reads ***), PUT changes the sent fields (null deletes the stored value, a
missing field is unchanged, a field whose variable is set is refused with 422) and
POST /api/v1/oidc/settings/test tests the provider without storing anything:
curl -X PUT https://secreview.example.com/api/v1/oidc/settings \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-d '{"enabled": true, "issuer": "https://sso.example.com/realms/acme",
"client_id": "security-review", "client_secret": "...",
"redirect_uri": "https://secreview.example.com/api/v1/auth/oidc/callback",
"display_name": null}'
OIDC accounts¶
Every OIDC user is an account of the Users screen (OIDC badge), identified by the
issuer and the sub claim bound at its first login, never by a name. It is named at its
creation by the value of the SECREVIEW_OIDC_USERNAME_CLAIM claim (preferred_username by default),
compared exactly, case included. There is no fallback to email or sub, and a value that is
not a valid user name (control or invisible character, space, more than 64 characters) is
refused.
At login, the controller first looks for the account bound to the issuer and the sub of the
login: it opens, whatever the username claim says now (a name changed at the provider keeps the
account and its user name). Only an identity bound to no account is looked up by its user name:
| Situation | Result |
|---|---|
Account bound to this issuer and sub |
Login, with the permissions of its groups (below), even if the username claim changed |
| OIDC account of this name, not bound yet (created by an administrator) | The account is bound to the issuer and sub, then login |
No account, SECREVIEW_OIDC_AUTO_CREATE_USERS=true |
The account is created (created_by: oidc) if its groups give it a permission; otherwise refused, without creation |
No account, SECREVIEW_OIDC_AUTO_CREATE_USERS=false (default) |
Refused: an administrator must first create the OIDC account |
| Local account with the same name | Refused: an OIDC login never opens a local account |
OIDC account bound to another sub or another issuer |
Refused (see below) |
| OIDC account deleted by an administrator | Refused, even with the automatic creation (see below) |
At the first login, the account is bound to the issuer and to the sub claim of the
provider: the account is identified by both, and no two accounts share them. A later login
with the same user name but another sub (provider account deleted then created again...) or
from another issuer (another identity provider, which could reuse the same sub) is refused.
An account bound when only the sub was recorded gets the issuer of its next login. To give
the user name to someone else, delete then create the account again.
Before version 0023 of the schema, a name changed at the provider created a second account bound to the same identity. The upgrade keeps the identity on the account that logged in last; the others keep their name, groups and history but can no longer log in: delete them in Users.
What an OIDC account may do comes from its groups: those an administrator gave it (Users or Groups screen) and the groups open to OIDC named by its groups claim, recorded at every login (see groups of the identity provider). An account without any permission is refused and its open sessions are closed. A change of its given groups applies at once; a change of its groups at the provider, at the next login.
Deleting an OIDC account (Utilisateurs screen, DELETE /api/v1/users/{id} or
security-review-controller user delete NAME) closes its sessions at once and revokes its
user name and its identity (issuer and sub): an OIDC login no longer creates it again, under
that name or another one (name changed at the provider), even with the automatic creation and
groups that give it a permission ("compte supprimé" message, account deleted, on the login page). To give it
access back, an administrator creates the account again, or lifts the revocation (Autoriser à nouveau, Allow again,
DELETE /api/v1/users/revoked/{username}): the account is then created again at its next
login. GET /api/v1/users/revoked lists the revoked accounts, with the author and the date of
the deletion.
The last holder of the users.manage permission
can be neither deleted nor lose it. Preferably keep a local administrator: if the identity
provider becomes unreachable, it remains the only access, besides the CLI.
INFO security_review_mcp.web: OIDC web user alice created at its first login
INFO security_review_mcp.web: OIDC login of oidc:alice with role reader through 1 OIDC group(s)
WARNING security_review_mcp.web: OIDC login of oidc:bob refused: no web user bob
INFO security_review_mcp.web: Web user carol (oidc) deleted by admin
WARNING security_review_mcp.web: OIDC login of oidc:carol refused: web user carol was deleted
Groups of the identity provider¶
The values of the SECREVIEW_OIDC_ROLES_CLAIM claim (list or string, dotted path accepted) are
compared with the names of the groups of the Groups screen: at each login, the user joins
every group open to OIDC whose name is exactly one of these values (case included), and
leaves the OIDC groups it no longer has. The other values are ignored: create a group of the same
name to use one. The roles of the group, and their projects, then apply (see
Roles, groups and permissions).
| Setting | Effect |
|---|---|
SECREVIEW_OIDC_ADMIN_GROUPS |
Comma-separated groups that hold the admin role: such a group, missing at the login of one of its members, is created open to OIDC with the admin role. An existing group is left as administrators changed it. Used to name the first administrator |
SECREVIEW_OIDC_DEFAULT_GROUP |
Name of a group every OIDC user joins at each login, whatever its claim (empty by default: none). A missing group gives nothing |
A group not open to OIDC (for example administrators) is never joined through the claim, even when a value has its name. A restricted session behaves
like a token restricted to a project: the other
projects, their reports and their findings are handled as missing.
Example: with the claim ["payments-team", "staff"], a group payments-team open to OIDC with the
triager role on billing, and no staff group, the user triages billing only.
The label of an OIDC session is oidc:<username>: it is the one that appears in the logs and
in the triage history.
Project read access¶
The approval of an enrollment creates the project
with its project group project:<project>, without any
member, and never a token nor an account. To let a developer read the project (and their personal MCP token
reach it), an administrator adds their account to that group, from the Groups screen (or
Users). With OIDC, the account exists once the developer has logged in (with
SECREVIEW_OIDC_AUTO_CREATE_USERS=true), or an administrator creates it beforehand in Users.
The access then shows in the groups of the account, where it is changed like the others.
Upgrading from the named accesses
Previous versions stored these accesses apart ("named accesses",
/api/v1/oidc/user-grants API). Schema migration 0006 turns them into reader OIDC
accounts on the same projects, then deletes the table. An access whose name is not a valid
user name or that bears the name of a local account is dropped, with a warning in the log of
the migration.
OIDC only¶
With SECREVIEW_OIDC_ONLY=true, the identity provider becomes the only way in: the login page
only offers its button, and POST /api/v1/auth/login refuses the user name and password of the
local accounts (403, logged). The sessions already open stay valid until they expire.
SECREVIEW_OIDC_ENABLED=true
SECREVIEW_OIDC_ONLY=true
It applies only while OIDC works: with OIDC disabled, or with settings refused at their check,
the local accounts log in again, so that nobody is locked out. SECREVIEW_OIDC_ONLY is a
variable only, never changed in the interface: if the provider is unavailable, unset it and
restart the controller, then log in with a local account (its password can be set with
security-review-controller user reset-password).
Sessions¶
- The session token is signed (HMAC-SHA256) with a key derived from
SECREVIEW_SECRET_KEY(at least 32 characters), like the one of the local accounts, and names the OIDC account, read again at every request: deleting the account or changing its groups or their roles applies at once. Every replica of the controller must share this secret; without it, a random secret is drawn at every start and sessions end at restart. - A group change at the provider only applies at the next login. To cut every session before it
expires, delete the account, or change
SECREVIEW_SECRET_KEYand restart the controller (every session is cut). - Log out clears the session of the tab, not the one of the identity provider.
On failure, the login page gives the cause (access denied at the provider, no permission, OIDC account not created yet or deleted, user name taken by a local account or bound to another account, missing user name, expired state, invalid token, unreachable provider); the detail is logged by the controller, without code or token.