Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 254 additions & 0 deletions test/generate-realm.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
#!/bin/bash
# generate-realm.sh - Generates test/import realm JSON files programmatically.
#
# This script is the source of truth for the Keycloak test realm configuration.
# It starts a temporary Keycloak instance, configures the realms using kcadm.sh,
# and exports the complete realms (including users with credentials) to JSON.
#
# Realms created:
# - pgrealm: The primary test realm.
# - wrongrealm: An identical realm used to test wrong-issuer scenarios.
#
# To update the realm JSON files, modify this script and re-run it.
#
# Requirements: podman or docker, curl, jq
#
# Usage: ./generate-realm.sh

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
IMPORT_DIR="$SCRIPT_DIR/import"

CONTAINER_NAME="kc-realm-gen-$$"
EXPORT_CONTAINER="kc-realm-export-$$"
VOLUME_NAME="kc-realm-gen-data-$$"
KC_PORT=18080
KC_IMAGE="quay.io/keycloak/keycloak:latest"

# --- Detect dependencies ---

if command -v podman &>/dev/null; then
RT=podman
elif command -v docker &>/dev/null; then
RT=docker
else
echo "Error: Neither podman nor docker found" >&2
exit 1
fi

for cmd in curl jq; do
if ! command -v "$cmd" &>/dev/null; then
echo "Error: $cmd is required but not found" >&2
exit 1
fi
done

echo "Using container runtime: $RT"

# --- Cleanup ---

cleanup() {
echo "Cleaning up..."
$RT stop "$CONTAINER_NAME" 2>/dev/null || true
$RT rm -f "$CONTAINER_NAME" 2>/dev/null || true
$RT rm -f "$EXPORT_CONTAINER" 2>/dev/null || true
$RT volume rm "$VOLUME_NAME" 2>/dev/null || true
}
trap cleanup EXIT

# --- Helpers ---

kcadm() {
$RT exec -i "$CONTAINER_NAME" /opt/keycloak/bin/kcadm.sh "$@"
}

# Configure a realm with the standard test resources:
# - client scope 'pgscope', 'pgscope2'
# - client 'pgtest' and 'pgtest2' (public, device flow enabled)
# - user 'testuser' (testuser@example.com / asdfasdf)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually true? It sets up two users, two scopes, two clients?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ops, that's a stale comment

# - user 'testuser2' (testuser2@example.com / asdfasdf)
# - role 'pgrole' (assigned to testuser2, required for pgtest2/pgscope2)
setup_realm() {
local realm=$1

echo "==> Creating realm '$realm'..."
kcadm create realms -s "realm=$realm" -s enabled=true

# kcadm doesn't handle empty-body PUTs well, so we use curl for scope assignments.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it a POST?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They refer to it as PUT everywhere in their docs: https://www.keycloak.org/docs-api/latest/rest-api/index.html

local token
token=$(curl -sf -X POST "http://localhost:$KC_PORT/realms/master/protocol/openid-connect/token" \
-d "client_id=admin-cli" \
-d "username=admin" \
-d "password=admin" \
-d "grant_type=password" | jq -r '.access_token')

echo " Creating client scopes 'pgscope' and 'pgscope2'..."
local scope_name scope_id
for scope_name in pgscope pgscope2; do
kcadm create client-scopes -r "$realm" -f - <<EOF
{
"name": "$scope_name",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"consent.screen.text": "PgTest Scope"
}
}
EOF

scope_id=$(kcadm get client-scopes -r "$realm" --fields id,name \
| jq -r --arg n "$scope_name" '.[] | select(.name==$n) | .id')
echo " $scope_name ID: $scope_id"

curl -sf -X PUT \
"http://localhost:$KC_PORT/admin/realms/$realm/default-optional-client-scopes/$scope_id" \
-H "Authorization: Bearer $token" \
-H "Content-Type: application/json"

# Store IDs for client scope assignment later
eval "local ${scope_name}_id=$scope_id"
done
echo " Added as default optional client scopes."

echo " Creating clients..."
local client_name client_id
for client_name in pgtest pgtest2; do
kcadm create clients -r "$realm" -f - <<EOF
{
"clientId": "$client_name",
"name": "$client_name",
"publicClient": true,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"alwaysDisplayInConsole": true,
"frontchannelLogout": true,
"protocol": "openid-connect",
"redirectUris": ["/*"],
"webOrigins": ["/*"],
"attributes": {
"oauth2.device.authorization.grant.enabled": "true",
"backchannel.logout.session.required": "true",
"post.logout.redirect.uris": "+"
}
}
EOF

client_id=$(kcadm get clients -r "$realm" --fields id,clientId \
| jq -r --arg c "$client_name" '.[] | select(.clientId==$c) | .id')
echo " $client_name ID: $client_id"

for scope_name in pgscope pgscope2; do
eval "scope_id=\$${scope_name}_id"
curl -sf -X PUT \
"http://localhost:$KC_PORT/admin/realms/$realm/clients/$client_id/optional-client-scopes/$scope_id" \
-H "Authorization: Bearer $token" \
-H "Content-Type: application/json"
done
done
echo " Assigned pgscope and pgscope2 as optional client scopes."

echo " Creating users..."
local user
for user in testuser testuser2; do
kcadm create users -r "$realm" \
-s "username=$user" \
-s firstName=Pg \
-s lastName=User \
-s "email=$user@example.com" \
-s emailVerified=true \
-s enabled=true

kcadm set-password -r "$realm" \
--username "$user" \
--new-password asdfasdf

echo " Created $user ($user@example.com)."
done

echo " Creating role 'pgrole'..."
kcadm create roles -r "$realm" -s name=pgrole

local role_id
role_id=$(kcadm get roles -r "$realm" --fields id,name \
| jq -r '.[] | select(.name=="pgrole") | .id')

