mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
274 lines
9.4 KiB
Bash
Executable File
274 lines
9.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# =============================================================================
|
|
# setup-keycloak.sh — Configure Keycloak for local OAuth testing
|
|
# =============================================================================
|
|
# Usage: ./scripts/setup-keycloak.sh
|
|
#
|
|
# Creates the `sprout` realm with:
|
|
# - sprout-desktop client (public, direct access grants)
|
|
# - Test users: tyler, alice, bob, charlie (password: password123)
|
|
# - nostr_pubkey custom attribute on each user
|
|
# - Protocol mapper: nostr_pubkey → JWT access token claim
|
|
#
|
|
# Keycloak is a LOCAL DEV STAND-IN for Okta/generic OIDC providers.
|
|
# It is NOT a production dependency.
|
|
#
|
|
# Prerequisites:
|
|
# - Keycloak running at http://localhost:8180 (docker compose up -d)
|
|
# - curl and jq installed
|
|
# =============================================================================
|
|
set -euo pipefail
|
|
|
|
KEYCLOAK_URL="${KEYCLOAK_URL:-http://localhost:8180}"
|
|
ADMIN_USER="${KEYCLOAK_ADMIN:-admin}"
|
|
ADMIN_PASS="${KEYCLOAK_ADMIN_PASSWORD:-admin}"
|
|
REALM="sprout"
|
|
CLIENT_ID="sprout-desktop"
|
|
TIMEOUT=120 # seconds to wait for Keycloak
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m'
|
|
|
|
log() { echo -e "${BLUE}[keycloak-setup]${NC} $*"; }
|
|
success(){ echo -e "${GREEN}[keycloak-setup]${NC} ✅ $*"; }
|
|
warn() { echo -e "${YELLOW}[keycloak-setup]${NC} ⚠️ $*"; }
|
|
error() { echo -e "${RED}[keycloak-setup]${NC} ❌ $*" >&2; }
|
|
|
|
# ---- Preflight --------------------------------------------------------------
|
|
|
|
for cmd in curl jq; do
|
|
if ! command -v "$cmd" &>/dev/null; then
|
|
error "Required tool not found: $cmd"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# ---- Wait for Keycloak ------------------------------------------------------
|
|
|
|
log "Waiting for Keycloak at ${KEYCLOAK_URL}..."
|
|
elapsed=0
|
|
interval=5
|
|
until curl -sf "${KEYCLOAK_URL}/health/ready" -o /dev/null 2>/dev/null; do
|
|
if [[ ${elapsed} -ge ${TIMEOUT} ]]; then
|
|
error "Timed out waiting for Keycloak (${TIMEOUT}s). Is it running?"
|
|
error " docker compose up -d keycloak"
|
|
exit 1
|
|
fi
|
|
echo -n "."
|
|
sleep "${interval}"
|
|
elapsed=$((elapsed + interval))
|
|
done
|
|
echo ""
|
|
success "Keycloak is ready"
|
|
|
|
# ---- Get admin token --------------------------------------------------------
|
|
|
|
log "Authenticating as admin..."
|
|
ADMIN_TOKEN=$(curl -sf \
|
|
-X POST "${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token" \
|
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
-d "client_id=admin-cli" \
|
|
-d "username=${ADMIN_USER}" \
|
|
-d "password=${ADMIN_PASS}" \
|
|
-d "grant_type=password" \
|
|
| jq -r '.access_token')
|
|
|
|
if [[ -z "${ADMIN_TOKEN}" || "${ADMIN_TOKEN}" == "null" ]]; then
|
|
error "Failed to get admin token. Check KEYCLOAK_ADMIN / KEYCLOAK_ADMIN_PASSWORD."
|
|
exit 1
|
|
fi
|
|
success "Admin token obtained"
|
|
|
|
# Helper: authenticated API call
|
|
kc() {
|
|
local method="$1"; shift
|
|
local path="$1"; shift
|
|
curl -sf \
|
|
-X "${method}" \
|
|
"${KEYCLOAK_URL}/admin/realms${path}" \
|
|
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
"$@"
|
|
}
|
|
|
|
kc_root() {
|
|
local method="$1"; shift
|
|
local path="$1"; shift
|
|
curl -sf \
|
|
-X "${method}" \
|
|
"${KEYCLOAK_URL}/admin${path}" \
|
|
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
"$@"
|
|
}
|
|
|
|
# ---- Create realm -----------------------------------------------------------
|
|
|
|
log "Checking for realm '${REALM}'..."
|
|
REALM_EXISTS=$(curl -sf \
|
|
"${KEYCLOAK_URL}/admin/realms/${REALM}" \
|
|
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
|
-o /dev/null -w "%{http_code}" 2>/dev/null || true)
|
|
|
|
if [[ "${REALM_EXISTS}" == "200" ]]; then
|
|
warn "Realm '${REALM}' already exists — skipping creation"
|
|
else
|
|
log "Creating realm '${REALM}'..."
|
|
curl -sf \
|
|
-X POST "${KEYCLOAK_URL}/admin/realms" \
|
|
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"realm": "'"${REALM}"'",
|
|
"displayName": "Sprout",
|
|
"enabled": true,
|
|
"registrationAllowed": false,
|
|
"loginWithEmailAllowed": true,
|
|
"duplicateEmailsAllowed": false,
|
|
"resetPasswordAllowed": false,
|
|
"editUsernameAllowed": false,
|
|
"bruteForceProtected": false
|
|
}'
|
|
success "Realm '${REALM}' created"
|
|
fi
|
|
|
|
# ---- Create client ----------------------------------------------------------
|
|
|
|
log "Checking for client '${CLIENT_ID}'..."
|
|
EXISTING_CLIENT=$(kc GET "/${REALM}/clients?clientId=${CLIENT_ID}" | jq -r '.[0].id // empty')
|
|
|
|
if [[ -n "${EXISTING_CLIENT}" ]]; then
|
|
warn "Client '${CLIENT_ID}' already exists (id: ${EXISTING_CLIENT}) — skipping creation"
|
|
CLIENT_UUID="${EXISTING_CLIENT}"
|
|
else
|
|
log "Creating client '${CLIENT_ID}'..."
|
|
kc POST "/${REALM}/clients" -d '{
|
|
"clientId": "'"${CLIENT_ID}"'",
|
|
"name": "Sprout Desktop",
|
|
"enabled": true,
|
|
"publicClient": true,
|
|
"directAccessGrantsEnabled": true,
|
|
"standardFlowEnabled": true,
|
|
"implicitFlowEnabled": false,
|
|
"serviceAccountsEnabled": false,
|
|
"redirectUris": [
|
|
"http://localhost:*",
|
|
"sprout://*"
|
|
],
|
|
"webOrigins": ["*"],
|
|
"protocol": "openid-connect"
|
|
}'
|
|
|
|
CLIENT_UUID=$(kc GET "/${REALM}/clients?clientId=${CLIENT_ID}" | jq -r '.[0].id')
|
|
success "Client '${CLIENT_ID}' created (id: ${CLIENT_UUID})"
|
|
fi
|
|
|
|
# ---- Add nostr_pubkey protocol mapper ---------------------------------------
|
|
|
|
log "Checking for nostr_pubkey protocol mapper..."
|
|
MAPPER_EXISTS=$(kc GET "/${REALM}/clients/${CLIENT_UUID}/protocol-mappers/models" \
|
|
| jq -r '.[] | select(.name == "nostr_pubkey") | .id // empty')
|
|
|
|
if [[ -n "${MAPPER_EXISTS}" ]]; then
|
|
warn "Protocol mapper 'nostr_pubkey' already exists — skipping"
|
|
else
|
|
log "Creating nostr_pubkey → JWT claim mapper..."
|
|
kc POST "/${REALM}/clients/${CLIENT_UUID}/protocol-mappers/models" -d '{
|
|
"name": "nostr_pubkey",
|
|
"protocol": "openid-connect",
|
|
"protocolMapper": "oidc-usermodel-attribute-mapper",
|
|
"consentRequired": false,
|
|
"config": {
|
|
"userinfo.token.claim": "true",
|
|
"user.attribute": "nostr_pubkey",
|
|
"id.token.claim": "true",
|
|
"access.token.claim": "true",
|
|
"claim.name": "nostr_pubkey",
|
|
"jsonType.label": "String"
|
|
}
|
|
}'
|
|
success "Protocol mapper 'nostr_pubkey' created"
|
|
fi
|
|
|
|
# ---- Create users -----------------------------------------------------------
|
|
|
|
# Format: "username:nostr_pubkey"
|
|
declare -a USERS=(
|
|
"tyler:e5ebc6cdb579be112e336cc319b5989b4bb6af11786ea90dbe52b5f08d741b34"
|
|
"alice:953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f"
|
|
"bob:bb22a5299220cad76ffd46190ccbeede8ab5dc260faa28b6e5a2cb31b9aff260"
|
|
"charlie:554cef57437abac34522ac2c9f0490d685b72c80478cf9f7ed6f9570ee8624ea"
|
|
)
|
|
|
|
for entry in "${USERS[@]}"; do
|
|
username="${entry%%:*}"
|
|
pubkey="${entry##*:}"
|
|
|
|
log "Checking for user '${username}'..."
|
|
EXISTING_USER=$(kc GET "/${REALM}/users?username=${username}&exact=true" | jq -r '.[0].id // empty')
|
|
|
|
if [[ -n "${EXISTING_USER}" ]]; then
|
|
warn "User '${username}' already exists (id: ${EXISTING_USER}) — updating nostr_pubkey attribute"
|
|
kc PUT "/${REALM}/users/${EXISTING_USER}" -d '{
|
|
"attributes": {
|
|
"nostr_pubkey": ["'"${pubkey}"'"]
|
|
}
|
|
}'
|
|
success "User '${username}' updated"
|
|
else
|
|
log "Creating user '${username}'..."
|
|
kc POST "/${REALM}/users" -d '{
|
|
"username": "'"${username}"'",
|
|
"email": "'"${username}"'@sprout.local",
|
|
"firstName": "'"${username^}"'",
|
|
"lastName": "Test",
|
|
"enabled": true,
|
|
"emailVerified": true,
|
|
"credentials": [{
|
|
"type": "password",
|
|
"value": "password123",
|
|
"temporary": false
|
|
}],
|
|
"attributes": {
|
|
"nostr_pubkey": ["'"${pubkey}"'"]
|
|
}
|
|
}'
|
|
success "User '${username}' created (nostr_pubkey: ${pubkey:0:16}...)"
|
|
fi
|
|
done
|
|
|
|
# ---- Summary ----------------------------------------------------------------
|
|
|
|
echo ""
|
|
echo -e "${GREEN}═══════════════════════════════════════════════════════${NC}"
|
|
echo -e "${GREEN} Keycloak realm setup complete! 🔑${NC}"
|
|
echo -e "${GREEN}═══════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
echo -e " ${BLUE}Admin UI${NC} http://localhost:8180 (admin / admin)"
|
|
echo -e " ${BLUE}Realm${NC} ${REALM}"
|
|
echo -e " ${BLUE}Client${NC} ${CLIENT_ID} (public, direct access grants)"
|
|
echo ""
|
|
echo -e " ${BLUE}Test users${NC} (password: password123)"
|
|
echo -e " tyler e5ebc6cdb579be112e336cc319b5989b4bb6af11786ea90dbe52b5f08d741b34"
|
|
echo -e " alice 953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f"
|
|
echo -e " bob bb22a5299220cad76ffd46190ccbeede8ab5dc260faa28b6e5a2cb31b9aff260"
|
|
echo -e " charlie 554cef57437abac34522ac2c9f0490d685b72c80478cf9f7ed6f9570ee8624ea"
|
|
echo ""
|
|
echo -e " ${YELLOW}Relay env vars for Keycloak:${NC}"
|
|
echo -e " OKTA_JWKS_URI=http://localhost:8180/realms/sprout/protocol/openid-connect/certs"
|
|
echo -e " OKTA_ISSUER=http://localhost:8180/realms/sprout"
|
|
echo -e " OKTA_AUDIENCE=sprout-desktop"
|
|
echo -e " OKTA_PUBKEY_CLAIM=nostr_pubkey"
|
|
echo ""
|
|
echo -e " ${YELLOW}Get a token (direct grant):${NC}"
|
|
echo -e " curl -s -X POST http://localhost:8180/realms/sprout/protocol/openid-connect/token \\"
|
|
echo -e " -d 'client_id=sprout-desktop&grant_type=password&username=tyler&password=password123' \\"
|
|
echo -e " | jq -r .access_token"
|
|
echo ""
|
|
|
|
exit 0
|