2d71aa8d added the Token URL section to
docs/temporal-authorization-setup.md and, by mistake, deleted the
Keycloak walkthrough before it: the design overview and steps 1 to 5,
137 lines. They come back word for word. The correction #161 asks for
in that walkthrough stays with #161.
6.6 KiB
title, audience, summary
| title | audience | summary | ||
|---|---|---|---|---|
| Temporal Authorization Setup |
|
Configure your identity provider to emit the multi-valued permissions claim that Temporal's default JWT authorizer expects, using a dedicated authz client, a reusable client scope, and a protocol mapper. |
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)
Gotchas:
- Make sure that the dedicated client scope has either "Full Scope Allowed" enabled, or disable it and assign the
temporal-authzclient roles to it. Otherwise, the mapper won't see any roles to map.
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.
Token URL
Set temporal-oauth-token-url to an address the console reaches without
passing through the TLS proxy in front of the deployment, such as the
identity provider's internal service address. The Temporal connection then
works while the proxy is down or still obtaining certificates. The console
does not wait for this connection before it serves, so a proxy that asks
the console's /domains/ask before issuing a certificate
(fedwiki-setup.md)
gets its answer either way.