Files
member-console/Keycloak-setup.md

189 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Temporal Authorization with Keycloak Using a Shared Client Scope
This document describes a pattern for integrating **Temporal** authorization with **Keycloak** using Temporals **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
Temporals 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 dont matter much)
* No login flows needed
### Create Client Roles on `temporal-authz`
Create client roles whose names exactly match Temporals 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"
]
}
```
Temporals default authorizer can consume this **without any customization**.