A - docs/README.md:149-190 rewritten. It prescribed a competing front-matter schema
(title/type/audience/status/source_of_truth) adopted by 4 of 128 live documents. Two
documented conventions in one repo is the defect this pass removes, so the README now
documents the contract and the 4 files convert in the same commit: `type` dropped
(kind replaces it), `title`/`audience`/`source_of_truth` kept.
B - source-of-truth leaves the kind enum, which is now 6 values, and returns as an orthogonal
boolean. kind was carrying two independent facts. docs/requirements/native-kanban-sot.md
is stamped `kind: spec` + `source_of_truth: true`, which is what it always was.
C - status gains `completed`. Applied to the two executed plans, on artifact evidence rather
than on their own say-so: --purpose push|merge ships in ci-queue-wait.sh, and every section
the README plan specifies exists in docs/README.md today.
D - kind follows content, never filename. docs/native-kanban-sot/TASKS.md is `kind: spec`
because its body says "a build plan, not a task tracker". The name stays wrong; that is a
rename and it is out of scope here.
E - the contract covers .md only, written into the README as a decision with vision's
YAML.parse measurement as the reason, so the omission does not read as an oversight.
F - channel-protocol.md guide -> spec. Applied, with a correction the reviewer should see: the
ruling cites "7 normative MUSTs" and there are ZERO uppercase RFC2119 terms in that file.
Control: the identical grep returns 25 lines in docs/requirements/native-kanban-sot.md. The
citation half of the finding does hold and is larger than stated. Consequence recorded in
the worklist: the file's own banner now contradicts its header.
Verified: 128 live .md under docs/ (127 baseline + this PR's worklist), 107 stamped, 0 invalid
kinds, 17 operator-held + 3 supersede-stamp deferrals + 1 generated = 21 unstamped. 107+21=128.
Control: the verifier reports valid=False when a kind is corrupted to `nonsense`, so the
0-invalid result is a real result. prettier --check clean across docs/.
193 lines
8.4 KiB
Markdown
193 lines
8.4 KiB
Markdown
---
|
|
kind: guide
|
|
status: active
|
|
title: SSO Providers
|
|
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)
|