Add local JWT development server with key generation scripts (#9647)

## What changed?
- Added `development-jwt.yaml` config that runs Temporal with JWT
authorization enabled using a JWKS file loaded from local disk. Loading
from local disk is added in
https://github.com/temporalio/temporal/pull/9590.
- Added `config/jwt/setup-keys.sh` to generate an RSA key pair and JWKS
file in `/tmp/temporal-jwt-test/`.
- Added `config/jwt/generate-token.sh` to create signed JWTs for
testing.
  - Added `make start-jwt` target.
 
## Why?
Testing JWT authentication currently requires an external identity
provider or manual key/token setup. This provides a self-contained local
workflow: `make start-jwt` generates keys automatically and starts the
server with auth enforced, and `generate-token.sh` produces tokens on
demand.

## How did you test it?
- [X] built
- [X] run locally and tested manually
- [ ] covered by existing tests
- [ ] added new unit test(s)
- [ ] added new functional test(s)

Tested this in conjunction with
https://github.com/temporalio/temporal/pull/9582. Was able to list the
workflows by passing the token and dumping their history. When the token
is not used, the auth fails with "Request denied".

## Potential risks
Keys are stored in /tmp/temporal-jwt-test/ which is world-readable. This
is acceptable for local development but the scripts should not be used
in shared or production environments.
This commit is contained in:
Vladyslav Simonenko
2026-04-15 16:26:53 -07:00
committed by GitHub
parent e5c7e41d71
commit 3081343d68
5 changed files with 326 additions and 0 deletions
+4
View File
@@ -697,6 +697,10 @@ start-xdc-cluster-b: temporal-server
start-xdc-cluster-c: temporal-server
./temporal-server --config-file config/development-cluster-c.yaml --allow-no-auth start
start-jwt: temporal-server
@./config/jwt/setup-keys.sh
./temporal-server --config-file config/development-jwt.yaml start --service frontend --service internal-frontend --service history --service matching --service worker
##### Grafana #####
update-dashboards:
@printf $(COLOR) "Update dashboards submodule from remote..."
+150
View File
@@ -0,0 +1,150 @@
log:
stdout: true
level: info
persistence:
defaultStore: sqlite-default
visibilityStore: sqlite-visibility
numHistoryShards: 1
datastores:
sqlite-default:
sql:
user: ""
password: ""
pluginName: "sqlite"
databaseName: "default"
connectAddr: "localhost"
connectProtocol: "tcp"
connectAttributes:
mode: "memory"
cache: "private"
maxConns: 1
maxIdleConns: 1
maxConnLifetime: "1h"
tls:
enabled: false
caFile: ""
certFile: ""
keyFile: ""
enableHostVerification: false
serverName: ""
sqlite-visibility:
sql:
user: ""
password: ""
pluginName: "sqlite"
databaseName: "default"
connectAddr: "localhost"
connectProtocol: "tcp"
connectAttributes:
mode: "memory"
cache: "private"
maxConns: 1
maxIdleConns: 1
maxConnLifetime: "1h"
tls:
enabled: false
caFile: ""
certFile: ""
keyFile: ""
enableHostVerification: false
serverName: ""
global:
membership:
maxJoinDuration: 30s
broadcastAddress: "127.0.0.1"
pprof:
port: 7936
metrics:
prometheus:
# # specify framework to use new approach for initializing metrics and/or use opentelemetry
# framework: "opentelemetry"
framework: "tally"
timerType: "histogram"
listenAddress: "127.0.0.1:8000"
authorization:
authorizer: "default"
claimMapper: "default"
jwtKeyProvider:
keySourceURIs:
- "file:///tmp/temporal-jwt-test/.well-known/jwks.json"
services:
frontend:
rpc:
grpcPort: 7233
membershipPort: 6933
bindOnLocalHost: true
httpPort: 7243
internal-frontend:
rpc:
grpcPort: 7236
membershipPort: 6936
bindOnLocalHost: true
matching:
rpc:
grpcPort: 7235
membershipPort: 6935
bindOnLocalHost: true
history:
rpc:
grpcPort: 7234
membershipPort: 6934
bindOnLocalHost: true
worker:
rpc:
grpcPort: 7239
membershipPort: 6939
bindOnLocalHost: true
clusterMetadata:
enableGlobalNamespace: false
failoverVersionIncrement: 10
masterClusterName: "active"
currentClusterName: "active"
clusterInformation:
active:
enabled: true
initialFailoverVersion: 1
rpcName: "frontend"
rpcAddress: "localhost:7233"
httpAddress: "localhost:7243"
dcRedirectionPolicy:
policy: "noop"
archival:
history:
state: "enabled"
enableRead: true
provider:
filestore:
fileMode: "0666"
dirMode: "0766"
gstorage:
credentialsPath: "/tmp/gcloud/keyfile.json"
visibility:
state: "enabled"
enableRead: true
provider:
filestore:
fileMode: "0666"
dirMode: "0766"
namespaceDefaults:
archival:
history:
state: "disabled"
URI: "file:///tmp/temporal_archival/development"
visibility:
state: "disabled"
URI: "file:///tmp/temporal_vis_archival/development"
dynamicConfigClient:
filepath: "config/dynamicconfig/development-sql.yaml"
pollInterval: "10s"
+59
View File
@@ -0,0 +1,59 @@
# Local JWT Development Setup
This document describes how to run Temporal server locally with JWT authentication enabled for test and development purposes.
## Overview
The setup uses:
- RSA key pair for signing/verifying JWTs
- JWKS file loaded directly from disk via `file://` URI
- Custom config file `development-jwt.yaml` with authorization enabled
## Files
- `config/development-jwt.yaml` - Server config with JWT auth enabled
- `config/jwt/setup-keys.sh` - Generates RSA key pair and JWKS
- `config/jwt/generate-token.sh` - Helper script to generate test JWTs
**Generated files** (created by `setup-keys.sh` in `/tmp/temporal-jwt-test/`):
- `/tmp/temporal-jwt-test/private-key.pem` - RSA private key for signing test JWTs
- `/tmp/temporal-jwt-test/.well-known/jwks.json` - JWKS file with RSA public key
## Makefile Targets
- `make start-jwt` - Start Temporal with JWT auth (no `--allow-no-auth` flag)
## Usage
### 1. Start Temporal with JWT Auth
```bash
make start-jwt
```
Note: Unlike other `start-*` targets, this does NOT use the `--allow-no-auth` flag, so authentication is enforced.
### 2. Generate Test JWTs
```bash
# Default: [email protected] with system:admin
./config/jwt/generate-token.sh
# Custom subject with system:admin
./config/jwt/generate-token.sh [email protected]
# Custom subject with namespace permission
./config/jwt/generate-token.sh [email protected] default:admin
# Multiple permissions
./config/jwt/generate-token.sh [email protected] system:admin default:writer
```
### 3. Use the Token
```bash
TOKEN=$(./config/jwt/generate-token.sh [email protected] default:admin)
temporal --tls=false --api-key "$TOKEN" workflow list
```
+58
View File
@@ -0,0 +1,58 @@
#!/bin/bash
# Generate a test JWT signed with the local private key
# Usage: ./generate-token.sh [subject] [permissions...]
#
# Examples:
# ./generate-token.sh # Default: [email protected] with system:admin
# ./generate-token.sh [email protected] # Custom subject with system:admin
# ./generate-token.sh [email protected] default:admin # Custom subject with namespace permission
# ./generate-token.sh [email protected] system:admin default:writer # Multiple permissions
#
# The generated JWT has the following structure:
# Header: {"alg": "RS256", "typ": "JWT", "kid": "test-key-1"}
# Payload: {"sub": "<subject>", "permissions": ["<perm1>", ...], "iat": <now>, "exp": <now+1h>}
set -euo pipefail
base64url() { base64 | tr -d '\n' | tr '+/' '-_' | tr -d '='; }
KEY_DIR="/tmp/temporal-jwt-test"
PRIVATE_KEY="$KEY_DIR/private-key.pem"
if [ ! -f "$PRIVATE_KEY" ]; then
echo "Private key not found at $PRIVATE_KEY, running setup-keys.sh..." >&2
"$(dirname "$0")/setup-keys.sh"
fi
if [ ! -f "$PRIVATE_KEY" ]; then
echo "Error: Private key still not found at $PRIVATE_KEY" >&2
exit 1
fi
SUBJECT="${1:-test[email protected]}"
shift 2>/dev/null || true
# Collect permissions (default to system:admin if none specified)
if [ $# -eq 0 ]; then
PERMISSIONS='["system:admin"]'
else
PERMISSIONS=$(printf '%s\n' "$@" | jq -R . | jq -s .)
fi
NOW=$(date +%s)
EXP=$((NOW + 3600))
HEADER='{"alg":"RS256","typ":"JWT","kid":"test-key-1"}'
HEADER_B64=$(echo -n "$HEADER" | base64url)
PAYLOAD=$(jq -n -c --arg sub "$SUBJECT" --argjson perms "$PERMISSIONS" \
--argjson iat "$NOW" --argjson exp "$EXP" \
'{sub: $sub, permissions: $perms, iat: $iat, exp: $exp}')
PAYLOAD_B64=$(echo -n "$PAYLOAD" | base64url)
SIGNATURE=$(echo -n "${HEADER_B64}.${PAYLOAD_B64}" | \
openssl dgst -sha256 -sign "$PRIVATE_KEY" | \
base64url)
# Output the complete JWT
echo "${HEADER_B64}.${PAYLOAD_B64}.${SIGNATURE}"
+55
View File
@@ -0,0 +1,55 @@
#!/bin/bash
# Generate RSA key pair and JWKS for local JWT testing
# This script is idempotent - it only generates keys if they don't exist
set -euo pipefail
KEY_DIR="/tmp/temporal-jwt-test"
PRIVATE_KEY="$KEY_DIR/private-key.pem"
JWKS_DIR="$KEY_DIR/.well-known"
JWKS_FILE="$JWKS_DIR/jwks.json"
mkdir -p "$JWKS_DIR"
# Generate private key if it doesn't exist
if [ ! -f "$PRIVATE_KEY" ]; then
echo "Generating RSA private key..."
openssl genrsa -out "$PRIVATE_KEY" 2048 2>/dev/null
# Remove JWKS so it gets regenerated with the new key
rm -f "$JWKS_FILE"
fi
# Generate JWKS from private key if it doesn't exist
if [ ! -f "$JWKS_FILE" ]; then
echo "Generating JWKS from private key..."
# Extract modulus in hex
MODULUS_HEX=$(openssl rsa -in "$PRIVATE_KEY" -noout -modulus 2>/dev/null | cut -d= -f2)
# Convert hex to binary, then to base64url
N=$(echo "$MODULUS_HEX" | xxd -r -p | base64 | tr '+/' '-_' | tr -d '=' | tr -d '\n')
# RSA public exponent is typically 65537 = 0x010001
# In base64url: AQAB
E="AQAB"
cat > "$JWKS_FILE" << EOF
{
"keys": [
{
"kty": "RSA",
"kid": "test-key-1",
"use": "sig",
"alg": "RS256",
"n": "$N",
"e": "$E"
}
]
}
EOF
echo "Created $JWKS_FILE"
fi
echo "Keys ready:"
echo " Private key: $PRIVATE_KEY"
echo " JWKS file: $JWKS_FILE"