Files
stack/docs/ADMIN-GUIDE/security/sso-providers.md
T
veronica f0d2dd9920 docs(W4): stamp kind and status front matter on 104 live documents
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`.
2026-08-20 19:30:25 -05:00

8.4 KiB

kind, status, title, type, audience, source_of_truth
kind status title type audience source_of_truth
guide active SSO Providers runbook admin 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:

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:

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:

{BETTER_AUTH_URL}/api/auth/oauth2/callback/authentik

WorkOS OIDC

Set all three variables to enable WorkOS:

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:

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:

{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:

KEYCLOAK_ISSUER=https://auth.example.com/realms/mosaic
KEYCLOAK_CLIENT_ID=mosaic
KEYCLOAK_CLIENT_SECRET=...

Derived issuer:

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:

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:

{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:

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:

{BETTER_AUTH_URL}/api/auth/oauth2/callback/{providerId}

The gateway exposes provider discovery at:

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.
  • loginModeoidc, 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:

    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.