mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-04 02:04:03 +00:00
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:
committed by
GitHub
parent
e5c7e41d71
commit
3081343d68
@@ -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..."
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
```
|
||||
|
||||
Executable
+58
@@ -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}"
|
||||
Executable
+55
@@ -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"
|
||||
Reference in New Issue
Block a user