189 lines
5.8 KiB
Markdown
189 lines
5.8 KiB
Markdown
# 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:
|
||
|
||
```
|
||
<namespace>:<permission>
|
||
```
|
||
|
||
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**.
|