Keycloak OIDC setup
Audience: IdP administrators wiring Keycloak as the OpenID Connect provider for TokenTimer Enterprise.
Multi-provider SSO: Configure Keycloak under System Settings → Manage SSO providers (or bootstrap via
sso-providers.json/ HelmssoProviders[]). Sign-in and callback URLs are slug-scoped (/auth/oidc/<slug>). See the OIDC quickstart first.
TokenTimer Enterprise uses OpenID Connect (authorization code flow). This guide covers Keycloak realm + client setup and the matching provider fields in SSO Providers or your bootstrap manifest.
Related docs:
- SSO identity provider guides: all provider guides and shared URLs
- Configuration reference: bootstrap env vars and provider
configfields - Claim mapping: claim paths (including Keycloak groups)
- Group mappings: map IdP groups to TokenTimer roles
- Authentication model: bootstrap modes (local admin vs SSO-only)
Prerequisites
| Requirement | Notes |
|---|---|
Enterprise license with feature:oidc | Without it, SSO buttons and SSO Providers are hidden |
| Public API URL | Browser and Keycloak must reach the same host the API uses for callbacks (API_URL) |
| HTTPS in production | Session cookies expect TLS in front of the API and dashboard (APP_URL) |
TRUST_PROXY_HOPS | Set to match your ingress chain (default 2 for LB then ingress). Wrong values break callback URL generation behind TLS |
| Ingress routes | Route /auth/* and /api/* to the API service. OIDC lives at /auth/oidc/<slug> on the same host as the JSON API |
URLs TokenTimer expects
Replace https://tokentimer.example.com with your deployment and <slug> with
your provider slug (e.g. keycloak).
| Purpose | Path |
|---|---|
| Sign-in start (browser) | https://tokentimer.example.com/auth/oidc/<slug> |
| OAuth redirect URI (register in Keycloak) | https://tokentimer.example.com/auth/oidc/<slug>/callback |
| Login page (dashboard) | https://tokentimer.example.com (APP_URL) |
Default when config.callback_url is unset on the provider row:
{API_URL}/auth/oidc/<slug>/callback
API_URL is the API host origin (e.g. https://tokentimer.example.com); the
server appends /auth/oidc/<slug>/callback itself. See the SSO identity provider guides for the
shared URL layout.
1. Create a realm
-
Sign in to the Keycloak admin console.
-
Create a realm (example:
tokentimer) or use an existing corporate realm. -
Note the issuer URL. It must match what TokenTimer sends on every token:
https://<keycloak-host>/realms/<realm-name>Use a trailing slash only if your Keycloak build consistently emits
isswith it; TokenTimer compares the issuer claim to the provider Issuer field exactly. When in doubt, use no trailing slash (Keycloak default in discovery). -
Open
https://<keycloak-host>/realms/<realm-name>/.well-known/openid-configurationin a browser orcurland confirm it returns JSON (discovery is used unless you disable discovery on the provider). If the API pod cannot reach discovery from the cluster, TokenTimer still applies Keycloak-shaped fallbacks (/protocol/openid-connect/auth, not/authorize); confirm with Test in SSO Providers or set the four endpoint overrides explicitly.
2. Create the OIDC client
In Clients, Create client:
| Setting | Value |
|---|---|
| Client type | OpenID Connect |
| Client ID | tokentimer (or your naming standard; must match Client ID in SSO Providers) |
| Name | TokenTimer Enterprise (optional) |
On Capability config:
| Setting | Value |
|---|---|
| Client authentication | On (confidential client) |
| Authorization | On |
| Standard flow | On |
| Direct access grants | Off (recommended for production) |
| Implicit / hybrid | Off |
On Login settings:
| Setting | Value |
|---|---|
| Root URL | https://tokentimer.example.com (APP_URL) |
| Home URL | optional |
| Valid redirect URIs | https://tokentimer.example.com/auth/oidc/<slug>/callback |
| Valid post logout redirect URIs | optional (APP_URL or /login) |
| Web origins | https://tokentimer.example.com (dashboard origin; add + if Keycloak suggests it) |
PKCE
TokenTimer uses Authorization Code + PKCE (S256) by default on each OIDC provider. Keycloak public/confidential clients both work; no extra Keycloak configuration is required beyond the redirect URI.
Client secret
Open the client, Credentials tab, copy the Client secret into
SSO Providers or SSO_<SLUG>_OIDC_CLIENT_SECRET.
3. Scopes
TokenTimer default scopes: openid profile email (set on the provider row).
If you rely on a custom groups claim in the userinfo or ID token, add the
scope your mappers need (often openid profile email is enough when mappers
attach groups without an extra scope).
4. Emit groups for role mapping
TokenTimer resolves workspace and system roles from IdP group values (see Group mappings). Keycloak does not send groups unless you map them.
Pick one approach.
Option A: Realm roles (common)
- Realm roles: create roles (example:
tokentimer-admins,tokentimer-viewers). - Assign roles to users (or via group role mappings).
- Client, Client scopes,
tokentimer-dedicated(or add mapper on assigned scope), Add mapper, User Realm Role. - Mapper settings:
- Token Claim Name: leave default or use nested structure; TokenTimer
default paths include
realm_access.roles. - Add to ID token: On (recommended)
- Add to userinfo: On (recommended)
- Add to access token: optional
- Token Claim Name: leave default or use nested structure; TokenTimer
default paths include
In SSO Providers under Claim mapping:
| Field | Paths |
|---|---|
| Groups | groups, realm_access.roles |
Option B: Keycloak groups (recommended for devops-style mappings)
Keycloak does not include group membership in tokens by default. Until you
add a mapper, TokenTimer audit rows show idp_groups_seen: 0 and group
mappings never match (even if System Settings → Single Sign-On
is correct).
Step 1: Create the group and assign users
- Keycloak Admin, your realm (example:
tt-test), Groups. - Create group (example:
devops). Use the short name you will map in TokenTimer (not a full path unless you enable full paths in step 2). - Open the group, Members, add users (example:
admin@test.com).
Step 2: Add a Group Membership mapper on the client
- Clients, your TokenTimer client (example client ID:
tokentimer). - Client scopes tab, open the dedicated scope for this client
(name is often
<client-id>-dedicated), or use the client's own Client scopes / Mappers tab if your Keycloak version exposes mappers there directly. - Add mapper, By configuration, Group Membership.
- Use these settings:
| Setting | Value |
|---|---|
| Name | groups (any label) |
| Token Claim Name | groups |
| Full group path | Off (unless TokenTimer mappings use paths like /devops) |
| Add to ID token | On (recommended; simplest for TokenTimer) |
| Add to userinfo | On (optional but fine) |
| Add to access token | optional |
- Save. Existing user sessions may need a fresh login (log out of TokenTimer and sign in via Keycloak again) before the claim appears.
Step 3: Align TokenTimer claim mapping
SSO Providers, your Keycloak provider, Claim mapping, Groups:
groups
Step 4: Add the TokenTimer group mapping and re-login
- System Settings → Single Sign-On: map IdP group
devopsto System Admin (global) (or workspace roles as needed). See Group mappings. - Log out of TokenTimer and sign in again via Keycloak.
Verify
After login, check audit metadata (SQL example):
SELECT metadata->>'idp_groups_seen', metadata->'matched_groups', metadata->>'admin_granted'
FROM audit_events
WHERE action = 'LOGIN_SUCCESS' AND metadata->>'method' = 'oidc'
ORDER BY id DESC LIMIT 1;
Expect idp_groups_seen above 0, matched_groups containing your group name, and
admin_granted = true when mapped to system admin.
Suggested claim paths in SSO Providers:
| Field | Paths |
|---|---|
| Groups | groups |
Option C: Client roles
Use only if you deliberately scope roles per client. Map with a User Client
Role mapper and set the groups claim path to the claim name you configure
(often under resource_access.<client-id>.roles).
5. Claim paths in TokenTimer
Align SSO Providers under Claim mapping with what Keycloak emits:
| Claim | Suggested paths (Keycloak) |
|---|---|
email | |
| Display name | name or preferred_username |
| Groups | groups, realm_access.roles |
| Subject (stable ID) | sub |
See Claim mapping for precedence and nested-path rules.
6. Bootstrap system admin (optional)
For SSO-only installs with no local admin password:
- Create a realm role or group (example:
tokentimer-admins). - Assign your first operator to that role/group.
- Set either:
- Bootstrap manifest:
config.bootstrap_admin_groups=tokentimer-adminson the provider row - Or a row in System Settings → Single Sign-On with role
adminand workspace Global.
- Bootstrap manifest:
The bootstrap field is an IaC shortcut; prefer System Settings → Single Sign-On for ongoing governance.
7. Wire TokenTimer
Docker Compose (bootstrap manifest)
cd compose
cp sso-providers.basic-oidc.example.json sso-providers.json
# Edit slug to keycloak, issuer, and client_id for your realm (the bundled OIDC example uses slug entra).
# Merge SSO placeholders into .env (see the Configuration reference and the Compose SSO README):
# grep -v '^#' .env.sso-basic.example >> .env
APP_URL=https://tokentimer.example.com
API_URL=https://tokentimer.example.com
SSO_SECRETS_KEYRING_FILE=/run/secrets/sso-keyring.json
SSO_PROVIDERS_BOOTSTRAP_FILE=/etc/tokentimer/sso-providers.json
SSO_KEYCLOAK_OIDC_CLIENT_SECRET=<from Keycloak Credentials tab; must match slug in sso-providers.json>
TRUST_PROXY_HOPS=2
Core OIDC fields live on the provider row in SSO Providers or the bootstrap manifest. Saves in SSO Providers reload strategies on all API pods. Bootstrap-only changes require an API pod restart.
Helm (helm/examples/values-sso-basic-oidc.yaml pattern)
ssoProviders:
- slug: keycloak
type: oidc
displayName: "Keycloak"
enabled: true
oidc:
issuer: "https://keycloak.example.com/realms/tokentimer"
clientId: "tokentimer"
clientSecretRef:
name: keycloak-oidc
key: SSO_KEYCLOAK_OIDC_CLIENT_SECRET
scopes: "openid profile email"
claim_mapping:
groups: "groups,realm_access.roles"
tokentimer:
config:
baseUrl: "https://tokentimer.example.com"
apiUrl: "https://tokentimer.example.com"
trustProxyHops: 2
SSO Providers UI
After the API is up and you have a system admin session, configure the same fields
under SSO Providers. Saves reload strategies without a pod restart. GitOps field
locks (SSO_<SLUG>_* env vars) take precedence and show as Managed by env.
8. Local dev with Keycloak (sidecar)
For a minimal compose-network local setup, contact TokenTimer support; the internal QA guide is not included in the delivery tarball.
Summary:
| Item | Example |
|---|---|
| Keycloak | http://localhost:8089 (admin console) |
| Issuer (from API container) | http://<container-name>:8080/realms/tt-test |
| Redirect URI | http://localhost:4000/auth/oidc/<slug>/callback |
| Browser login | http://localhost:5173/login |
Use the internal hostname (kc, host.docker.internal, etc.) in the
provider Issuer and token endpoint overrides so the API container can reach Keycloak. Keep
redirect URIs on hosts/ports the user's browser can open.
9. Verify
- License: log in as admin and open System Settings → License (valid license with
feature:oidc). Or callGET /api/license/statuswith your session cookie (unauthenticatedcurlreturns 401). - Features:
curl -s https://tokentimer.example.com/api/auth/featuresshould list your slug inproviders.oidc[]withsignInReady: true. - Login: open
APP_URL/login, click Sign in with Keycloak (or your providerdisplayName), complete Keycloak login, land on the dashboard. - Audit: check
LOGIN_SUCCESSwithmethod=oidcandidp_groups_seenabove 0 when groups are configured. - Mappings: confirm System Settings → Single Sign-On mappings match Keycloak group/role names (matching is case-insensitive).
10. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No provider button on login | License missing feature:oidc/feature:saml, provider disabled, or signInReady: false | Fix license (System Settings → License); enable provider and run Test in SSO Providers; confirm slug in GET /api/auth/features providers.oidc[] or providers.saml[] |
| Redirect URI mismatch | Keycloak client URIs do not match callback URL | Align exactly: scheme, host, and /auth/oidc/<slug>/callback path |
Invalid issuer | Issuer does not match token iss | Match discovery issuer string (slash, host, realm) in SSO Providers |
| Discovery errors / offline IdP | API cannot reach /.well-known/openid-configuration | Fix network policy from API pods; Keycloak realms still get /protocol/openid-connect/* fallbacks, or set overrides / use Test in SSO Providers |
Redirect to /realms/.../authorize (404) | Wrong authorization path (Auth0-shaped fallback) | Use realm issuer URL; ensure discovery works or set authorization URL to .../protocol/openid-connect/auth |
| Login works, no roles | Groups claim empty | Add Group Membership or Realm Role mapper; fix groups claim paths in SSO Providers |
| HTTP callback behind TLS | TRUST_PROXY_HOPS too low | Set to number of proxies in front of API |
local_account_exists_auto_link_disabled | Email already used by local user | Migrate user or set SSO_ALLOW_AUTO_LINK_LOCAL_ACCOUNTS=true (see the Authentication model) |
| Callback 404 | Ingress sends /auth to wrong service | Route /auth prefix to API (see Helm ingress examples) |
SAML instead of OIDC
Keycloak also supports SAML. TokenTimer can run multiple OIDC and SAML providers
side by side when licensed and configured; most deployments pick one IdP protocol
per realm. For SAML, add a second provider row (type saml) and Keycloak's
SAML client type; claim mapping uses per-provider attribute paths. Verify
with GET /api/auth/features returning your slug in providers.saml[] with
signInReady: true. OIDC is the recommended path for new Keycloak integrations.