Skip to main content
Version: 0.8

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 / Helm ssoProviders[]). 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:

Prerequisites

RequirementNotes
Enterprise license with feature:oidcWithout it, SSO buttons and SSO Providers are hidden
Public API URLBrowser and Keycloak must reach the same host the API uses for callbacks (API_URL)
HTTPS in productionSession cookies expect TLS in front of the API and dashboard (APP_URL)
TRUST_PROXY_HOPSSet to match your ingress chain (default 2 for LB then ingress). Wrong values break callback URL generation behind TLS
Ingress routesRoute /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).

PurposePath
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

  1. Sign in to the Keycloak admin console.

  2. Create a realm (example: tokentimer) or use an existing corporate realm.

  3. 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 iss with it; TokenTimer compares the issuer claim to the provider Issuer field exactly. When in doubt, use no trailing slash (Keycloak default in discovery).

  4. Open https://<keycloak-host>/realms/<realm-name>/.well-known/openid-configuration in a browser or curl and 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:

SettingValue
Client typeOpenID Connect
Client IDtokentimer (or your naming standard; must match Client ID in SSO Providers)
NameTokenTimer Enterprise (optional)

On Capability config:

SettingValue
Client authenticationOn (confidential client)
AuthorizationOn
Standard flowOn
Direct access grantsOff (recommended for production)
Implicit / hybridOff

On Login settings:

SettingValue
Root URLhttps://tokentimer.example.com (APP_URL)
Home URLoptional
Valid redirect URIshttps://tokentimer.example.com/auth/oidc/<slug>/callback
Valid post logout redirect URIsoptional (APP_URL or /login)
Web originshttps://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)

  1. Realm roles: create roles (example: tokentimer-admins, tokentimer-viewers).
  2. Assign roles to users (or via group role mappings).
  3. Client, Client scopes, tokentimer-dedicated (or add mapper on assigned scope), Add mapper, User Realm Role.
  4. 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

In SSO Providers under Claim mapping:

FieldPaths
Groupsgroups, realm_access.roles

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

  1. Keycloak Admin, your realm (example: tt-test), Groups.
  2. 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).
  3. Open the group, Members, add users (example: admin@test.com).

Step 2: Add a Group Membership mapper on the client

  1. Clients, your TokenTimer client (example client ID: tokentimer).
  2. 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.
  3. Add mapper, By configuration, Group Membership.
  4. Use these settings:
SettingValue
Namegroups (any label)
Token Claim Namegroups
Full group pathOff (unless TokenTimer mappings use paths like /devops)
Add to ID tokenOn (recommended; simplest for TokenTimer)
Add to userinfoOn (optional but fine)
Add to access tokenoptional
  1. 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

  1. System Settings → Single Sign-On: map IdP group devops to System Admin (global) (or workspace roles as needed). See Group mappings.
  2. 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:

FieldPaths
Groupsgroups

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:

ClaimSuggested paths (Keycloak)
Emailemail
Display namename or preferred_username
Groupsgroups, 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:

  1. Create a realm role or group (example: tokentimer-admins).
  2. Assign your first operator to that role/group.
  3. Set either:
    • Bootstrap manifest: config.bootstrap_admin_groups=tokentimer-admins on the provider row
    • Or a row in System Settings → Single Sign-On with role admin and workspace Global.

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:

ItemExample
Keycloakhttp://localhost:8089 (admin console)
Issuer (from API container)http://<container-name>:8080/realms/tt-test
Redirect URIhttp://localhost:4000/auth/oidc/<slug>/callback
Browser loginhttp://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

  1. License: log in as admin and open System Settings → License (valid license with feature:oidc). Or call GET /api/license/status with your session cookie (unauthenticated curl returns 401).
  2. Features: curl -s https://tokentimer.example.com/api/auth/features should list your slug in providers.oidc[] with signInReady: true.
  3. Login: open APP_URL/login, click Sign in with Keycloak (or your provider displayName), complete Keycloak login, land on the dashboard.
  4. Audit: check LOGIN_SUCCESS with method=oidc and idp_groups_seen above 0 when groups are configured.
  5. Mappings: confirm System Settings → Single Sign-On mappings match Keycloak group/role names (matching is case-insensitive).

10. Troubleshooting

SymptomLikely causeFix
No provider button on loginLicense missing feature:oidc/feature:saml, provider disabled, or signInReady: falseFix 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 mismatchKeycloak client URIs do not match callback URLAlign exactly: scheme, host, and /auth/oidc/<slug>/callback path
Invalid issuerIssuer does not match token issMatch discovery issuer string (slash, host, realm) in SSO Providers
Discovery errors / offline IdPAPI cannot reach /.well-known/openid-configurationFix 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 rolesGroups claim emptyAdd Group Membership or Realm Role mapper; fix groups claim paths in SSO Providers
HTTP callback behind TLSTRUST_PROXY_HOPS too lowSet to number of proxies in front of API
local_account_exists_auto_link_disabledEmail already used by local userMigrate user or set SSO_ALLOW_AUTO_LINK_LOCAL_ACCOUNTS=true (see the Authentication model)
Callback 404Ingress sends /auth to wrong serviceRoute /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.