OIDC / SSO setup
How it works
Section titled “How it works”- calit acts as an OIDC relying party, running an authorization-code flow scoped to a single endpoint,
/api/oidc/login. Everything else (session cookie,/me, public booking pages) is unaffected. - The OIDC login is login-only: once the identity is verified, calit bridges into its own normal form-auth session. There is no ongoing OIDC session to manage.
- Account linking matches the token’s
emailclaim against an existing calit user’s settings email, but only when the provider marked itemail_verified. If no verified-email match exists, calit provisions a new account — but only whenSIGNUP_ENABLED=true. WithSIGNUP_ENABLED=false, an unmatched SSO login is rejected rather than silently creating an account. If the verified email matches more than one calit account, the SSO login is rejected (the user must sign in with a password instead) rather than calit picking one — resolve it by making the accounts’ settings emails unique. - Admin is grant-only, never demote: if
OIDC_ADMIN_GROUPis set and the token’sgroupsclaim contains it, the user gets calit admin on that login. Losing the group on a later login revokes the OIDC-granted admin — but an admin granted locally (via/me/users) is never demoted by OIDC group state.
1. Register a client with your provider
Section titled “1. Register a client with your provider”Every OIDC-compliant provider needs the same three facts from calit:
| Setting | Value |
|---|---|
| Redirect URI | ${APP_BASE_URL}/api/oidc/login (e.g. https://cal.example.com/api/oidc/login) |
| Scopes | openid email profile groups |
| Claims calit reads | sub, email, email_verified, groups |
2. Set the environment variables
Section titled “2. Set the environment variables”OIDC_ENABLED=trueOIDC_ISSUER_URL=https://idp.example.comOIDC_CLIENT_ID=calitOIDC_CLIENT_SECRET=change-meOIDC_ADMIN_GROUP=calit-adminsOIDC_ISSUER_URL is the base issuer URL — calit discovers the token/authorization/JWKS endpoints from ${OIDC_ISSUER_URL}/.well-known/openid-configuration. Leave OIDC_ADMIN_GROUP blank if no group should map to calit admin.
Example A — generic OIDC provider (any compliant IdP)
Section titled “Example A — generic OIDC provider (any compliant IdP)”This is the calit side only, and is identical for Keycloak, Auth0, Zitadel, Authentik, or any other compliant provider — only the issuer URL and client credentials change.
# calit — generic OIDC client config (any compliant provider)OIDC_ENABLED=trueOIDC_ISSUER_URL=https://idp.example.com # base issuer; calit discovers {issuer}/.well-known/openid-configurationOIDC_CLIENT_ID=calitOIDC_CLIENT_SECRET=<plaintext secret from the provider>OIDC_ADMIN_GROUP=calit-admins # group whose members get admin; blank = no OIDC admin# Register in your provider:# redirect URI : https://cal.example.com/api/oidc/login# scopes : openid email profile groups# client auth : client_secret_post (or client_secret_basic)Example B — Authelia (concrete)
Section titled “Example B — Authelia (concrete)”Authelia is configured on the provider side with a client block in configuration.yml, plus group membership in users_database.yml that drives the calit admin grant. Note the secret split: Authelia stores it hashed, calit’s OIDC_CLIENT_SECRET is the plaintext value you hashed.
# Authelia configuration.yml — identity_providers.oidc.clientsidentity_providers: oidc: clients: - client_id: calit client_name: calit # Generate: authelia crypto hash generate pbkdf2 --password '<plaintext>' # Store the HASH here; put the PLAINTEXT in calit's OIDC_CLIENT_SECRET. client_secret: '$pbkdf2-sha512$310000$...' public: false authorization_policy: two_factor redirect_uris: - https://cal.example.com/api/oidc/login scopes: [openid, email, profile, groups] token_endpoint_auth_method: client_secret_post# Authelia users_database.yml — group membership drives calit adminusers: pavel: disabled: false groups: - calit-admins # matches OIDC_ADMIN_GROUP -> admin in calit# calit .env — matching the Authelia client aboveOIDC_ENABLED=trueOIDC_ISSUER_URL=https://auth.example.comOIDC_CLIENT_ID=calitOIDC_CLIENT_SECRET=<the plaintext you hashed for Authelia>OIDC_ADMIN_GROUP=calit-adminsShared behaviour (applies to any provider)
Section titled “Shared behaviour (applies to any provider)”- Email must match and be verified. Account linking requires the token’s
emailclaim to equal an existing user’s settings email andemail_verified: trueon the token. An unverified email is treated the same as no match. - An ambiguous email is rejected. If the verified email matches more than one calit account, the SSO login is rejected (the user must sign in with a password instead) rather than calit picking one — resolve it by making the accounts’ settings emails unique.
- New-account creation is gated by
SIGNUP_ENABLED. With no matching local account, a new user is auto-provisioned only whenSIGNUP_ENABLED=true; otherwise the login is rejected. - Admin is grant-only, never a demotion.
OIDC_ADMIN_GROUPmembership grants calit admin on each login where present, and is revoked on a subsequent login if the group is removed — but a locally-granted admin (via/me/users) is never demoted by OIDC group state. - Silent failures look like
/login. A wrong issuer URL, client ID/secret, or a provider that isn’t reachable typically manifests as the user landing back on/loginrather than a clear error page — check calit’s logs and the provider’s client configuration first.