Authentication and SSO

August 2, 2026 · View on GitHub

How Observal authenticates users and signs tokens, and how to wire up SSO.

Authentication modes

Observal supports password auth, API keys, OAuth / OIDC, and SAML. The public login page shows only the methods enabled for the deployment.

MethodHow it is enabledNotes
Email + passwordDefault password authUsed by bootstrap admins and locally managed users
Self registrationauth.self_registration_enabled=trueCreates standard user accounts only
OAuth / OIDCSSO tab settings: oauth.client_id, oauth.client_secret, oauth.server_metadata_urlUses IdP discovery metadata. API restart required after changes.
SAMLSSO tab settings: saml.*SAML setup with JIT provisioning
API keysUser generated after loginInherits the user's role

Use deployment.sso_only=true when password login should be hidden and only SSO should be available.

SSO-only mode {#sso-only-mode}

deployment.sso_only controls whether password-based authentication is available.

ValueEffect
false (default)Email and password login stays available alongside SSO.
truePassword login, password reset, and local user bootstrap are blocked. Users must sign in with OIDC or SAML.

Set it in Admin → SSO → Access policy. Confirm OIDC or SAML works with a real test user before enabling it.

Self registration {#self-registration}

Controls whether visitors can create their own Observal account from the login page.

ValueEffect
trueShows a Register button on the login page and allows public account creation
false (default)Hides registration and blocks POST /api/v1/auth/register

You can set this in the web UI at Admin → Settings → Authentication → Self Registration Enabled. If you prefer the CLI, set the same dynamic setting directly:

observal admin set auth.self_registration_enabled true

New accounts are created with the built-in user role. They cannot review submissions, manage users, or change server settings unless an admin promotes them later.

Disable it again with:

observal admin set auth.self_registration_enabled false

The bootstrap flow

On a fresh server with no users, the /api/v1/auth/bootstrap endpoint is available to localhost only. When you run observal auth login, the CLI detects the empty user table and bootstraps an admin account interactively.

This is how you create the first admin without any pre-existing credential.

Once the first admin exists, bootstrap is disabled.

JWT signing keys

Tokens are signed with asymmetric keys (ES256 by default, RS256 also supported). Keys are generated on first startup and stored in the apidata volume at $JWT_KEY_DIR (default /data/keys).

JWT_SIGNING_ALGORITHM=ES256          # ES256 (default) or RS256
JWT_KEY_DIR=/data/keys

Critical: back up $JWT_KEY_DIR

Losing these keys invalidates every access and refresh token. All users must log in again. Tokens rotate, but only the private key can issue new ones, and there is no recovery path without the keys.

Back up the apidata volume every time you back up Postgres. See Backup and restore.

Key rotation

To rotate keys, stop the API, delete the files under $JWT_KEY_DIR, and restart. New keys are generated. All existing sessions are invalidated. Plan for the outage.

OAuth / OIDC SSO

Set these three in Admin → SSO → SSO settings, then restart the API so the OIDC client is rebuilt:

SettingValue
oauth.client_id {#oauth-client-id}Client ID from your IdP
oauth.client_secret {#oauth-client-secret}Client secret from your IdP
oauth.server_metadata_url {#oauth-server-metadata-url}OIDC discovery URL, for example https://accounts.example.com/.well-known/openid-configuration

Observal uses Authlib and reads the IdP discovery document, so any OIDC-compliant provider works (Auth0, Okta, Azure AD, Google Workspace, Keycloak, Authentik, Dex, etc.).

OIDC Client ID {#oauth-client-id}

The public client identifier from your IdP application registration.

OIDC Client Secret {#oauth-client-secret}

The private client secret from your IdP application registration. It is stored encrypted and is never shown again after saving.

OIDC Discovery URL {#oauth-server-metadata-url}

The .well-known/openid-configuration URL for your IdP tenant or authorization server.

Redirect URI

Configure your IdP to allow:

{FRONTEND_URL}/api/v1/auth/oauth/callback

With FRONTEND_URL=https://observal.your-company.internal, that's:

https://observal.your-company.internal/api/v1/auth/oauth/callback

First OAuth login

The first user who logs in via OAuth is not automatically an admin. Bootstrap a local admin first (via observal auth login before enabling OAuth, or via the demo super admin), then use that admin to promote the OAuth user.

Scope / claims

Observal requests standard openid profile email scope. The IdP's email claim is the canonical user identifier.

Google OAuth (first-class provider) {#google-oauth}

Google sign-in runs as its own provider, separate from the generic OIDC slot above. Both can be enabled at the same time, so a deployment can offer Okta and Google on the login screen.

Set these in the SSO settings page, or set them as container env vars for one-time import at startup. The Sign in with Google button appears after the API restarts:

GOOGLE_OAUTH_CLIENT_ID=1234567890-abc...apps.googleusercontent.com
GOOGLE_OAUTH_CLIENT_SECRET=GOCSPX-...

The Google OIDC discovery URL is hardcoded server-side, so you don't need to set it.

Creating the Google OAuth client

  1. Open the Google Cloud Console in the project you want to use.
  2. Click Create Credentials → OAuth client ID.
  3. Application type: Web application.
  4. Authorized JavaScript origins: {FRONTEND_URL} (e.g. https://observal.your-company.internal).
  5. Authorized redirect URI: {FRONTEND_URL}/api/v1/auth/oauth/google/callback.
  6. Copy the generated Client ID and Client secret into SSO settings or your container env.
  7. Restart the API container so the Authlib client is rebuilt.

Restricting to specific email domains {#google-allowed-domains}

Set GOOGLE_OAUTH_ALLOWED_DOMAINS to a comma-separated list of domains. Anyone outside the list is rejected with a 403, even if they have a valid Google account.

GOOGLE_OAUTH_ALLOWED_DOMAINS=acme.com,acme.io

Leave it unset to allow any Google account (including personal @gmail.com addresses) to provision themselves as role=user.

Notes

  • Observal additionally requires Google's email_verified claim to be true. Unverified accounts (rare on Google but possible) are rejected with a 400.
  • The first Google user is not automatically an admin (matches the generic OIDC behavior). Bootstrap a local admin first, then use that account to promote the Google user.
  • The auth provider and Google subject ID are recorded on the user row (auth_provider="google", sso_subject_id=<google-sub>) for audit purposes.

GitHub OAuth (first-class provider) {#github-oauth}

GitHub sign-in runs as its own provider, alongside generic OIDC and Google. Unlike those two, GitHub is plain OAuth 2.0 (not OIDC): there is no discovery URL and no ID token. Observal fetches the profile and email list from the GitHub REST API after the code exchange.

Set these in the SSO settings page, or as container env vars for one-time import at startup. The Sign in with GitHub button appears after the API restarts:

GITHUB_OAUTH_CLIENT_ID=Iv1.abc123...
GITHUB_OAUTH_CLIENT_SECRET=ghp_... / github_pat_... style secret

Creating the GitHub OAuth app

  1. Open Settings → Developer settings → OAuth Apps (on your personal account or, preferably, your GitHub organization) and click New OAuth App.
  2. Homepage URL: {FRONTEND_URL} (e.g. https://observal.your-company.internal).
  3. Authorization callback URL: {FRONTEND_URL}/api/v1/auth/oauth/github/callback.
  4. Copy the generated Client ID and generate a Client secret; put both into SSO settings or your container env.
  5. Restart the API container so the Authlib client is rebuilt.

Restricting to GitHub organizations {#github-allowed-orgs}

Set GITHUB_OAUTH_ALLOWED_ORGS to a comma-separated list of GitHub organization slugs. Only active members of at least one listed org can sign in; everyone else is rejected with a 403. Pending invitations don't count.

GITHUB_OAUTH_ALLOWED_ORGS=acme-inc,acme-labs

When an org allowlist is configured, Observal requests the read:org scope (in addition to read:user user:email) so it can see private org memberships. Members may need to grant/request org approval for the OAuth app if the org restricts third-party access.

Leave it unset to allow any GitHub account to provision itself as role=user.

Notes

  • Observal only accepts verified email addresses from GET /user/emails (preferring the primary). The profile-level email field is never trusted, and accounts with no verified email are rejected with a 400.
  • Departments are not populated automatically from GitHub (there is no groups claim). Assign them in Admin → Users, individually or via bulk upload — same as Google users.
  • The auth provider and the numeric GitHub user ID are recorded on the user row (auth_provider="github", sso_subject_id=<github-id>). The numeric ID is used instead of the login handle because handles can be renamed and re-registered.

Role-based access control (RBAC)

Four built-in roles enforced on every endpoint:

RoleTypical abilities
userPublish components, install agents, view their own data
reviewer+ approve/reject registry submissions
admin+ manage users, change server settings
super_admin+ sensitive super-admin-only operations

Change a user's role:

observal admin users
# GET /api/v1/admin/users/{id}/role   to inspect
# PUT /api/v1/admin/users/{id}/role   to change

Or in the web UI at /settings/users.

API keys

Users can generate API keys for scripts and CI. The key inherits the user's role.

# Get a key - flow depends on your setup (web UI usually)
# Then in CI:
export OBSERVAL_API_KEY=<key>
export OBSERVAL_SERVER_URL=https://observal.your-company.internal

observal ops traces --limit 100 --output json | jq

Keys can be revoked via POST /api/v1/auth/token/revoke.

Rate limits

Auth endpoints are rate-limited to slow brute-force attempts:

SettingDefaultScope
RATE_LIMIT_AUTH10/minuteGeneral auth endpoints
RATE_LIMIT_AUTH_STRICT5/minuteLogin, registration, and password reset

Tighten for public-facing deployments.

Password reset

Users who forget their password request a reset code via observal auth reset-password --email <email> or the web UI Forgot password? link. The server logs a 6-character code to its console:

WARNING - PASSWORD RESET CODE for alice@example.com: A7X9B2 (expires in 15 minutes)

An operator reads the log and passes the code to the user out-of-band (Slack, phone). This is deliberate: no email infrastructure needed for the default flow. If you want emailed reset codes, implement an email transport in the server.

Operational controls

Observal includes:

  • Audit logging: every privileged action lands in ClickHouse's audit_log
  • SSO-only mode (deployment.sso_only=true)

See docs/self-hosting/sso-cli.md for SSO CLI commands.

Next

Session tracking and reconciliation