# Temporal Authorization with Keycloak Using a Shared Client Scope This document describes a pattern for integrating **Temporal** authorization with **Keycloak** using Temporal’s **default JWT claims mapper and authorizer**. > Note: You do not strictly need separate OAuth2 clients for “web login” vs “Temporal worker”. A single Keycloak client can support both Authorization Code (browser login) and Client Credentials (service account) flows. Using separate clients is still recommended for least privilege, but reusing a single client is acceptable as long as role assignment and claim mapping are done carefully. The goal is to: * Avoid polluting **realm roles** with Temporal-specific permissions * Avoid duplicating **client roles** across multiple Temporal-using clients * Support **human users**, **service accounts**, and **multiple clients** * Emit the exact `permissions` claim that Temporal expects, without custom code --- ## Background: What Temporal Expects Temporal’s default authorizer looks for a JWT claim (by default named `permissions`) containing **multiple string values** in the format: ``` : ``` Examples: * `default:read` * `payments:write` * `analytics:worker` * `system:admin` Temporal does **not** understand Keycloak roles, groups, or scopes directly — it only evaluates this claim. Therefore, the entire Keycloak setup is about **reliably producing a `permissions[]` claim** in access tokens. --- ## Design Overview We use three Keycloak concepts together: 1. **A dedicated “authorization-only” client** to own Temporal roles 2. **A reusable Client Scope** to emit the `permissions` claim 3. **Role assignments** on users, groups, and service accounts This design works whether you: - use separate Keycloak clients for your web UI vs worker, or - reuse the same Keycloak client for both (by enabling service accounts on it) ### Why this design? | Problem | Why this solves it | | -------------------------- | --------------------------------------------- | | Realm roles feel cluttered | Temporal roles live in one obvious place | | Multiple Temporal clients | Roles are shared, not duplicated | | Service accounts | Work naturally via service-account users | | Temporal defaults | No custom authorizer or claim mapper required | --- ## Step 1: Create a Temporal Authorization Client Create a new Keycloak client whose only purpose is to **hold Temporal permission roles**. Example: * **Client ID:** `temporal-authz` * **Access Type:** Confidential (details don’t matter much) * No login flows needed ### Create Client Roles on `temporal-authz` Create client roles whose names exactly match Temporal’s expected format: ``` default:read default:write default:worker default:admin payments:worker payments:admin ``` > Tip: You may use **composite roles** (e.g. `namespace:admin` composed of read/write/worker) to reduce assignment complexity. This client is now the **single source of truth** for Temporal permissions. --- ## Step 2: Assign Temporal Roles ### Human Users * Prefer assigning roles via **groups** * Groups can be named however you like (`temporal-default-writers`, etc.) * Groups receive `temporal-authz` client roles * Users inherit them automatically ### Service Accounts (client_credentials) For each client that uses service accounts: 1. Enable **Service Accounts** 2. Go to **Service Account Roles** 3. Assign roles from `temporal-authz` Keycloak will treat the service account as a user, and role mapping works the same way. #### Reusing a single client for web login + service account If you want to reuse the same Keycloak client for both: - web login (Authorization Code + PKCE) and - the Temporal worker (Client Credentials / service account) the critical rule is: **assign Temporal worker permissions to the service account user, not to regular users**. This way: - the service account token includes `permissions: ["default:worker", ...]` (Temporal can poll task queues), but - user tokens do not accidentally include `default:worker` (reduces the impact of a leaked user token). --- ## Step 3: Create a Client Scope to Emit Permissions Create a **Client Scope**: * **Name:** `temporal-permissions` * **Type:** Optional * **Display on consent screen:** Off * **Include in token scope:** Off This scope is responsible for producing the `permissions` claim. --- ## Step 4: Add a Protocol Mapper Inside the `temporal-permissions` client scope, add a mapper: ### Mapper Configuration * **Mapper Type:** User Client Role (or similar, depending on Keycloak version) * **Client ID:** `temporal-authz` * **Token Claim Name:** `permissions` * **Claim JSON Type:** `String` * **Multivalued:** On * **Add to access token:** On * **Add to ID token:** Off (usually) * **Add to userinfo:** Off (usually) This mapper: * Reads roles assigned from the `temporal-authz` client * Emits them as a multi-valued `permissions` claim --- ## Step 5: Attach the Client Scope For every client that should be allowed to call Temporal: 1. Go to **Clients → (client) → Client scopes** 2. Add `temporal-permissions` as a **Default Client Scope** Now any token issued for that client from a user login or service account will include the `permissions[]` claim (if roles are assigned) Gotchas: - Make sure that the dedicated client scope has either "Full Scope Allowed" enabled, or disable it and assign the `temporal-authz` client roles to it. Otherwise, the mapper won't see any roles to map. --- ## Resulting Token Example ```json { "sub": "…", "iss": "https://keycloak.example/realms/myrealm", "aud": "temporal-client", "permissions": [ "default:worker", "default:write" ] } ``` Temporal’s default authorizer can consume this **without any customization**.