docs: document multiple auth providers and the callback URL change

This commit is contained in:
2026-08-03 11:05:40 +01:00
parent 37f2c1457e
commit b6fc8c3f77
3 changed files with 60 additions and 17 deletions
+9 -4
View File
@@ -362,7 +362,8 @@ password-change endpoint at all, so there is no competing writer for the hash.
- **Bootstrap** — first run has no users. `GET /auth/bootstrap-status` drives `/setup`, `POST /auth/bootstrap` creates the first org plus its owner.
- **Local auth** — email + password (bcrypt), `POST /auth/login`.
- **OIDC** — configured _per org_ (`org_oidc`), issuer + client ID + encrypted client secret. `/auth/oidc/start``/auth/oidc/callback`.
- **Auth providers** — configured _per instance_ in `auth_providers`, any number of them, each named and independently enabled. Issuer, client ID and an encrypted client secret per provider. `/auth/oidc/:providerId/start``/auth/oidc/:providerId/callback`. Presets (Entra, Google, Okta, GitHub) are a Go table in `server/internal/auth/presets.go` and expand to a real issuer on save, so nothing downstream knows a preset existed. GitHub is OAuth2 rather than OIDC and takes its own branch, requiring an address that is both primary **and** verified — an unverified address is not proof of control.
- **Local login** — `settings.local_login_enabled`, a `*bool` because absent must mean enabled; a plain bool would disable password sign-in fleet-wide at upgrade. `services.CheckLockout` refuses any change leaving neither local login nor an enabled provider, and is enforced in the service layer so the settings path and the provider path cannot disagree.
- **Sessions** — opaque 32-byte hex ID in the `km_session` cookie, session body stored in Redis with a 24h TTL.
- **Roles** — `owner`, `admin`, `member`. `/api/settings` and `/api/org/*` require owner or admin.
- **Host/org guard** — `APP_ROOT_LABEL` (default `vantage`) defines the app root label. A request to `<slug>.vantage.<tld>` resolves that org from the slug and rejects sessions belonging to a different one. Org lookups are cached for 60s.
@@ -422,7 +423,8 @@ GET /install /install.ps1 # dynamic agent install scripts
GET /update /update.ps1
GET /auth/bootstrap-status
POST /auth/bootstrap /auth/login /auth/logout
GET /auth/me /auth/oidc/start /auth/oidc/callback
GET /auth/me
GET /auth/providers # {local_enabled, providers:[{id,name,preset}]} — no issuer, client ID or secret
GET /api/secrets/:group/values # bearer token (ESO)
```
@@ -450,7 +452,8 @@ agent GET /agent/latest-version
settings GET,PUT /settings · POST /settings/secrets-token (owner|admin)
licence GET /license · POST /license (POST: self-hosted only)
org GET,POST /org/users · PUT /org/users/:id/role · DELETE /org/users/:id
GET,PUT /org/oidc (owner|admin)
providers GET,POST /auth/providers · PUT,DELETE /auth/providers/:id
POST /auth/providers/:id/{test,ack-notice} · GET /auth/presets (owner|admin)
```
`GET /license` reports `deployment`, and **`POST /license` answers 409 `cloud_managed` when it is `cloud`**. A cloud instance's licence is written by `admin/internal/inject` straight into the database and never through this endpoint, so the refusal cannot break injection — it only stops a customer pasting over a licence they do not own. `web/` hides the paste form and points at the HQ portal instead, but as with `hq`-managed users, the API is the boundary and the UI is the courtesy.
@@ -524,7 +527,7 @@ Paddle is merchant of record; `admin/internal/paddle` is a thin REST client (no
## MongoDB Collections
`servers` · `keys` · `assignments` · `orgs` · `users` · `org_oidc` · `settings` · `secrets` · `workflows` · `workflow_steps` · `workflow_runs` · `workflow_log_lines` · `workflow_log_seq` · `monitors` · `incidents` · `monitor_rollups` · `notification_channels` · `console_sessions` · `audit_logs` · `migrations`
`servers` · `keys` · `assignments` · `orgs` · `users` · `auth_providers` · `settings` · `secrets` · `workflows` · `workflow_steps` · `workflow_runs` · `workflow_log_lines` · `workflow_log_seq` · `monitors` · `incidents` · `monitor_rollups` · `notification_channels` · `console_sessions` · `audit_logs` · `migrations`
Every document except `migrations` carries `org_id`. Struct definitions are the source of truth — see `server/internal/models/`.
@@ -536,6 +539,7 @@ Notes that are not obvious from the structs:
- `assignments.revoked_at: null` means active. Revocation is soft, preserving audit history.
- `workflow_runs.steps_snapshot` freezes the resolved steps so editing the library never rewrites history.
- `console_sessions.token_consumed_at` is set atomically to enforce one-time use.
- `auth_providers.provider_id` is a short random identifier, not the Mongo `_id`: it appears in the callback URL a customer pastes into their IdP, and an `_id` there would publish a database key. `callback_notice` marks a provider migrated from the old single-provider shape, whose redirect URI therefore changed.
- `workflow_log_lines` is keyed `(run_id, server_id, seq)` — the index is not an optimisation, every read is a range scan over it. `workflow_log_seq` holds one counter document per `run_id/server_id`, which is what lets two pods interleave into one ordered log. Neither carries `instance_id`: they are reached only through a run, and a run is already scoped.
- `users.auth_source` is `local`, `oidc` or `hq`. An `hq` user was projected from a Vantage HQ account and carries `hq_user_id`; HQ owns its role, password and existence.
@@ -550,6 +554,7 @@ Admin's own database is separate and holds `accounts` · `admin_instances` · `l
- `0001_default_org_backfill`
- `0002_settings_org_backfill` (must run before 0003 — 0003 can create a `default` org, which pushes 0002 into its ambiguous multi-org branch)
- `0003_missed_org_scopes`
- `0005_auth_providers` — copies each `instance_oidc` document into `auth_providers`, ciphertext verbatim rather than decrypted and re-encrypted, so it does not need `KEY_ENCRYPTION_KEY` and cannot strand an instance's SSO configuration that has none set.
Index builders (`EnsureAuthIndexes`, `EnsureSettingsIndexes`) are fatal on failure; `EnsureSecretIndexes` and `EnsureWorkflowIndexes` only warn.
+5 -3
View File
@@ -66,9 +66,11 @@ Go to **Settings → Access**. Add members with a role:
Settings and organisation management require `owner` or `admin`.
If you would rather not manage passwords, configure OIDC instead see
[Settings](../vantage/settings.md#single-sign-on-oidc). OIDC is configured per
organisation, and the client secret is stored encrypted.
If you would rather not manage passwords, configure single sign-on instead:
see [Settings](../vantage/settings.md#single-sign-on). You can add more than
one identity provider; each gets its own button on the login page, and no
buttons appear at all until at least one provider is configured. Client
secrets are stored encrypted.
## Next
+46 -10
View File
@@ -16,6 +16,16 @@ this instance behaves" produced two half-pages and a nav entry nobody could
distinguish from Settings. The old path still redirects.
:::
:::danger Upgrading breaks existing single sign-on until you re-register the callback URL
Callback URLs are now per provider instead of one shared URL for the whole
instance. If you already had single sign-on configured, it was carried
forward automatically, but its callback URL changed and **sign-in through it
will fail until you copy the new callback URL from its settings card and
register it with your identity provider**. The migrated provider's card shows
a dismissable warning as a reminder. Password sign-in is not affected by this
change, so an administrator can always sign in locally to make the update.
:::
## Access
### Members
@@ -42,20 +52,46 @@ change would be overwritten by the next sync and would leave two writers for one
password hash. Manage them from [People and roles](../hq/people-and-roles.md).
:::
### Single sign-on (OIDC)
### Single sign-on
Configured per organisation:
Add as many identity providers as you need: one instance can have several at
once, each with its own name, its own button on the login page and its own
callback URL.
| Field | |
| ------------- | ---------------------------- |
| Issuer | Your provider's issuer URL |
| Client ID | |
| Client secret | Stored AES-256-GCM encrypted |
Pick a provider from the list of presets:
Sign-in then goes `/auth/oidc/start` → your provider → `/auth/oidc/callback`.
| Preset | You provide |
| ---------------------- | ------------------------------------------- |
| Microsoft Entra ID | Directory (tenant) ID |
| Google Workspace | nothing further, the issuer is fixed |
| Okta | Your Okta org domain |
| GitHub | Client ID and client secret only |
| Other (OpenID Connect) | The issuer URL of your identity provider |
Local and OIDC users coexist. Keep at least one local owner: if the provider is
misconfigured or unreachable, a local account is the way back in.
Every provider also needs a **Client ID** and **Client secret**; the secret is
stored AES-256-GCM encrypted and never shown again after you save it.
:::info GitHub requires a verified primary email
Vantage signs a person in by their email address. GitHub is asked for the
account's addresses and only accepts one that is **both** the account's
primary address **and** marked verified: an address GitHub has not confirmed
is not proof anyone controls it.
:::
#### Callback URL
Each provider gets its own callback URL, shown on its settings card with a
copy button. This is the address you register with the identity provider when
you set up the application on their side: each provider is registered
separately, even if you have several with the same identity provider.
#### Turning off password sign-in
You can disable local (email and password) sign-in once at least one provider
is enabled. Vantage refuses to save a change that would leave nobody able to
sign in, whether that change comes from the local login toggle or from
disabling the last enabled provider. Keep at least one option open until every
person who needs access can reach the new one.
## Monitoring