- Create QUICK_REFERENCE.md for a concise guide on setting up temporal authorization. - Add README_AUTHORIZATION.md detailing the implementation steps and common issues. - Introduce REVERSE_PROXY_APPROACH.md as an alternative method for authorization using a reverse proxy. - Implement Dockerfile for building a custom Temporal server with authorization features. - Add main.go to initialize the custom Temporal server with JWT authorization. - Create example-keycloak-mapper.json for mapping Keycloak groups to Temporal permissions. - Add development.yaml for configuring the Temporal server with JWT settings. - Implement test-authorization.sh script to verify JWT token claims and Temporal server access. - Include go.mod for managing Go dependencies in the custom server. - Document troubleshooting steps and customization options in README.md.
Custom Temporal Server with OIDC Authorization
This directory contains the code to build a custom Temporal server with JWT-based authorization support for your Keycloak/OIDC setup.
Quick Start
1. Update your Docker Compose
Edit your compose.yaml to build and use the custom server:
services:
temporal:
build:
context: ./custom-server
dockerfile: Dockerfile
depends_on:
- db
configs:
- source: entrypoint
target: /entrypoint.sh
mode: 0555
- source: dynamicconfig
target: /etc/temporal/config/dynamicconfig/development-sql.yaml
entrypoint: /entrypoint.sh
command: "autosetup"
environment:
- DB=postgres12
- DB_PORT=5432
- POSTGRES_USER=temporal
- POSTGRES_PWD_FILE=/run/secrets/db_password
- POSTGRES_SEEDS=db
- DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development-sql.yaml
# Add this new environment variable for JWKS endpoint
- TEMPORAL_AUTH_ISSUER_URL=${TEMPORAL_AUTH_ISSUER_URL}
networks:
- internal
secrets:
- db_password
2. Set Environment Variables
Add to your .env file:
# Your Keycloak realm issuer URL (without .well-known/openid-configuration)
TEMPORAL_AUTH_ISSUER_URL=https://your-keycloak.com/realms/your-realm
3. Configure Keycloak
Import the Protocol Mapper
- Go to Keycloak Admin Console
- Navigate to: Clients → [your temporal-ui client] → Client scopes → [client-scope] → Mappers
- Click "Add mapper" → "By configuration" → "Script Mapper"
- Import settings from
example-keycloak-mapper.jsonor create manually
Create Groups
Create the following groups in Keycloak (or customize the mapper script):
temporal-admins- Full system accessdev-team- Development namespace accessops-team- Production namespace accessqa-team- Read-only accesstemporal-workers- Worker service accounts
Assign Users to Groups
- Go to Users → [user] → Groups
- Add users to appropriate groups
4. Build and Deploy
# Build the custom server
docker-compose build temporal
# Deploy
docker stack deploy temporal --detach=true -c compose.yaml
# Or with docker-compose
docker-compose up -d
5. Test
- Log in to Temporal UI as a user in the
dev-teamgroup - Try to access the
developmentnamespace - should work - Try to start a workflow - should work (write access)
- Try to access
productionnamespace - should only be able to view (read access)
Log in as a user in the qa-team group:
- Should only be able to view workflows
- Should not be able to start or terminate workflows
Customization
Modify Permission Mappings
Edit the script in example-keycloak-mapper.json to match your organization's structure. The format is:
permissions.add('<namespace>:<role>');
Where:
<namespace>is your Temporal namespace name<role>is one of:read,write,worker,admin
Examples:
permissions.add('production:admin'); // Full access to production
permissions.add('development:write'); // Read and write to development
permissions.add('staging:read'); // Read-only access to staging
permissions.add('temporal-system:admin'); // Cluster admin
Use Different Claim Names
If you want to use a different claim name instead of permissions, edit config/development.yaml:
global:
authorization:
permissionsClaimName: "temporal_perms" # Use this claim from JWT
Advanced: Custom ClaimMapper
If you need more complex logic (like mapping based on email domains, user attributes, or multiple claims), you can implement a custom ClaimMapper.
Create a new file custom-server/custom_claim_mapper.go:
package main
import (
"encoding/json"
"strings"
"go.temporal.io/server/common/authorization"
)
type customClaimMapper struct{}
func newCustomClaimMapper() authorization.ClaimMapper {
return &customClaimMapper{}
}
func (c *customClaimMapper) GetClaims(authInfo *authorization.AuthInfo) (*authorization.Claims, error) {
claims := &authorization.Claims{}
if authInfo.AuthToken == "" {
return claims, nil
}
// Parse JWT - simple example, use a proper JWT library in production
parts := strings.Split(authInfo.AuthToken, ".")
if len(parts) != 3 {
return claims, nil
}
// Decode payload (you should use a proper JWT library)
// This is just for illustration
var payload map[string]interface{}
// ... decode base64 and unmarshal JSON
// Extract groups from JWT
groups, ok := payload["groups"].([]interface{})
if !ok {
return claims, nil
}
// Map groups to Temporal roles
claims.Namespaces = make(map[string]authorization.Role)
for _, g := range groups {
group := g.(string)
switch group {
case "temporal-admins":
claims.System = authorization.RoleAdmin
case "dev-team":
claims.Namespaces["development"] = authorization.RoleWriter
claims.Namespaces["staging"] = authorization.RoleWriter
claims.Namespaces["production"] = authorization.RoleReader
case "ops-team":
claims.Namespaces["production"] = authorization.RoleAdmin
}
}
return claims, nil
}
Then update main.go to use it:
temporal.WithClaimMapper(func(cfg *config.Config) authorization.ClaimMapper {
return newCustomClaimMapper()
})
Troubleshooting
Enable Debug Logging
Add to your environment variables:
environment:
- LOG_LEVEL=debug
Check JWT Token
Use this to decode and inspect your JWT token:
# Get token from browser (F12 → Network → find request with Authorization header)
TOKEN="your.jwt.token"
# Decode (install jq first: apt-get install jq)
echo $TOKEN | cut -d'.' -f2 | base64 -d | jq .
Look for the permissions array.
Common Issues
Error: "PermissionDenied"
- Check that the JWT contains the
permissionsclaim - Verify the permissions are in the correct format:
"namespace:role" - Ensure the JWKS URL is accessible from the Temporal server container
Error: "InvalidToken" or "TokenExpired"
- Check that
TEMPORAL_AUTH_ISSUER_URLis correct - Verify the JWKS endpoint is reachable:
https://your-keycloak.com/realms/your-realm/protocol/openid-connect/certs - Ensure token hasn't expired (check
expclaim)
Server won't start
- Check server logs:
docker logs temporal_temporal_1 - Verify go.mod dependencies:
cd custom-server && go mod tidy - Ensure config file syntax is correct:
yamllint config/development.yaml
View Server Logs
# Docker Compose
docker-compose logs -f temporal
# Docker Swarm
docker service logs -f temporal_temporal
Files in This Directory
main.go- Custom Temporal server entrypoint with authorizationgo.mod- Go module dependenciesconfig/development.yaml- Temporal configuration with JWT settingsDockerfile- Multi-stage build for custom serverexample-keycloak-mapper.json- Keycloak protocol mapper configurationREADME.md- This file