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.
| Method | How it is enabled | Notes |
|---|---|---|
| Email + password | Default password auth | Used by bootstrap admins and locally managed users |
| Self registration | auth.self_registration_enabled=true | Creates standard user accounts only |
| OAuth / OIDC | SSO tab settings: oauth.client_id, oauth.client_secret, oauth.server_metadata_url | Uses IdP discovery metadata. API restart required after changes. |
| SAML | SSO tab settings: saml.* | SAML setup with JIT provisioning |
| API keys | User generated after login | Inherits 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.
| Value | Effect |
|---|---|
false (default) | Email and password login stays available alongside SSO. |
true | Password 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.
| Value | Effect |
|---|---|
true | Shows 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:
| Setting | Value |
|---|---|
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
- Open the Google Cloud Console in the project you want to use.
- Click Create Credentials → OAuth client ID.
- Application type: Web application.
- Authorized JavaScript origins:
{FRONTEND_URL}(e.g.https://observal.your-company.internal). - Authorized redirect URI:
{FRONTEND_URL}/api/v1/auth/oauth/google/callback. - Copy the generated Client ID and Client secret into SSO settings or your container env.
- 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_verifiedclaim to betrue. 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
- Open Settings → Developer settings → OAuth Apps (on your personal account or, preferably, your GitHub organization) and click New OAuth App.
- Homepage URL:
{FRONTEND_URL}(e.g.https://observal.your-company.internal). - Authorization callback URL:
{FRONTEND_URL}/api/v1/auth/oauth/github/callback. - Copy the generated Client ID and generate a Client secret; put both into SSO settings or your container env.
- 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-levelemailfield 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:
| Role | Typical abilities |
|---|---|
user | Publish 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:
| Setting | Default | Scope |
|---|---|---|
RATE_LIMIT_AUTH | 10/minute | General auth endpoints |
RATE_LIMIT_AUTH_STRICT | 5/minute | Login, 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.