Scrumboy supports OpenID Connect (OIDC) for single sign-on. After a successful OIDC login the user gets a normal scrumboy_session cookie — no JWTs are exposed to the browser.
Compatible providers: Keycloak, Auth0, Authentik, Zitadel, Azure AD (external tenant), Google Workspace (when configured as a standard OIDC client), and any provider with standard OIDC Discovery.
Set all four required environment variables and restart the server.
SCRUMBOY_OIDC_ISSUER=https://auth.example.com/realms/myrealm
SCRUMBOY_OIDC_CLIENT_ID=scrumboy
SCRUMBOY_OIDC_CLIENT_SECRET=your-client-secret
SCRUMBOY_OIDC_REDIRECT_URL=https://scrumboy.example.com/api/auth/oidc/callbackLocalhost example (e.g. Keycloak running locally):
SCRUMBOY_OIDC_ISSUER=http://localhost:8180/realms/dev
SCRUMBOY_OIDC_CLIENT_ID=scrumboy-local
SCRUMBOY_OIDC_CLIENT_SECRET=secret
SCRUMBOY_OIDC_REDIRECT_URL=http://localhost:8080/api/auth/oidc/callbackRequirements:
- All four variables must be set. Any missing variable disables OIDC entirely.
- The redirect URL must be registered at the provider exactly as written here.
- Restart the server after changing any of these values.
If configured correctly, a Continue with SSO button appears on the login screen (and on first-time setup when local password bootstrap is also available).
OIDC is disabled by default. Local password login remains available unless you also set:
SCRUMBOY_OIDC_LOCAL_AUTH_DISABLED=true- User clicks Continue with SSO on the login screen.
- Browser is redirected to the provider's authorization endpoint.
- User authenticates at the provider.
- Provider redirects back to
/api/auth/oidc/callbackwith an authorization code. - Scrumboy exchanges the code for tokens server-side, validates the ID token, and creates a session.
- Browser receives a
scrumboy_sessioncookie and is redirected to the original page.
All token handling happens on the server. The browser never sees access tokens or ID tokens.
- The ID token must include an
emailclaim (non-empty). email_verifiedmust betrue(boolean or string). If absent or false, login is denied.- Email is stored normalized to lowercase.
- On first OIDC login for a given
(issuer, subject),users.emailis set from the token. On later logins, Scrumboy does not sync email or display name from the IdP (matches current server behavior).
Identity
- Identity is tracked by
(issuer, subject)— the provider'siss+subclaims. - Email is not used as a join key. Changing your email at the provider does not break the link; the row in Scrumboy keeps the email from first login until you change it in-app or via admin.
- There is no account linking between local password accounts and OIDC accounts.
First user / ownership
- If no users exist when an OIDC login succeeds, that user becomes the instance owner — but only if the token's issuer matches
SCRUMBOY_OIDC_ISSUERexactly. - If a local password account was created first via bootstrap, subsequent OIDC users are created as normal users.
Display name
- Set from the
nameclaim at account creation. - Falls back to
preferred_username, then to the last segment ofsub. - Not updated on subsequent logins.
Scopes requested
openid email profile— fixed, not configurable.
Issuer normalization
Scrumboy strips trailing slashes and whitespace from SCRUMBOY_OIDC_ISSUER at startup. Set the issuer without a trailing slash to avoid confusion:
# Correct
SCRUMBOY_OIDC_ISSUER=https://auth.example.com/realms/myrealm
# Avoid (will be normalized, but don't rely on it)
SCRUMBOY_OIDC_ISSUER=https://auth.example.com/realms/myrealm/The issuer in the ID token's iss claim must match the normalized value character-for-character.
Redirect URL
- Must be an absolute URL.
- Must match the redirect URI registered at the provider exactly — including scheme, host, port, and path.
- Path is always
/api/auth/oidc/callback. - Do not derive it from request headers. Set it explicitly.
Behind a reverse proxy
OIDC works behind nginx, Caddy, Traefik, etc. as long as:
SCRUMBOY_OIDC_REDIRECT_URLreflects the public-facing URL (not the internal address).- The proxy forwards
X-Forwarded-Proto: httpsif terminating TLS — Scrumboy uses this to setSecureon the session cookie.
Discovery
Scrumboy fetches the provider's discovery document ({issuer}/.well-known/openid-configuration) on the first login attempt, not at startup. The app starts normally even if the provider is unreachable; OIDC is unavailable until discovery succeeds.
- One or more of the four required env vars is missing or empty.
- The server has not been restarted since the vars were set.
Check:
curl http://localhost:8080/api/auth/status | jq .oidcEnabledoidcEnabled only means the four env vars are set — discovery runs on the first click. The SSO button can appear even when discovery has not succeeded yet; clicking it returns 503 until the provider is reachable and discovery succeeds.
Discovery failed. Possible causes:
- Provider is unreachable from the Scrumboy server.
SCRUMBOY_OIDC_ISSUERpoints to the wrong URL (wrong realm, wrong host).- The issuer field in the discovery document does not match
SCRUMBOY_OIDC_ISSUERafter normalization.
Check the server logs for a line like:
oidc: discovery/login error: oidc discovery failed for "...": ...
SCRUMBOY_OIDC_REDIRECT_URLdoes not match the redirect URI configured at the provider.- Common mismatches:
httpvshttps, missing port, trailing slash.
The ID token was rejected. Common causes:
- Token audience (
aud) does not matchSCRUMBOY_OIDC_CLIENT_ID. - Issuer in the token does not match
SCRUMBOY_OIDC_ISSUER(after normalization). - Token has expired in transit (clock skew between server and provider).
- Wrong
client_secret.
Check server logs for oidc: callback error: token.
- Provider did not include an
emailclaim. email_verifiedisfalseor absent.- Enable email verification in the provider, or check whether the client/scope configuration returns the email claim.
A different user already has that email address (registered via local password login). Account linking is not supported. The server logs will contain:
oidc login aborted: email already in use by existing user (OIDC identity not linked)
Options: delete the local account via admin UI, or use a different email at the provider.
GET /api/auth/status must return "oidcEnabled": true. If it does not, see the first troubleshooting entry above. If it does, hard-refresh the browser (the SPA caches auth status for the page lifetime).
- Uses Authorization Code flow with PKCE (S256). The code verifier and state are stored server-side in memory with a 10-minute TTL.
- State and nonce are validated on every callback. Replayed or expired state values are rejected.
return_to(post-login redirect) is validated strictly: must be a path-only relative URL, no//, no://, no..segments, no fragments.- Session cookies are
HttpOnly,SameSite=Lax, andSecurewhen served over HTTPS. - The client secret never leaves the server.
The following are explicitly out of scope in the current version:
- IdP logout — clicking logout ends the Scrumboy session only; the provider session remains active.
- Role or group claim mapping — all OIDC users are created as
userrole (except the first user who becomesowner). - Account linking — an existing local-password account cannot be linked to an OIDC identity.
- Multiple OIDC providers — only one provider can be configured.
- Userinfo endpoint — claims are taken from the ID token only.
- Refresh token usage — sessions use the existing Scrumboy session TTL (30 days), not provider token lifetimes.
- Domain allowlists or invitation-only provisioning — any user the provider authenticates can log in.