64 docs indexed
Configure OIDC single sign-on (Globus, CILogon, or any discovery-capable provider) for the obleth control-plane dashboard, alongside the break-glass local admin account.
The obleth dashboard supports two authentication methods that coexist: an OIDC SSO path for institutional identity providers and a local email/password admin that always works even when the IdP is unreachable.
obleth acts as an OIDC Relying Party. When SSO is configured, users sign in via their institution's IdP (Globus, CILogon, or any provider that exposes a discovery document). New SSO users land in a pending state with no access until an admin assigns them a role and a tenant on the dashboard's Users screen.
Two roles are available:
| Role | Access |
|---|---|
admin | Full dashboard: models, tenants, keys, usage, settings |
user | Self-service portal: read-only model list, manage own API keys, view own usage |
The local admin bypasses SSO and is always available as a break-glass account.
SSO is configured through environment variables on the control plane. Where you set them depends on how you run it:
deploy/docker/.env (read only by the Docker stack).npm run dev): control-plane/.env.local.controlPlane.* values (see Kubernetes below).Set the OIDC_PROVIDERS environment variable to a JSON array. Each entry describes one 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"]
}
]
| Field | Description |
|---|---|
providerId | Short identifier used in the callback URL (alphanumeric + hyphens) |
displayName | Label shown on the sign-in button |
discoveryUrl | OIDC discovery document URL for the provider |
clientId | Client ID from the IdP registration |
clientSecret | Client secret from the IdP registration |
scopes | OAuth scopes to request; openid email profile is sufficient for obleth |
When registering an obleth client in your IdP's developer console, use this redirect URI:
https://<dashboard-host>/api/auth/oauth2/callback/<providerId>
For example, with providerId: "globus" and a dashboard at https://dashboard.example.edu:
https://dashboard.example.edu/api/auth/oauth2/callback/globus
Note:
The host in the callback comes from BETTER_AUTH_URL — set it to the external, browser-facing URL of the dashboard (e.g. https://dashboard.example.edu). The redirect URI you register with the IdP must match it exactly, including scheme and port.
Discovery URL: https://auth.globus.org/.well-known/openid-configuration
Register a confidential client in the Globus Developer Console. Set the redirect URL above as an allowed redirect URI and request the openid email profile scopes.
{
"providerId": "globus",
"displayName": "Globus",
"discoveryUrl": "https://auth.globus.org/.well-known/openid-configuration",
"clientId": "YOUR_GLOBUS_CLIENT_ID",
"clientSecret": "YOUR_GLOBUS_CLIENT_SECRET",
"scopes": ["openid", "email", "profile"]
}
Discovery URL: https://cilogon.org/.well-known/openid-configuration
Register a client at cilogon.org/oauth2/register. Set the callback URL and request the openid email profile scopes.
{
"providerId": "cilogon",
"displayName": "CILogon",
"discoveryUrl": "https://cilogon.org/.well-known/openid-configuration",
"clientId": "cilogon:/client_id/YOUR_ID",
"clientSecret": "YOUR_CLIENT_SECRET",
"scopes": ["openid", "email", "profile"]
}
Pass multiple objects in the array to show a sign-in button for each:
[
{
"providerId": "globus",
"displayName": "Globus",
"discoveryUrl": "https://auth.globus.org/.well-known/openid-configuration",
"clientId": "...",
"clientSecret": "...",
"scopes": ["openid", "email", "profile"]
},
{
"providerId": "cilogon",
"displayName": "CILogon",
"discoveryUrl": "https://cilogon.org/.well-known/openid-configuration",
"clientId": "...",
"clientSecret": "...",
"scopes": ["openid", "email", "profile"]
}
]
A local email/password admin account is seeded on first boot and always works regardless of IdP availability:
| Variable | Default | Notes |
|---|---|---|
DASHBOARD_ADMIN_EMAIL | admin@example.com | Email used to sign in |
DASHBOARD_PASSWORD | — | Must be at least 8 characters; no default in production |
Set both in your deployment's environment (or .env for Docker Compose). The local admin has the admin role and is not subject to the pending-approval flow.
Change DASHBOARD_ADMIN_EMAIL and DASHBOARD_PASSWORD from any dev defaults before exposing the dashboard to a network.
The dashboard sign-in form now requires an email address, not a username. Two requirements must be met before the break-glass admin can be seeded on the upgraded control plane:
DASHBOARD_ADMIN_EMAIL must be a real email address. A bare value like admin fails validation and no admin account will be created.DASHBOARD_PASSWORD must be at least 8 characters. A shorter password causes the admin seed to fail; the error is logged and no account is created.Upgrade steps for an existing install:
DASHBOARD_ADMIN_EMAIL to a real email address (e.g. admin@your-org.example).DASHBOARD_PASSWORD is 8 or more characters.Institutional users who sign in via OIDC SSO are unaffected by this change; they sign in through their provider as before and land in the pending state until an admin assigns them a role and tenant (see Admin approval flow below).
When a user signs in via SSO for the first time, their account is created with a pending status. Pending users cannot access the dashboard or the self-service portal.
To grant access:
admin or user) and a tenant.Users assigned the user role land in the self-service portal rather than the full dashboard. They can view available models, manage their own API keys, and review their own usage. They cannot see other tenants' data or modify gateway configuration.
The Docker Compose stack ships a Dex container that acts as a local test identity provider, gated behind the sso-dev profile so it never starts in production. It comes preloaded with one test user:
researcher@example.edu / passwordThe Dex issuer must resolve to the same address from both your browser and the control-plane container. localhost cannot do that — inside the container it points at the container itself, not Dex — so the shipped config uses host.docker.internal, which the compose file gives Dex as a network alias.
Set the profile and provider in deploy/docker/.env (this file is read only by the Docker stack):
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"]}]
cd deploy/docker
docker compose up -d
Open the dashboard at http://localhost:3002 — you'll see a Sign in with Dev SSO (Dex) button. Sign in as the test user above; you'll land on an "awaiting approval" screen (expected — see Admin approval flow). That screen has a Sign out button so you can switch between the break-glass admin and SSO accounts while testing.
Note:
If you change the dashboard's published port from 3002, also update Dex's redirectURIs in deploy/docker/dex/config.docker.yaml and BETTER_AUTH_URL to match.
If you run the control plane on the host with npm run dev (port 3000) while only Dex runs in Docker, both the browser and the Node server reach Dex at plain localhost:5556. Put the config in control-plane/.env.local (the Docker .env is not read in this mode):
# control-plane/.env.local
BETTER_AUTH_URL=http://localhost:3000
OIDC_PROVIDERS=[{"providerId":"dex","displayName":"Dev SSO (Dex)","discoveryUrl":"http://localhost:5556/dex/.well-known/openid-configuration","clientId":"obleth-control-plane","clientSecret":"dev-dex-secret","scopes":["openid","email","profile"]}]
cd deploy/docker && docker compose --profile sso-dev up -d dex postgres
cd ../../control-plane && npm run dev # dashboard on :3000
Dex's users live in deploy/docker/dex/config.docker.yaml under staticPasswords. Add an entry (the password is a bcrypt hash) and restart Dex:
node -e "console.log(require('bcryptjs').hashSync('yourpassword',10))"
docker compose -f deploy/docker/docker-compose.yml restart dex
Dex storage is in-memory, so the static list is the source of truth and resets when the container restarts. In production, users live in your real provider (Globus/CILogon) — obleth never creates SSO identities itself, it only records an account the first time someone signs in.
The Helm chart exposes the same settings under controlPlane:
controlPlane:
# Break-glass admin (always works, even when the IdP is down)
dashboardAdminEmail: "admin@your-org.example"
dashboardPassword: "" # >= 8 chars; supply via --set or an external Secret
dashboardSessionSecret: "" # >= 32 chars; reused as BETTER_AUTH_SECRET
# OIDC SSO (optional; omit or leave empty for break-glass only)
betterAuthUrl: "https://dashboard.example.com"
oidcProviders: |
[{"providerId":"globus","displayName":"Globus","discoveryUrl":"https://auth.globus.org/.well-known/openid-configuration","clientId":"ID","clientSecret":"SECRET","scopes":["openid","email","profile"]}]
For production, store these in a pre-created Secret and reference it with controlPlane.existingSecret. Because the chart then renders no control-plane Secret of its own, that Secret must carry every key: DASHBOARD_PASSWORD, DASHBOARD_SESSION_SECRET, DATABASE_URL, DASHBOARD_ADMIN_EMAIL, BETTER_AUTH_URL, and OIDC_PROVIDERS. See deploy/k8s/obleth/examples/values-production.yaml and the Helm values reference.
Switching from local Dex to a live Globus or CILogon deployment requires only an environment variable swap — no code change:
OIDC_PROVIDERS to the real discovery URL and client credentials.Everything else — the approval flow, roles, the break-glass admin — stays the same.