Applies the document contract from
docs/plans/2026-08-20_stack-docs-flatten-and-alignment.md section 3, partially:
`kind` and `status` only. `parent` is deliberately held until the flatten in
section 4 lands, so that 127 documents do not have to be re-pointed by hand
when docs/fleet/NORTH_STAR.yaml moves to docs/NORTH_STAR.yaml.
Scope, measured on origin/next at 63069149:
127 live docs = all *.md under docs/ minus docs/archive/ minus docs/_old_structure/
104 stamped here
19 held operator judgement (plan section 9), worklist in the same PR
3 held the SUPERSEDED TASKS.md stamps, which cite the moving path
1 untouched docs/fleet/FLEET-DOCTRINE.md, already stamped in W1
Kinds applied: 54 guide, 34 record, 9 spec, 6 tracking, 1 projection.
Every row carries a confidence and a one-line rationale in the worklist.
Two collisions with the existing state, both flagged rather than resolved:
1. docs/README.md:150-160 already documents a front-matter convention
(title/type/audience/status/source_of_truth) with its own allowed values.
It is applied to 4 of 127 files. Its `status` vocabulary is
current|draft|deprecated|historical; the new contract's is active|superseded-by.
The key collides. This commit lets the new contract win and rewrites
`status: current` to `status: active` on those 4 files, keeping their other
legacy keys untouched. No code reads any of them: `git grep source_of_truth`
outside docs/ returns nothing. docs/README.md still prescribes the old
convention and is an operator row, so it is not edited here.
2. Two of the plan's 20 operator rows are YAML files, not markdown
(docs/fleet/examples/roster-v2.yaml, docs/openapi-tess.yaml), and the
contract's front-matter form has no defined meaning for a .yaml document.
That gap also applies to docs/fleet/NORTH_STAR.yaml, the source of truth
itself. Raised in the worklist.
A third row from the plan, docs/fleet/north-star.md, no longer exists: W1
renamed it to docs/fleet/FLEET-DOCTRINE.md.
Verification: 104/104 parse with the expected kind and status in front matter;
the check was shown to reject a wrong kind before it was trusted. The diff
removes 4 lines total, all of them `status: current`.
194 lines
8.4 KiB
Markdown
194 lines
8.4 KiB
Markdown
---
|
|
kind: guide
|
|
status: active
|
|
title: SSO Providers
|
|
type: runbook
|
|
audience: admin
|
|
source_of_truth: false
|
|
---
|
|
|
|
# SSO Providers
|
|
|
|
Configure optional enterprise single sign-on for Mosaic Stack through Better Auth's generic OAuth integration. The gateway owns provider configuration and discovery; the web application renders only providers reported by the gateway.
|
|
|
|
> **Current behavior:** Authentik, WorkOS, and Keycloak are supported in the checked-in implementation. Authentik and WorkOS use OIDC. Keycloak supports OIDC and an optional direct SAML login URL. The web application does **not** read `NEXT_PUBLIC_WORKOS_ENABLED` or `NEXT_PUBLIC_KEYCLOAK_ENABLED`; provider buttons are discovered dynamically from the gateway.
|
|
|
|
## Prerequisites
|
|
|
|
Before configuring a provider, establish these gateway settings:
|
|
|
|
- `BETTER_AUTH_URL` — the public base URL used to construct OAuth callback URLs.
|
|
- `BETTER_AUTH_SECRET` — a strong secret for Better Auth sessions and tokens.
|
|
- `GATEWAY_CORS_ORIGIN` — the web origin or comma-separated origins allowed by the gateway.
|
|
|
|
Keep client secrets and Better Auth secrets in the deployment secret store. Do not commit them to `.env` files or expose them to the web bundle.
|
|
|
|
## Provider configuration
|
|
|
|
### Authentik OIDC
|
|
|
|
Set all three variables to enable Authentik:
|
|
|
|
```bash
|
|
AUTHENTIK_ISSUER=https://auth.example.com/application/o/mosaic
|
|
AUTHENTIK_CLIENT_ID=...
|
|
AUTHENTIK_CLIENT_SECRET=...
|
|
```
|
|
|
|
The implementation derives OIDC discovery and endpoint URLs from `AUTHENTIK_ISSUER`. An optional team-claim label can be exposed in provider discovery:
|
|
|
|
```bash
|
|
AUTHENTIK_TEAM_SYNC_CLAIM=groups
|
|
```
|
|
|
|
The default reported claim is `groups`. The discovery payload reports this claim; verify any downstream membership-sync behavior separately before treating it as an authorization guarantee.
|
|
|
|
Register this redirect URI with the Authentik application:
|
|
|
|
```text
|
|
{BETTER_AUTH_URL}/api/auth/oauth2/callback/authentik
|
|
```
|
|
|
|
### WorkOS OIDC
|
|
|
|
Set all three variables to enable WorkOS:
|
|
|
|
```bash
|
|
WORKOS_ISSUER=https://your-company.authkit.app
|
|
WORKOS_CLIENT_ID=client_...
|
|
WORKOS_CLIENT_SECRET=...
|
|
```
|
|
|
|
Use the WorkOS AuthKit issuer or custom authentication domain, not a raw WorkOS REST API hostname. Mosaic derives the OIDC discovery URL by appending `/.well-known/openid-configuration` to the issuer. WorkOS uses PKCE and issuer validation in the current auth configuration.
|
|
|
|
An optional team-claim label can be exposed in provider discovery:
|
|
|
|
```bash
|
|
WORKOS_TEAM_SYNC_CLAIM=organization_id
|
|
```
|
|
|
|
The default reported claim is `organization_id`. The discovery payload reports this claim; verify any downstream membership-sync behavior separately before treating it as an authorization guarantee.
|
|
|
|
Register this redirect URI with the WorkOS application:
|
|
|
|
```text
|
|
{BETTER_AUTH_URL}/api/auth/oauth2/callback/workos
|
|
```
|
|
|
|
### Keycloak OIDC
|
|
|
|
Use either an explicit issuer or the URL-plus-realm form. The client ID and secret are required in both forms.
|
|
|
|
Explicit issuer:
|
|
|
|
```bash
|
|
KEYCLOAK_ISSUER=https://auth.example.com/realms/mosaic
|
|
KEYCLOAK_CLIENT_ID=mosaic
|
|
KEYCLOAK_CLIENT_SECRET=...
|
|
```
|
|
|
|
Derived issuer:
|
|
|
|
```bash
|
|
KEYCLOAK_URL=https://auth.example.com
|
|
KEYCLOAK_REALM=mosaic
|
|
KEYCLOAK_CLIENT_ID=mosaic
|
|
KEYCLOAK_CLIENT_SECRET=...
|
|
```
|
|
|
|
`KEYCLOAK_ISSUER` takes precedence when both forms are present. Keycloak uses PKCE and issuer validation in the current auth configuration.
|
|
|
|
An optional team-claim label can be exposed in provider discovery:
|
|
|
|
```bash
|
|
KEYCLOAK_TEAM_SYNC_CLAIM=groups
|
|
```
|
|
|
|
The default reported claim is `groups`. The discovery payload reports this claim; verify any downstream membership-sync behavior separately before treating it as an authorization guarantee.
|
|
|
|
Register this redirect URI with the Keycloak client:
|
|
|
|
```text
|
|
{BETTER_AUTH_URL}/api/auth/oauth2/callback/keycloak
|
|
```
|
|
|
|
### Keycloak direct SAML fallback
|
|
|
|
The current web flow supports a direct SAML link for Keycloak when `KEYCLOAK_SAML_LOGIN_URL` is configured:
|
|
|
|
```bash
|
|
KEYCLOAK_SAML_LOGIN_URL=https://auth.example.com/realms/mosaic/protocol/saml
|
|
```
|
|
|
|
This creates a configured Keycloak provider with `loginMode: saml`. The web login button links directly to the supplied URL as `Continue with Keycloak (SAML)`; there is no Better Auth OIDC callback for this mode. The URL must be the provider's valid SAML launch URL for the deployment.
|
|
|
|
A SAML-only Keycloak configuration does not require the Keycloak OIDC client variables. Do not combine an incomplete OIDC variable set with SAML-only configuration: partial OIDC configuration is rejected during auth setup.
|
|
|
|
## Callback and discovery contract
|
|
|
|
Better Auth is mounted at `/api/auth`. OIDC callbacks use:
|
|
|
|
```text
|
|
{BETTER_AUTH_URL}/api/auth/oauth2/callback/{providerId}
|
|
```
|
|
|
|
The gateway exposes provider discovery at:
|
|
|
|
```text
|
|
GET /api/sso/providers
|
|
```
|
|
|
|
The response includes `authentik`, `workos`, and `keycloak` records with:
|
|
|
|
- `configured` — whether a usable OIDC or Keycloak SAML configuration is present.
|
|
- `protocols` — supported protocols for the provider record.
|
|
- `loginMode` — `oidc`, `saml`, or `null`.
|
|
- `callbackPath` — the OIDC callback path, or `null` for SAML-only mode.
|
|
- `teamSync` — the configured/default claim label exposed to the UI.
|
|
- `samlFallback` — whether a direct Keycloak SAML URL is configured.
|
|
- `warnings` — partial OIDC configuration warnings when reported.
|
|
|
|
The web login page filters this response to configured providers. OIDC buttons use Better Auth's `signIn.oauth2` flow. A configured Keycloak SAML fallback is rendered as a direct link. No provider-specific `NEXT_PUBLIC_*_ENABLED` flag is required or consumed.
|
|
|
|
## Configuration procedure
|
|
|
|
1. Set `BETTER_AUTH_URL` to the public gateway URL that the identity provider can reach.
|
|
2. Set `BETTER_AUTH_SECRET` and the correct `GATEWAY_CORS_ORIGIN` values.
|
|
3. Choose one provider configuration above and set its complete required variable group.
|
|
4. Register the exact OIDC callback URI with the identity provider, when using OIDC.
|
|
5. Restart or redeploy the gateway so it loads the changed environment.
|
|
6. Inspect discovery without exposing secrets:
|
|
|
|
```bash
|
|
curl "$BETTER_AUTH_URL/api/sso/providers"
|
|
```
|
|
|
|
7. Open the web login page and confirm that only configured providers are shown.
|
|
8. Complete a sign-in and verify the callback returns to the configured application.
|
|
|
|
## Partial configuration and failure modes
|
|
|
|
Provider configuration is optional. If no provider variables are set, the gateway can run without SSO providers and the web login page renders no SSO section.
|
|
|
|
For Authentik and WorkOS, setting only part of the issuer/client ID/client secret group raises a configuration error. For Keycloak OIDC, the client ID, client secret, and either an explicit issuer or a complete URL-plus-realm pair are required. Empty or whitespace-only values are treated as unset.
|
|
|
|
Common failures:
|
|
|
|
| Symptom | Check |
|
|
| ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
| Provider does not appear on the login page | Query `/api/sso/providers`; verify the complete provider variable group is present in the gateway environment. |
|
|
| OAuth callback is rejected | Compare the registered redirect URI character-for-character with `BETTER_AUTH_URL` and the provider callback path. |
|
|
| Provider is shown but sign-in cannot start | Check `loginMode`, issuer discovery, client credentials, and gateway logs. |
|
|
| Keycloak SAML button is absent | Set `KEYCLOAK_SAML_LOGIN_URL` to the provider's direct launch URL and reload the gateway. |
|
|
| Startup/auth initialization reports missing variables | Remove the partial provider configuration or provide the complete required group. |
|
|
| SSO succeeds but team membership is unexpected | Treat `teamSync.claim` as discovery metadata and verify the actual claim mapping and membership-sync implementation. |
|
|
|
|
Do not enable a provider by adding the obsolete `NEXT_PUBLIC_WORKOS_ENABLED` or `NEXT_PUBLIC_KEYCLOAK_ENABLED` variables. They are not read by the current web application.
|
|
|
|
## Related
|
|
|
|
- [Administrator guide](../README.md)
|
|
- [Security chapter](README.md)
|
|
- [API documentation index](../../API/README.md)
|
|
- [Documentation atlas](../../README.md)
|