5.6 KiB
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
permissionsclaim 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:
<namespace>:<permission>
Examples:
default:readpayments:writeanalytics:workersystem: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:
- A dedicated “authorization-only” client to own Temporal roles
- A reusable Client Scope to emit the
permissionsclaim - 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:admincomposed 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-authzclient roles - Users inherit them automatically
Service Accounts (client_credentials)
For each client that uses service accounts:
- Enable Service Accounts
- Go to Service Account Roles
- 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-authzclient - Emits them as a multi-valued
permissionsclaim
Step 5: Attach the Client Scope
For every client that should be allowed to call Temporal:
- Go to Clients → (client) → Client scopes
- Add
temporal-permissionsas 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)
Resulting Token Example
{
"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.