Single sign-on#
Wheelhouse can authenticate people against an OIDC identity provider. When it is configured,
the login screen offers a Continue with <label> button beside the password form —
disabled, with the reason printed, when discovery is not answering, the Account page grows
a Single sign-on panel, and the Users page gains a field for the provider's subject.
The provider decides who somebody is; Wheelhouse decides what they may do. This page covers
the whole of it: what the UI manages, what only a start-up flag can change, and the exact
order in which an assertion is turned into an account.
Password, two-factor, API token and break-glass logins all keep working when single sign-on is on. A router that can only be entered through an identity provider is one provider outage away from being unmanageable.
What the UI manages, and what it does not#
| Managed in the UI | Only by a flag on the agent |
|---|---|
| Linking an account to a subject, and unlinking it — Users and Account | The issuer, client id and client secret |
| Seeing whether the provider currently answers | Which claim becomes the username |
| Seeing who is linked, from the admin view | Which groups map to which role |
| Signing in, and signing out | Whether unknown identities are provisioned at all |
That split is deliberate: an agent whose provider settings could be edited through the UI
that the provider gates has a bootstrapping problem. The flags live on the systemd unit and
are validated at start-up, so a half-configured provider fails the agent rather than every
sign-in (agent/oidc.go validateOIDCFlags).
The flags#
| Flag | Default | What it does |
|---|---|---|
--oidc-issuer | — | The issuer URL. Must be an absolute http(s) URL, and needs a client id. |
--oidc-client-id | — | The client id. Needs an issuer. |
--oidc-client-secret-file | — | File holding the client secret. --oidc-client-secret exists and is visible in ps; prefer the file. |
--oidc-redirect-url | derived per request | The exact callback URL registered at the provider. Validated to end in /api/oidc/callback. |
--oidc-scopes | openid profile email groups | Requested scopes. openid is added if you leave it out. |
--oidc-username-claim | preferred_username | preferred_username, email, name or sub. |
--oidc-groups-claim | groups | Where group membership lives, for providers that nest it. |
--oidc-admin-groups | — | Comma-separated groups mapping to admin. Empty means no group mapping at all. |
--oidc-operator-groups | — | Comma-separated groups mapping to operator. |
--oidc-default-role | viewer | Role for a provisioned user outside every mapped group. |
--oidc-provision | true | Create a local account on the first sign-in of an unknown identity. |
--oidc-link-by-email | false | Link an identity to an existing account by email — and only when the provider asserts email_verified. |
--oidc-label | Authentik | The provider name shown on the login screen. |
--oidc-ca-file | — | A PEM root CA to trust for the issuer instead of the system trust store. |
--oidc-post-logout-redirect | — | A registered post-logout URI for provider-initiated logout. |
(agent/main.go registerFlags)
The sign-in flow#
GET /api/oidc/loginmints a state value and a PKCE verifier, remembers them for ten minutes, and redirects to the provider.- The provider returns the browser to
GET /api/oidc/callback. - The agent looks the state up and consumes it. An unknown or expired state is refused before anything is exchanged — that is what makes a callback honourable.
- The authorization code is exchanged with the PKCE verifier, the ID token is verified, and the nonce is checked against the one this attempt used.
- The claims are resolved to an account (below). The failure of any step redirects to the
login screen with a short reason in
sso_error; the provider's own error description is logged and never forwarded. - A session is created with
provider: "oidc", the subject, and the ID token — which is kept only so logout can offer provider-initiated logout, and lives exactly as long as the session does.
The sign-in is recorded in the audit log as login (oidc) with the issuer, the subject and
the groups the assertion carried.
How an identity becomes an account#
In this order (agent/oidc.go oidcResolveUser):
- By subject. An account whose
oidc_subjectmatches the assertion'ssubis the account. The subject is the only link Wheelhouse trusts by default: it is the provider's own immutable identifier, where an email address is a claim the provider could hand to somebody else. The lookup is case-sensitive, because a subject is opaque. - By verified email, if you turned it on. With
--oidc-link-by-email, an assertion carrying an email address andemail_verified: truelinks to the account holding that address, and the link is audited asoidc-link. An account that already has a different subject is refused. Off by default: an auto-link on an unverified address turns provider self-service into a Wheelhouse privilege path. - By provisioning, when
--oidc-provisionis on. A local account is created with the name derived from the claims, the mapped or default role, and no password — the provider is the credential, and an SSO deployment does not want to be nagged into inventing a second way in. Audited asoidc-provision. - Otherwise refused with this identity is not linked to a Wheelhouse account.
The derived username comes from the configured claim, falling back through the others,
truncated at the first @, reduced to letters, digits, dot, underscore and hyphen, and cut
to 48 characters.
Group-to-role mapping#
Mapping is opt-in: with neither --oidc-admin-groups nor --oidc-operator-groups set, the
assertion has no opinion about roles and an existing account keeps the role it has.
With either set, membership is matched case-insensitively, admin is checked before
operator so somebody in both lists is never downgraded by ordering, and an identity in
neither gets --oidc-default-role.
A mapped role is applied to the account on every sign-in, which means a change at the
provider takes effect at the next sign-in. Two guards apply
(agent/oidc.go oidcApplyRole):
- The last admin is never demoted by a mapping. The count and the change happen inside one transaction, and a mapping that would leave no admin is logged and ignored.
- A role change drops that account's sessions, exactly as it does when an admin makes it in the UI. The role a session carries can never be out of date.
Two-factor and single sign-on#
require_totp is not applied to a sign-on session. The second factor for these accounts
belongs at the provider, which is where a customer configures it; enrolling a competing
secret in Wheelhouse would be a second, weaker factor for the agent to store. The comment in
the callback names the honest alternative — requiring an MFA acr or amr claim — and says
it is not what the code does today.
Signing out#
POST /api/auth/logout drops the session and clears the cookie. When the session came
through the provider and the provider publishes an end_session_endpoint, the response also
carries a logout_url with the ID token as the hint, and the UI offers it. The
post-logout redirect is only added when --oidc-post-logout-redirect is set, because a
provider honours only a registered URI.
Signing out at Wheelhouse does not end the provider's session unless you follow that link, and revoking access at the provider does not end a Wheelhouse session already open — it takes effect at the next sign-in. The Account page says both.
Unlinking#
POST /api/auth/oidc/unlink detaches your own account. It answers:
404when the account is not linked;409when the account has no password of its own — set one before unlinking single sign-on, because unlinking the only credential deletes the only way in;- otherwise it clears the subject and records
oidc-unlink.
An admin can also clear somebody else's subject from the Users panel, which is audited
as oidc-unlink-user.
What an admin can see#
GET /api/admin/oidc (admin only) returns everything about the configuration except the
client secret: the issuer, client id, scopes, the username and groups claims, both group
lists, the default role, whether provisioning and email linking are on, the redirect URI,
whether discovery currently answers, the provider's end-session endpoint, and the list of
linked accounts with their subjects and roles. That last one is the question worth being
able to answer at a glance on an internet-facing router: who can come in through the
provider.
See also#
- Users — linking an account to a subject, and the refusals around it.
- Account — the panel a person sees, and unlinking their own account.
- Roles — what a mapped role actually grants.
- Accounts and sessions — the concept.
- Authenticating — the session a sign-on produces.
- Agent flags — every flag on this page, in one table.
Checked against#
agent/oidc.go,
agent/authhttp.go,
agent/admin.go,
agent/main.go,
ui/src/pages/Account.tsx,
ui/src/pages/Users.tsx,
docs/adr/002-oidc-client.md,
docs/security.md.