This guide covers running and testing the OIDC authentication flow locally. This is the auth mode used for enterprise self-hosted deployments where all users sign in through their organization's identity provider (Keycloak, Okta, Auth0, Microsoft Entra ID, etc.).
In OIDC mode (AUTH_MODE=oidc), the app has no login page. Unauthenticated users are immediately redirected through a chain:
- App detects no session, redirects to backend's OIDC sign-in endpoint
- Backend redirects to the OIDC provider's authorization endpoint
- User authenticates with their identity provider (corporate SSO)
- Provider redirects back to backend with an auth code
- Backend exchanges code for tokens, creates/updates user + session
- Backend redirects to frontend — user is authenticated
Any OIDC-compliant provider works — the implementation uses standard OIDC discovery (.well-known/openid-configuration).
The docs/mozilla-realm.json file contains a ready-to-go realm with a client and test users. Mount it on startup so there's zero manual setup:
cd backend # run from backend/ so the volume mount path resolves correctly
docker run -d \
--name keycloak \
-p 8180:8080 \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
-v $(pwd)/docs/mozilla-realm.json:/opt/keycloak/data/import/mozilla-realm.json \
quay.io/keycloak/keycloak:latest \
start-dev --import-realmThis creates:
- Realm:
mozilla - Client:
thunderbolt-app(secret:thunderbolt-dev-secret) - Users:
mitchell@mozilla.org/password,laura@mozilla.org/password
Keycloak admin panel is at http://localhost:8180 (login: admin / admin).
Backend (backend/.env):
AUTH_MODE=oidc
WAITLIST_ENABLED=false
OIDC_CLIENT_ID=thunderbolt-app
OIDC_CLIENT_SECRET=thunderbolt-dev-secret
OIDC_ISSUER=http://localhost:8180/realms/mozilla
# The SSO plugin validates discovery URLs against trusted origins — include the IdP origin
TRUSTED_ORIGINS=http://localhost:1420,http://localhost:8180Frontend (.env.local in project root, or whatever your local .env file is called):
VITE_AUTH_MODE=sso
# Make sure VITE_BYPASS_WAITLIST is NOT set (or set to false) — it skips the auth gate entirely# Terminal 1 — backend
cd backend && bun dev
# Terminal 2 — frontend
bun devOpen http://localhost:1420 — you should be redirected to Keycloak's login page for the "mozilla" realm. Sign in as mitchell@mozilla.org / password.
The realm import file at docs/mozilla-realm.json defines everything Keycloak needs. To modify it:
- Add users: Add entries to the
usersarray withusername,email,credentials - Change client secret: Update
clients[0].secretand yourOIDC_CLIENT_SECRETenv var - Change redirect URIs: Update
clients[0].redirectUris(must match your backend's callback URL)
After modifying the JSON, remove the old container and re-run the docker command:
docker rm -f keycloak
# Then run the docker command from step 1 againThe implementation is provider-agnostic. To use Okta, Auth0, Entra ID, or any other OIDC provider, just set the three env vars:
# Okta example
OIDC_CLIENT_ID=0oaXXXXXXXXXXXXXXX
OIDC_CLIENT_SECRET=your-client-secret
OIDC_ISSUER=https://your-org.okta.com
# Auth0 example
OIDC_CLIENT_ID=your-client-id
OIDC_CLIENT_SECRET=your-client-secret
OIDC_ISSUER=https://your-tenant.auth0.com
# Microsoft Entra ID example
OIDC_CLIENT_ID=your-app-registration-id
OIDC_CLIENT_SECRET=your-client-secret
OIDC_ISSUER=https://login.microsoftonline.com/your-tenant-id/v2.0The only requirement is that the provider supports OIDC discovery at {OIDC_ISSUER}/.well-known/openid-configuration.
You'll need to register a callback URL with the provider:
https://<your-backend>/v1/api/auth/sso/callback/sso
Most OIDC providers maintain their own session. Logging out of Thunderbolt alone won't clear the provider session — the user will be silently re-authenticated on the next visit. This is expected SSO behavior. In enterprise deployments, users typically stay signed in via their corporate identity provider.
For staging on Render, you can't use a local OIDC provider. Options:
- Use your company's existing identity provider sandbox (ask for a client ID, secret, and test users)
- Deploy Keycloak as a Render Docker service using the same image and realm import
What you'll need from whoever manages the identity provider:
| Value | Maps to env var | Example |
|---|---|---|
| Issuer URL | OIDC_ISSUER |
https://keycloak.company.com/realms/thunderbolt |
| Client ID | OIDC_CLIENT_ID |
thunderbolt-app |
| Client secret | OIDC_CLIENT_SECRET |
(from provider's credentials page) |
You'll need to give them your callback URL to register:
https://<your-backend>.onrender.com/v1/api/auth/sso/callback/sso
| Symptom | Cause | Fix |
|---|---|---|
| App loads normally, no redirect to IdP | VITE_BYPASS_WAITLIST is set to true |
Remove it or set to false, restart frontend |
| App loads normally, no redirect to IdP | Stale auth session from a previous login | Clear site data (DevTools → Application → Storage → Clear site data) |
discovery_untrusted_origin error |
IdP origin not in TRUSTED_ORIGINS |
Add http://localhost:8180 to TRUSTED_ORIGINS in backend/.env |
discovery_unexpected_error error |
Keycloak is not running or not reachable | Run docker ps | grep keycloak and start it if needed |
| OIDC callback 404 | Wrong redirect URI in Keycloak client | Ensure redirectUris in realm JSON matches /v1/api/auth/sso/callback/sso |
Integration tests use oauth2-mock-server — a lightweight in-process OIDC server that needs no Docker:
cd backend && bun test src/auth/oidc-integration.test.ts| File | Purpose |
|---|---|
backend/src/auth/auth.ts |
Conditionally adds @better-auth/sso plugin when AUTH_MODE=oidc or saml |
backend/src/config/settings.ts |
authMode, oidcClientId, oidcClientSecret, oidcIssuer env vars |
backend/src/auth/oidc-integration.test.ts |
OIDC integration tests using mock OIDC server |
backend/docs/mozilla-realm.json |
Pre-configured Keycloak realm for local development (OIDC + SAML clients) |
src/lib/auth-mode.ts |
isSsoMode() — reads VITE_AUTH_MODE |
src/app.tsx |
SsoRedirect component, conditional routing for SSO vs consumer mode |
src/contexts/auth-context.tsx |
credentials: 'include' in SSO mode for cookie-based session bootstrap |