Sign-in and SSO
Let people sign in to the dashboard through your institution's identity provider, next to the break-glass admin account that always works.
4 min readUpdated for v2.4.0Edit on GitHub
Set up SSO
- 1
Register the dashboard with your provider
Create a confidential client in the provider's developer console, requesting the
openid email profilescopes, with this redirect URI:https://<dashboard-host>/api/auth/oauth2/callback/<providerId><providerId>is a short name you choose, such asglobus. - 2
Set the dashboard's URL
Set
BETTER_AUTH_URLto the dashboard's browser-facing URL, such ashttps://dashboard.example.edu. The redirect URI you registered must match it exactly, scheme and port included. - 3
Add the provider
Set
OIDC_PROVIDERSto a JSON array with one object per provider:[{ "providerId": "globus", "displayName": "Globus", "discoveryUrl": "https://auth.globus.org/.well-known/openid-configuration", "clientId": "YOUR_CLIENT_ID", "clientSecret": "YOUR_CLIENT_SECRET", "scopes": ["openid", "email", "profile"] }] - 4
Restart the control plane and approve people
The sign-in page shows a button per provider. Each person's first sign-in creates a pending account; approve it on Users.
Where the variables go depends on how you run the dashboard: deploy/docker/.env for Docker Compose, control-plane/.env.local when running it on the host with npm run dev, or the controlPlane values under Helm (see below). Moving from a test provider to a real one is only a change to these variables.
Provider fields
| Field | Meaning |
|---|---|
providerId | Short name used in the redirect URI: letters, digits and hyphens |
displayName | The label on the sign-in button |
discoveryUrl | The provider's OIDC discovery document |
clientId, clientSecret | From the provider's client registration |
scopes | Defaults to openid email profile, which is enough |
claims | Read email, name or image from a different claim. See below |
overrideUserInfo | true re-applies the profile to existing users at every sign-in, not only the first |
pkce | true sends a PKCE challenge, so an intercepted code cannot be redeemed alone. Off by default; turn it on where the provider supports it |
authentication | How the client authenticates at the token endpoint: "post" (credentials in the body, the default) or "basic" |
The discovery URLs are https://auth.globus.org/.well-known/openid-configuration for Globus and https://cilogon.org/.well-known/openid-configuration for CILogon (register at cilogon.org/oauth2/register). An unknown authentication value or claims field is refused with an error.
Admin approval flow
A first SSO sign-in creates an account with no access. Pending users see an awaiting-approval screen with Sign out.
On Users, accounts waiting for approval are listed first. Pick a role and a tenant, then Approve; Not now leaves them waiting. Below them, everyone else is listed with what they can reach and when they joined, and can be changed or deactivated.
| Role | Can reach |
|---|---|
admin | The whole dashboard |
user | The self-service portal for their tenant: its models, its keys and its usage. See Tenants and keys |
Two changes are always refused, so nobody locks the gateway out: removing admin from your own account, and demoting or deactivating the last active admin. The same rule covers the sign-in library's own admin endpoints under /api/auth/admin/*.
Break-glass local admin
One email-and-password admin is seeded at start-up and works even when the identity provider is down. Public sign-up is closed; every other account comes from a first SSO sign-in.
| Variable | Default | Notes |
|---|---|---|
DASHBOARD_ADMIN_EMAIL | admin@example.com | A real email address |
DASHBOARD_PASSWORD | none | At least 8 characters. Required by Compose and Helm |
The account is created only when there is no active admin, so it is made on first boot and not recreated. A password shorter than 8 characters, or an existing non-admin account with the same email, is logged as an error and no account is made; the dashboard still starts and serves SSO. Change both values from any development defaults before the dashboard is reachable from a network.
Reaching the dashboard from another host
Sign-in checks the browser's Origin against BETTER_AUTH_URL, which defaults to http://localhost:3000 (the Compose stack uses http://localhost:3002). Opening the dashboard by IP address or another hostname fails with an invalid-origin error, for the local admin as well as SSO, until you either set BETTER_AUTH_URL to the URL you browse to or list the other origins:
TRUSTED_ORIGINS=http://10.0.0.10:3002,http://obleth.example.internal:3002
A single * trusts every origin: acceptable on a trusted private network, unsafe anywhere reachable from the internet. BETTER_AUTH_URL still sets the host in redirect URIs, whatever TRUSTED_ORIGINS allows.
Kubernetes (Helm)
controlPlane:
dashboardAdminEmail: "admin@your-org.example"
dashboardPassword: "" # at least 8 characters; pass with --set or a Secret
dashboardSessionSecret: "" # at least 32 characters; also used as BETTER_AUTH_SECRET
betterAuthUrl: "https://dashboard.example.edu"
trustedOrigins: ""
oidcProviders: |
[{"providerId":"globus","displayName":"Globus","discoveryUrl":"https://auth.globus.org/.well-known/openid-configuration","clientId":"ID","clientSecret":"SECRET","scopes":["openid","email","profile"]}]
In production, put these in a Secret you create and set controlPlane.existingSecret. The chart then renders no control-plane Secret, so yours must carry every key: DASHBOARD_PASSWORD, DASHBOARD_SESSION_SECRET, BETTER_AUTH_SECRET, DATABASE_URL, DASHBOARD_ADMIN_EMAIL, BETTER_AUTH_URL, TRUSTED_ORIGINS and OIDC_PROVIDERS. See Helm values.
Try it locally with Dex
The Compose stack includes a Dex test provider behind the sso-dev profile, with one user, researcher@example.edu / password. In deploy/docker/.env:
COMPOSE_PROFILES=sso-dev
OIDC_PROVIDERS=[{"providerId":"dex","displayName":"Dev SSO (Dex)","discoveryUrl":"http://host.docker.internal:5556/dex/.well-known/openid-configuration","clientId":"obleth-control-plane","clientSecret":"dev-dex-secret","scopes":["openid","email","profile"]}]
Run docker compose up -d in deploy/docker and open http://localhost:3002. The issuer uses host.docker.internal because it must resolve to the same address from your browser and from the control-plane container. If you change the dashboard's port, update Dex's redirectURIs in deploy/docker/dex/config.docker.yaml and BETTER_AUTH_URL to match.
When the control plane runs on the host (npm run dev, port 3000) and only Dex runs in Docker, use http://localhost:5556/dex/.well-known/openid-configuration as the discovery URL and set BETTER_AUTH_URL=http://localhost:3000 in control-plane/.env.local. More test users go under staticPasswords in the Dex config, with bcrypt-hashed passwords; restart Dex after editing.
Mapping user fields from non-standard claims
Some providers send an email that is not the identifier your institution keys accounts on. Globus, for a university identity, can send a display alias such as Ada.Lovelace@example.edu as email and the canonical alovelace@example.edu as preferred_username. Without a mapping, the same person can arrive as a second, unrecognised user. Map the claim:
{ "claims": { "email": "preferred_username" }, "overrideUserInfo": true, "pkce": true }
A missing, empty or non-string mapped claim falls back to the standard claim. Changing claims.email does not merge existing accounts: a returning user is found by the provider's sub and keeps their original email unless overrideUserInfo is true, so set it when correcting a mapping after users exist.
When the callback fails after a successful login
If the provider's login succeeds and the browser comes back to the dashboard, but the callback then fails, the server-to-server code exchange was refused. Check the provider's token_endpoint_auth_methods_supported and set authentication to match. Only "basic" and "post" are accepted, not the discovery spellings client_secret_basic and client_secret_post. Most providers accept body credentials whatever their discovery document says; Globus advertises only client_secret_basic and still accepts them.