kcadm add-roles -r "$realm" --uusername testuser2 --rolename pgrole
echo " Assigned 'pgrole' to testuser2."

# Add pgrole as scope mapping for pgscope2
kcadm create "client-scopes/$pgscope2_id/scope-mappings/realm" -r "$realm" -f - <<EOF
[{"id":"$role_id","name":"pgrole"}]
EOF

# Add pgrole as scope mapping for pgtest2
local pgtest2_cid
pgtest2_cid=$(kcadm get clients -r "$realm" --fields id,clientId \
| jq -r '.[] | select(.clientId=="pgtest2") | .id')
kcadm create "clients/$pgtest2_cid/scope-mappings/realm" -r "$realm" -f - <<EOF
[{"id":"$role_id","name":"pgrole"}]
EOF
echo " Added 'pgrole' as scope mapping for pgtest2 and pgscope2."

echo " Realm '$realm' configured."
}

# --- Step 1: Start Keycloak ---

echo "==> Starting Keycloak..."
$RT volume create "$VOLUME_NAME" >/dev/null
$RT run -d --name "$CONTAINER_NAME" \
-p "127.0.0.1:$KC_PORT:8080" \
-v "$VOLUME_NAME:/opt/keycloak/data" \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
"$KC_IMAGE" start-dev >/dev/null

echo "==> Waiting for Keycloak to start..."
for i in $(seq 1 90); do
if curl -sf "http://localhost:$KC_PORT/realms/master" >/dev/null 2>&1; then
echo " Keycloak is ready."
break
fi
if [ "$i" -eq 90 ]; then
echo "Error: Keycloak did not start within 90 seconds" >&2
$RT logs "$CONTAINER_NAME" 2>&1 | tail -20
exit 1
fi
sleep 1
done

# --- Step 2: Authenticate ---

kcadm config credentials \
--server http://localhost:8080 \
--realm master \
--user admin \
--password admin

# --- Step 3: Create realms ---

setup_realm pgrealm
setup_realm wrongrealm

# --- Step 4: Export realms ---

echo "==> Stopping Keycloak for export..."
$RT stop "$CONTAINER_NAME" >/dev/null

echo "==> Exporting realms..."
# Run kc.sh export in a new container with the same data volume.
# Using --dir so each realm gets its own file, with users included.
$RT run --name "$EXPORT_CONTAINER" \
-v "$VOLUME_NAME:/opt/keycloak/data" \
"$KC_IMAGE" \
export --dir /tmp/export --users realm_file

# Copy the exported realm files from the (stopped) container
$RT cp "$EXPORT_CONTAINER:/tmp/export/pgrealm-realm.json" "$IMPORT_DIR/pgrealm.json"
$RT cp "$EXPORT_CONTAINER:/tmp/export/wrongrealm-realm.json" "$IMPORT_DIR/wrongrealm.json"

echo "==> Realms exported to $IMPORT_DIR/"
echo " Done!"
64 changes: 64 additions & 0 deletions test/get-token.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/bin/bash
# get-token.sh - Get an access token from Keycloak via the password grant.
#
# Usage:
# ./get-token.sh [options]
#
# Options:
# -r REALM Realm name (default: pgrealm)
# -u USER Username (default: testuser)
# -p PASSWORD Password (default: asdfasdf)
# -c CLIENT Client ID (default: pgtest)
# -s SCOPES Space-separated scopes (default: "email pgscope")
# -h HOST Keycloak base URL (default: https://localhost:8443)
# -f FIELD Output a specific field instead of the full JSON response
# (e.g. "access_token", "refresh_token", "expires_in")
#
# Examples:
# ./get-token.sh # full JSON response
# ./get-token.sh -f access_token # just the access token
# ./get-token.sh -u testuser2 -s "email pgscope pgscope2"
# ./get-token.sh -r wrongrealm -f access_token

set -euo pipefail

REALM="pgrealm"
USER="testuser"
PASSWORD="asdfasdf"
CLIENT="pgtest"
SCOPES="email pgscope"
HOST="https://localhost:8443"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not a that big fan of all these defaults, especially since we have multiple of each. Why do we just pick one? Do these default actually add enough value?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value is that you can specify only one parameter that you change, and get a different token. For example if you want to test the situation "logging in with a token from the wrong realm", you only have to specify the realm.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is cleaner if e.g. CLIENT always need to be specified.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you decide which fields we have to always specify?

FIELD=""

while getopts "r:u:p:c:s:h:f:" opt; do
case $opt in
r) REALM="$OPTARG" ;;
u) USER="$OPTARG" ;;
p) PASSWORD="$OPTARG" ;;
c) CLIENT="$OPTARG" ;;
s) SCOPES="$OPTARG" ;;
h) HOST="$OPTARG" ;;
f) FIELD="$OPTARG" ;;
*) echo "Usage: $0 [-r realm] [-u user] [-p password] [-c client] [-s scopes] [-h host] [-f field]" >&2; exit 1 ;;
esac
done

TOKEN_URL="$HOST/realms/$REALM/protocol/openid-connect/token"

RESPONSE=$(curl -sk -X POST "$TOKEN_URL" \
-d "grant_type=password" \
-d "client_id=$CLIENT" \
-d "username=$USER" \
-d "password=$PASSWORD" \
-d "scope=$SCOPES")

if echo "$RESPONSE" | jq -e '.error' >/dev/null 2>&1; then
echo "Error: $(echo "$RESPONSE" | jq -r '.error_description // .error')" >&2
exit 1
fi

if [ -n "$FIELD" ]; then
echo "$RESPONSE" | jq -r --arg f "$FIELD" '.[$f]'
else
echo "$RESPONSE" | jq .
fi
Loading