# Single Sign-On

> Federated sign-in over OIDC — turning it on, auto-provisioning, linking and unlinking your own account, and the guardrails that keep root a local-only account.

- Updated: 2026-09-25
- Source: https://contextator.com/en/docs/sso/
- Language: en-US
- Author: Muhammet Şafak

---
Federated sign-in over OIDC, as a second door beside the password form — never a replacement for it.

## Turning it on

Setting `OIDC_ISSUER_URL` is the on/off switch. Left empty, no discovery request is ever made and no
button renders on `/login`, though the SSO routes stay registered (`GET /api/auth/oidc/login` answers
`404`, and the callback redirects to `/login?oidc_error=not_configured`). Set it, and discovery is
fetched once and cached.

| Variable | Default | Notes |
|----------|---------|-------|
| `OIDC_ISSUER_URL` | – | The provider's issuer URL. On/off switch for the whole feature |
| `OIDC_CLIENT_ID` / `OIDC_CLIENT_SECRET` | – | Issued by the provider. `OIDC_CLIENT_SECRET` is a process secret like `ADMIN_TOKEN` — read from the environment, never written to the database, so no backup or dump ever carries it |
| `OIDC_REDIRECT_URI` | – | The callback URL registered with the provider, e.g. `https://docs.example.com/api/auth/oidc/callback` |
| `OIDC_SCOPES` | `openid profile email` | Space-separated scopes requested at the provider |
| `OIDC_BUTTON_LABEL` | `Single sign-on` | Text on the `/login` SSO button |
| `OIDC_AUTO_PROVISION` | `0` | `1` lets a first successful provider sign-in create an account here on its own. Off by default: an unmapped provider identity is refused rather than silently handed an account |
| `OIDC_DEFAULT_ROLE` | `member` | Role a provisioned account gets. `admin` or `member` only — never `root` |

There is nothing to configure from the dashboard for the provider itself; these variables are set by the
operator in the instance's environment. See [Configuration](/en/docs/configuration/) for how settings are
applied.

## Signing in

A federated sign-in is always matched to an account by the provider's `sub` claim, never its e-mail
claim — an e-mail is exactly what a misconfigured or compromised provider could forge into reaching a
different local account. Changing your e-mail at the provider does not disconnect you and cannot
accidentally connect you to someone else's account.

Whether a new account can be created this way at all is the operator's choice
(`OIDC_AUTO_PROVISION`), off by default. With it off, signing in through the provider is refused unless
an admin already created the account or you have linked it yourself (below). Turn it on and a first
successful sign-in mints an account with `OIDC_DEFAULT_ROLE`.

Once signed in, a federated account is governed by the same [role table](/en/docs/accounts-and-permissions/)
as any other account — nothing about what you can reach depends on how you signed in — and a provider
outage never blocks local password sign-in. The two doors do not depend on each other.

## Linking or unlinking your own account

Once signed in — by password or by the provider — every non-root account can connect or disconnect its
own federated identity from its own account page, under **Single sign-on**. Connecting starts the
identical provider redirect the login button uses, but links the identity it comes back with to the
*current* session instead of signing in to whichever account already owns it. Linking a provider
identity that is already linked to a different account is refused, not merged — each identity belongs to
exactly one account.

**Unlinking is self-service and revokes the account's standing credentials in the same request:** every
session and every API token that account holds is invalidated the moment the identity is removed —
including the session making the request — because unlinking is also what clears the way for a later
promotion to root, and nothing opened while the account was still provably tied to an external identity
provider should outlive that link. The same transaction revokes the account's **MCP OAuth credentials** —
every access and refresh token it was issued for `/mcp/<project>` — so an MCP client signed in as that
account gets `401` on its next request and has to authorize again; other accounts' credentials are
untouched. The audit event of the unlink records how many were revoked, as `detail.revokedMcpCredentials`.
Sign back in locally, since the link is gone, to get a working session again.

If the account has no local password and this link is its only way to sign in, unlinking is refused
(`409 last_sign_in_method`) — nothing removed, nothing revoked — until an admin sets a password for it
first with `POST /api/users/:id/password`.

## root stays local, always

`root` cannot link a provider identity in the first place — the control never appears for it in the
dashboard, and the API refuses the attempt directly (`403 root_local_only`). The reverse direction is
guarded too: granting `root` to an account that still has a linked identity is refused
(`409 root_requires_unlink`), on the Users page and at the API, checked ahead of the write — so the
account's role never actually becomes `root` while the link exists. As a second line of defence, a
session's role and sign-in method are re-read on every request; if an account somehow did reach `root`
while still linked, a session opened over SSO for it would be signed out on its very next request rather
than allowed to act as root. Only after unlinking can another root account grant the `root` role.

This is why the one account this product cannot recreate from a provider claim — `root` — never signs in
over SSO.

## The Admin API

Linking and unlinking are also two ordinary endpoints, callable from a script by a signed-in caller's own
session — see [Admin API](/en/docs/admin-api/#single-sign-on).
