Identity Provider
The SCF Controls Platform can authenticate users through OpenID Connect (OIDC) single sign-on.
This is optional and off by default — with VITE_OIDC_ENABLED unset or false, the platform
keeps using the existing Google Sign-In / API-key authentication and nothing on this page applies.
When you turn it on, you have two paths:
- Bundled Keycloak — the stack can bring up a self-hosted Keycloak identity provider for you. No external IdP account required; users live in a dedicated schema on the bundled Postgres container. It requires the bundled database — see the warning below.
- Bring your own OIDC — point the platform at Okta, Microsoft Entra ID, Auth0, Google Workspace, or any standards-compliant OIDC provider. The generic relying-party layer (authorization code + PKCE, issuer discovery, JWKS) works with any of them.
The bundled IdP lives behind a Docker Compose profile, so it stays off until you ask for it.
Keycloak stores its realm and users in a dedicated keycloak schema on the existing Postgres
(KC_DB_SCHEMA=keycloak) and auto-imports its realm from idp/scf-realm.json on first boot.
-
Set the required variables in
.env. At minimum, turn on OIDC and give Keycloak its admin credentials (the IdP containers exit loudly ifKC_ADMIN_USER/KC_ADMIN_PASSWORDare empty):Terminal window VITE_OIDC_ENABLED=trueKC_ADMIN_USER=adminKC_ADMIN_PASSWORD=<a-strong-password>OIDC_CLIENT_SECRET=<openssl rand -hex 32>BOOTSTRAP_ADMIN_EMAIL=you@example.com # seeds a first platform admin (optional)See the environment variables table below for the full list and defaults.
-
Start the stack with the
idpprofile:Terminal window docker compose --profile idp up -dThis brings up Keycloak 26.3 (
quay.io/keycloak/keycloak:26.3) plus a one-shotidp-initcontainer.idp-initwritesOIDC_CLIENT_SECRETinto the realm’s client and — ifBOOTSTRAP_ADMIN_EMAILis set — creates that admin user with a random temporary password. It is idempotent, so re-running the profile is safe. -
Read the one-time temporary password from the
idp-initlogs. The password is printed once, only to that container’s logs:Terminal window docker compose logs idp-initCopy the temporary password for the bootstrapped admin before continuing.
-
Sign in for the first time. Open the app at
http://localhost:5173, click Sign in, and you are redirected to Keycloak (http://localhost:8081). Enter yourBOOTSTRAP_ADMIN_EMAILand the temporary password. Keycloak then forces two required actions on this first login:- Update Password — replace the temporary password.
- Update Account Information — set your first and last name.
You are then redirected back through
/api/auth/callbackand land in the app. -
(Optional) Reach the Keycloak admin console at
http://localhost:8081/admin, signing in withKC_ADMIN_USER/KC_ADMIN_PASSWORD. This is where you create additional users, enable self-registration, or adjust the realm.
To use an existing provider (Okta, Microsoft Entra ID, Auth0, Google Workspace, or any compliant
OIDC IdP), do not start the idp profile. Just point the OIDC_* variables at your provider:
-
Register an application / client in your IdP as a confidential client with the authorization code flow and PKCE. Register the redirect URI
http://localhost:5173/api/auth/callback(adjust the host for your deployment) and note the client ID and secret. -
Set the OIDC variables in
.env:Terminal window VITE_OIDC_ENABLED=trueOIDC_ISSUER=https://your-idp.example.com/… # public issuer, byte-for-byte the token `iss`OIDC_DISCOVERY_URL=https://your-idp.example.com/… # bare realm/tenant base the backend fetchesOIDC_CLIENT_ID=<your-client-id>OIDC_CLIENT_SECRET=<your-client-secret>OIDC_REDIRECT_URI=http://localhost:5173/api/auth/callbackOIDC_SCOPES=openid email profileRead Choosing ISSUER vs DISCOVERY_URL before you fill these in — mixing them up is the most common cause of failed logins behind a reverse proxy.
-
Start the stack normally (no
idpprofile):Terminal window docker compose up -d -
Sign in. The app redirects to your IdP, the user authenticates there, and is returned through
/api/auth/callback. Users must already exist in your IdP — see User provisioning.
Deploying on a remote host
Section titled “Deploying on a remote host”The setup examples above use localhost, which is correct only when your browser runs on the same
machine as the Docker stack. If the stack runs on a different machine (a home server, a VM, a
cloud host), the sign-in redirect and token validation both involve URLs your browser must
reach — localhost won’t resolve there, and the first login fails.
Set these three in .env to the host’s reachable address (IP or DNS name):
KC_HOSTNAME=http://<docker-host>:8081 # what Keycloak advertisesOIDC_ISSUER=http://<docker-host>:8081/realms/scf # must byte-match the token `iss`OIDC_REDIRECT_URI=http://<docker-host>:5173/api/auth/callback # where the browser returns toOIDC_DISCOVERY_URLdoes not change. The backend fetches it server-side over the Docker network; it is never browser-facing.- If you remapped
KEYCLOAK_PORTorFRONTEND_PORT, use the remapped ports in the URLs above. - Behind a reverse proxy with TLS, use the public
https://URL for all three and addKC_PROXY_HEADERS=xforwardedto the keycloak service.
After changing them, restart the stack (docker compose --profile idp up -d) and verify discovery
returns the public issuer: curl http://<docker-host>:8081/realms/scf should return HTTP 200 and
the JSON’s issuer should match your OIDC_ISSUER exactly.
Environment variables
Section titled “Environment variables”These variables configure the OIDC relying-party layer and the bundled Keycloak. KC_* values only
apply when you run the idp profile.
| Variable | Default | Purpose |
|---|---|---|
VITE_OIDC_ENABLED | false | Frontend: use redirect-based OIDC sign-in instead of Google. Build-time (baked into the frontend image) |
OIDC_ISSUER | http://localhost:8081/realms/scf | PUBLIC issuer; must byte-match the token iss claim and be browser-reachable |
OIDC_DISCOVERY_URL | http://keycloak:8080/realms/scf | INTERNAL realm base the backend fetches (bare base, no /.well-known suffix) |
OIDC_CLIENT_ID | scf-platform | OIDC client ID |
OIDC_CLIENT_SECRET | changeme-generate-a-real-oidc-secret | Confidential client secret; idp-init writes it into Keycloak at boot |
OIDC_REDIRECT_URI | http://localhost:5173/api/auth/callback | OAuth callback URI (must be registered on the client) |
OIDC_SCOPES | openid email profile | Requested scopes |
KC_ADMIN_USER | admin | Keycloak admin user — REQUIRED with --profile idp. The backend also reads it to create Keycloak accounts at invite time — see User provisioning |
KC_ADMIN_PASSWORD | changeme-keycloak-admin | Keycloak admin password — REQUIRED with --profile idp. Also read by the backend for invite-time provisioning |
KC_HOSTNAME | http://localhost:<KEYCLOAK_PORT> | Public base URL Keycloak advertises; default follows a remapped KEYCLOAK_PORT. Set explicitly for remote hosts or a reverse proxy |
BOOTSTRAP_ADMIN_EMAIL | (blank) | If set, idp-init seeds this platform admin with a one-time random temp password printed to the idp-init logs |
Choosing ISSUER vs DISCOVERY_URL
Section titled “Choosing ISSUER vs DISCOVERY_URL”OIDC_ISSUER and OIDC_DISCOVERY_URL look similar but serve different roles, and confusing them is
the classic footgun — especially behind a reverse proxy.
After first login: assign an organization
Section titled “After first login: assign an organization”A freshly-provisioned OIDC user — bundled or bring-your-own — authenticates successfully but has zero organization memberships. Until an admin adds them to an organization, the app has no workspace to show them and they land on a loading state rather than data.
This applies to the bootstrapped BOOTSTRAP_ADMIN_EMAIL user too: it is a platform admin but
still needs an organization membership before a workspace appears.
To grant membership, an existing admin adds the user to an organization via Admin → User Management (or the org members API). See User Management for the workflow.
User provisioning
Section titled “User provisioning”What an invite does depends on which identity provider the platform is pointed at.
Bundled Keycloak
Section titled “Bundled Keycloak”When the platform runs the bundled Keycloak (docker compose --profile idp) and the backend has
KC_ADMIN_USER, KC_ADMIN_PASSWORD and OIDC_DISCOVERY_URL / OIDC_ISSUER configured, inviting
somebody from Settings › User Management › Invite User also creates their Keycloak account.
The backend looks the email up in the realm first:
- Not in the realm — it creates the user (username = the email address, enabled, email marked verified, required action Update Password) and sets a generated temporary password. The password is temporary, so Keycloak forces a new one at first sign-in.
- Already in the realm — nothing is changed and no password is set. The existing identity is left exactly as it is.
When the invite created the account, the invitee’s first sign-in accepts the invitation: after Keycloak has made them replace the temporary password they arrive in the organisation with the role and employment type on the invite, and they appear in User Management straight away. They do not need to open the invitation link, though the link still works if they do. The platform ties this to the Keycloak account it created (the subject on the invite), never to an email address, so an account the platform did not create is not joined this way — those invitees, like everyone on the paths below, join through the link.
Cancelling an invite deletes the Keycloak account only when the invite created it and the person has never signed in to the platform. In every other case the identity is left alone.
Every Keycloak account the platform creates or deletes is written to the platform audit log under
entity type idp_user.
Bring your own OIDC, or Google
Section titled “Bring your own OIDC, or Google”Nothing changes on these paths. The platform does not create accounts in an external identity provider: an admin still creates the account in the IdP as usual, and the invite links that identity to the organisation the first time the person signs in.
Reading the invite badges
Section titled “Reading the invite badges”Each outstanding invitation in the pending-invite list carries a badge saying which of the above applied:
| Badge | Meaning |
|---|---|
| IdP account | An account exists in the bundled Keycloak — created by the invite, or already there before it |
| No IdP account | The bundled Keycloak is enabled, but no account exists for this email |
| External IdP | Provisioning is disabled — a bring-your-own OIDC or Google install, where the IdP owns accounts |
The BOOTSTRAP_ADMIN_EMAIL user seeded by idp-init is created independently of any invite, and
still is.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause | Fix |
|---|---|---|
| Login fails with a 404 fetching discovery | OIDC_DISCOVERY_URL has a /.well-known/openid-configuration suffix, giving a doubled path | Set OIDC_DISCOVERY_URL to the bare realm base — the backend appends the suffix itself |
| Signed in but stuck on the loading spinner, no workspace | The user has no organization membership | An admin adds them to an organization (see above) |
| User can’t sign in at the IdP at all | No account exists for them in the IdP | Check the badge on their pending invite. No IdP account on a bundled-Keycloak install means KC_ADMIN_USER / KC_ADMIN_PASSWORD were not configured for the backend when they were invited — cancel the invite and re-invite, or create the user in the Keycloak admin console. On External IdP installs, create the user in your own IdP |
idp-init or Keycloak container exits immediately | KC_ADMIN_USER / KC_ADMIN_PASSWORD are empty | Set both in .env; they are required with --profile idp |
| Login works locally but fails behind a reverse proxy | OIDC_ISSUER does not byte-match the public token iss | Set OIDC_ISSUER to the exact public issuer; keep OIDC_DISCOVERY_URL internal |
Sign-in redirects to localhost from another machine, or iss validation fails | Stack runs on a remote host but the browser-facing URLs still say localhost | Set KC_HOSTNAME, OIDC_ISSUER, and OIDC_REDIRECT_URI to the host’s reachable address — see Deploying on a remote host |
Related Guides
Section titled “Related Guides”- Authentication — Sign-in paths and account management
- Configuration — Environment variable reference
- Deployment — Bringing up the self-hosted stack
- User Management — Assigning organizations and roles

