Pulse/docs/OIDC.md
rcourtman 9496d6f6d8 Fix four customer-facing doc drift findings (RBAC, OIDC, helm, webhooks)
RBAC.md (alerts:read → monitoring:read):
The example team-setup table told operators to issue API tokens with an
"alerts:read" scope. That scope does not exist in pkg/auth/scopes.go;
defined scopes are monitoring:read, settings:read, etc. /api/alerts/ is
gated by RequireAuth (no specific scope required), so an integrator
issuing a token would naturally pick the closest real scope —
monitoring:read — and that is what the doc should have shown.

OIDC.md (OIDC_GROUP_ROLE_MAPPINGS, OIDC_CA_BUNDLE):
Both env vars were documented but zero code reads them. OIDC config is
per-provider in internal/config/sso.go and OIDCProviderConfig in
internal/config/oidc.go: groupRoleMappings is a map field; caBundle is a
path field. Replace both env-var snippets with the actual UI/API path so
operators following the secure-install flow don't silently get no group
mapping or no custom CA trust. Same drift pattern as the earlier rc.1 →
rc.5 PULSE_RELAY_* aspiration-without-implementation.

WEBHOOKS.md (missing helpers):
notifications.go's templateFuncMap registers jsonString and pathescape
on every webhook template, but the helper list only documented title /
upper / lower / printf / urlquery / urlencode / urlpath. Add both, with
a short note that jsonString is the safe way to embed arbitrary string
values inside a JSON payload — Pulse's shipped templates use it
everywhere a value goes inside JSON, and operators writing custom
templates were missing the canonical escape primitive.

KUBERNETES.md (helm path + markdown fence):
- "deployment.strategy.type=Recreate" was the wrong helm path. The
  chart's strategy block is at the top level (deploy/helm/pulse/values.yaml
  line 9), so `strategy.type=Recreate` is what operators must actually
  --set. Following the broken path produced no override and left RWO
  PVC deployments on the default RollingUpdate, the exact Multi-Attach
  failure mode the note was trying to warn against.
- Trailing ```text on the helm-template code block closed the fence
  but tagged it as a language, breaking markdown rendering in some
  readers. Reduced to plain ```.

All four are doc-only changes; no code reads the names they document.
2026-05-12 15:54:24 +01:00

6 KiB

🔐 OIDC Single Sign-On

Enable Single Sign-On (SSO) with providers like Authentik, Keycloak, Okta, and Azure AD.

