SSO IdP — single login for every app 🔒
Phase Г.2 of the GA roadmap. Quazzar’s Control Panel runs an embedded OpenID Connect / SAML identity provider (built on dex ) so users sign in once and reach every installed app — Plex, Nextcloud, Grafana, Vaultwarden, or any OIDC-aware service you ship.
Plan: OIDC clients require Orbit Pro. SAML upstream connectors require Enterprise. See Orbit Pro for the full tier matrix.
How it fits together
| Plane | Role | Lives in |
|---|---|---|
| Quazzar IdP | Issues OIDC tokens, optionally federates to upstream IdPs | CP internal/sso/dex_runner.go |
| Per-tenant issuer | https://<cp_host>/oidc/<tenant_id> | CP tenant_resolver.go |
| App OIDC client | One per installed app | CP client_repo.go |
| Auto-config | Renders the client into the app’s env / config | OS internal/sso/templates.go + per-app writers |
Enabling SSO on an app
- Install an app whose template carries an
sso_templateblock (Grafana, Nextcloud, Vaultwarden, Plex, or any template usingkind: generic_oidc_env). - Open Settings → SSO on the node, find the app card, click Enable.
- Quazzar OS calls
POST /api/sso/clientson the CP, receives a freshclient_id+client_secret+ the issuer URL, and the per-app writer renders them into the app’s compose env (and where needed a sidecar JSON config under<app_dir>/sso/). - The next time the app starts, your existing Quazzar login takes you straight in.
To pause, toggle Disable — the OS deletes the client from the CP and clears the SSO env on next recreate. The local writer output stays idempotent so re-enable is a no-op for the env block.
Federating to your existing IdP
Pro+ tenants can also flip the IdP into “broker” mode by adding an upstream connector in the CP Settings → SSO view. Supported kinds:
| Kind | Plan | Notes |
|---|---|---|
| Pro+ | OAuth — paste client_id + secret from Cloud Console. | |
| GitHub | Pro+ | OAuth — paste client_id + secret from GitHub Settings → Developer. |
| Generic OIDC | Pro+ | Any OIDC-compliant provider. |
| SAML 2.0 | Enterprise | Metadata URL or pasted SP descriptor. |
| LDAP / AD | Enterprise | Read-only bind for legacy directory services. |
Once a connector is enabled, the IdP login screen offers it as an option;
groups + role mapping flow through to every downstream app via the
configured group_claim.
Per-app cap
The cap is plan-based:
| Tier | App OIDC clients | Upstream connectors |
|---|---|---|
| Community | n/a — IdP locked | n/a |
| Pro | ≤ 5 | 1 |
| Business | ≤ 25 | 3 |
| Enterprise | unlimited | unlimited (SAML included) |
When you hit the cap the OS-side useFeature("sso_idp") hook surfaces
UpgradePrompt with the next plan up.
API surface
CP-side:
| Method + path | Purpose |
|---|---|
GET /api/sso/issuer | Announces the per-tenant issuer URL. |
GET /api/sso/clients | Lists OIDC clients (secrets stripped). |
POST /api/sso/clients | Provisions / refreshes a client. |
DELETE /api/sso/clients/{id} | Revokes the client. |
GET /api/sso/connectors | Lists upstream connectors. |
POST /api/sso/connectors | Adds an upstream connector. |
GET /oidc/{tenant_id}/.well-known/openid-configuration | Public OIDC discovery. |
OS-side:
| Method + path | Purpose |
|---|---|
GET /api/sso/state | Lists every app’s SSO state + plan headroom. |
GET /api/sso/apps/{app_id}/status | Returns one app’s state. |
POST /api/sso/apps/{app_id}/enable | Wires the app to the IdP. |
POST /api/sso/apps/{app_id}/disable | Tears it down. |
All OS paths return HTTP 402 with the canonical upgrade_required
body on community tenants — see Orbit Pro for the
shape.
Writing your own template
Add an sso_template block to your app’s YAML. Five built-in writers ship today:
sso_template:
kind: grafana # or nextcloud, vaultwarden, plex, generic_oidc_env
protocol: oidc
redirect_uris:
- "${app_url}/login/generic_oauth"
scopes: [openid, email, profile]
# Optional — only used by generic_oidc_env:
env_map:
OIDC_ISSUER: "${issuer}"
OIDC_CLIENT: "${client_id}"
OIDC_SECRET: "${client_secret}"
OIDC_REDIRECT: "${redirect_uri}"${app_url} resolves to the public URL of the running app; the
${issuer}, ${client_id}, ${client_secret}, ${redirect_uri} and
${scopes} placeholders are filled from the IdP’s response when the
user clicks Enable.