Files
buzz/scripts/setup-keycloak.sh

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