🚀 Quick Start

  1. Configure Provider: Create an OIDC application in your IdP.
    • Redirect URI: https://<your-pulse-domain>/api/oidc/<provider-id>/callback
    • Scopes: openid, profile, email
  2. Enable in Pulse: Go to Settings → Security → Single Sign-On.
  3. Enter Details:
    • Issuer URL: The base URL of your IdP (e.g., https://auth.example.com/application/o/pulse/).
    • Client ID & Secret: From your IdP.
  4. Save: The login page will now show your configured SSO provider button(s).

Tip: To hide the username/password form and only show the SSO button, set PULSE_AUTH_HIDE_LOCAL_LOGIN=true in your environment. You can still access the local login by appending ?show_local=true to the URL (e.g., https://your-pulse-instance/?show_local=true).

⚙️ Configuration

Setting Description
Issuer URL The OIDC provider's issuer URL. Must match the iss claim in tokens.
Client ID The application ID from your provider.
Client Secret The application secret.
Redirect URL Auto-detected. Override only if running behind a complex proxy setup.
Scopes Space-separated scopes. Default: openid profile email.
Claim Mapping Map email, username, and groups to specific token claims.

Note

: Setting OIDC_* environment variables locks those fields in the UI. See CONFIGURATION.md for the full list of overrides.

Access Control

Restrict access to specific users or groups:

  • Allowed Groups: Only users in these groups can login. Requires the groups scope/claim.
  • Allowed Domains: Restrict to specific email domains (e.g., example.com).
  • Allowed Emails: Allow specific email addresses.

Group-to-Role Mapping (Pro and Above)

Automatically assign Pulse roles based on OIDC group membership. When a user logs in, Pulse checks their groups claim and assigns the corresponding roles.

Configuration: Group-role mappings are configured per SSO provider through the UI (or the SSO provider API for automated setup) — there is no environment-variable override. Go to Settings → Security → Single Sign-On, edit the provider, and populate Group Role Mappings with entries like:

  • oidc-adminsadmin
  • oidc-operatorsoperator
  • oidc-viewersviewer

The mappings persist on the provider record as a groupRoleMappings JSON field. Provider-level config (including this field) can be PUT through the SSO provider API for automated setup.

How it works:

  • On each login, Pulse reads the user's groups from the configured groups claim.
  • For each group that matches a mapping, the corresponding role is assigned.
  • Multiple groups can map to multiple roles (user gets all matching roles).
  • Role assignments are updated on every login to reflect current group membership.
  • Role changes are logged to the audit log for compliance tracking.

Example: If a user has groups ["oidc-admins", "developers"] and you have mappings:

  • oidc-adminsadmin
  • developersoperator

The user will be assigned both admin and operator roles.

Note

: Ensure your IdP includes the groups scope and that the groups claim is properly configured. Some providers use groups, others use roles or custom claims.

Long-Lived Sessions with offline_access

For persistent sessions that don't require frequent re-authentication:

  1. Add offline_access scope: Include offline_access in your OIDC scopes (e.g., openid profile email offline_access).
  2. Configure your IdP: Ensure your identity provider issues refresh tokens when offline_access is requested.

How it works:

  • When you login with offline_access, Pulse stores the refresh token alongside your session.
  • When your access token expires, Pulse automatically refreshes it using the stored refresh token.
  • Your session remains valid as long as the refresh token is valid (typically 30-90 days depending on your IdP).
  • If the IdP revokes access (user disabled, token revoked), Pulse detects this on the next refresh attempt and logs you out.

Security considerations:

  • Refresh tokens are stored encrypted at rest.
  • If the IdP configuration changes, existing sessions with mismatched issuers are automatically invalidated.
  • Failed refresh attempts immediately invalidate the session.

📚 Provider Examples

Authentik

  • Type: OAuth2/OpenID (Confidential)
  • Redirect URI: https://pulse.example.com/api/oidc/<provider-id>/callback
  • Signing Key: Must use RS256 (create a certificate/key pair if needed).
  • Issuer URL: https://auth.example.com/application/o/pulse/

Keycloak

  • Client ID: pulse
  • Access Type: Confidential
  • Valid Redirect URIs: https://pulse.example.com/api/oidc/<provider-id>/callback
  • Issuer URL: https://keycloak.example.com/realms/myrealm

Azure AD

  • Redirect URI: https://pulse.example.com/api/oidc/<provider-id>/callback (Web)
  • Issuer URL: https://login.microsoftonline.com/<tenant-id>/v2.0
  • Note: Enable "ID tokens" in Authentication settings.

🔧 Troubleshooting

Issue Solution
invalid_id_token Issuer URL mismatch. Check logs (LOG_LEVEL=debug) to see the expected vs. received issuer.
unexpected signature algorithm "HS256" Your IdP is signing with HS256. Configure it to use RS256.
Redirect Loop Check X-Forwarded-Proto header (must be https) and cookie settings.
Self-Signed Certs Set the CA Bundle field on the SSO provider to a host path readable by Pulse (e.g. /etc/ssl/certs/oidc-ca.pem mounted into the container). The field is stored on the provider record as oidc.caBundle; there is no OIDC_CA_BUNDLE env var.

Debugging

Enable debug logs to trace the OIDC flow:

export LOG_LEVEL=debug
# Restart Pulse

Logs will show discovery, token exchange, and claim parsing details.