Skip to content

Instantly share code, notes, and snippets.

@thomasdarimont
Last active June 24, 2026 10:42
Show Gist options
  • Select an option

  • Save thomasdarimont/05ff048dbc6f066f8e1aa7c0aba74a59 to your computer and use it in GitHub Desktop.

Select an option

Save thomasdarimont/05ff048dbc6f066f8e1aa7c0aba74a59 to your computer and use it in GitHub Desktop.
Example script for creating an idjag token with Keycloak
#!/usr/bin/env zsh
# Manual end-to-end test for the Identity Assertion JWT (ID-JAG) token-exchange flow.
#
# Prereq: start Keycloak with the experimental feature enabled, e.g.
# bin/kc.sh start-dev --features=identity-assertion-jwt,token-exchange
#
# Requires: curl, jq, openssl (all standard on macOS).
set -u
BASE=${BASE:-http://localhost:8081}
REALM=${REALM:-master}
ADMIN_USER=${ADMIN_USER:-admin}
ADMIN_PASS=${ADMIN_PASS:-admin}
CLIENT_ID=mcp-client
CLIENT_SECRET=secret
# --- helpers ---------------------------------------------------------------
# Portable base64url decode (macOS/BSD openssl, no GNU-only flags).
b64url_decode() {
local data="${1//-/+}"; data="${data//_//}"
local pad=$(( ${#data} % 4 ))
(( pad )) && data="${data}$(printf '=%.0s' {1..$((4 - pad))})"
printf '%s' "$data" | openssl base64 -d -A
}
# Decode a JWT segment: jwt_decode <jwt> <1=header|2=payload>
jwt_decode() {
b64url_decode "$(printf '%s' "$1" | cut -d. -f"$2")" | jq .
}
die() { print -u2 "ERROR: $*"; exit 1; }
# --- admin token (cached) --------------------------------------------------
print "== getting admin token =="
ADMIN_TOKEN=$(curl -s "$BASE/realms/master/protocol/openid-connect/token" \
-d grant_type=password -d client_id=admin-cli \
-d username="$ADMIN_USER" -d password="$ADMIN_PASS" | jq -r '.access_token // empty')
[[ -n "$ADMIN_TOKEN" ]] || die "could not get admin token (check $ADMIN_USER/$ADMIN_PASS creds)"
print "admin token OK"
AUTH=(-H "Authorization: Bearer $ADMIN_TOKEN")
# --- (re)create the mcp-client ---------------------------------------------
print "\n== (re)creating client '$CLIENT_ID' =="
CID=$(curl -s "${AUTH[@]}" \
"$BASE/admin/realms/$REALM/clients?clientId=$CLIENT_ID" | jq -r '.[0].id // empty')
if [[ -n "$CID" ]]; then
curl -s -X DELETE "${AUTH[@]}" "$BASE/admin/realms/$REALM/clients/$CID" >/dev/null
print "deleted existing client ($CID)"
fi
HTTP=$(curl -s -o /dev/null -w '%{http_code}' \
-X POST "$BASE/admin/realms/$REALM/clients" \
"${AUTH[@]}" -H "Content-Type: application/json" -d @- <<JSON
{
"clientId": "$CLIENT_ID",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"secret": "$CLIENT_SECRET",
"directAccessGrantsEnabled": true,
"standardFlowEnabled": false,
"attributes": {
"id.jag.issuance.enabled": "true",
"id.jag.allowed.audiences": "https://auth.chat.example",
"id.jag.client.id": "mcp-client-at-resource-as",
"id.jag.allowed.scopes": "chat.read##chat.history"
}
}
JSON
)
[[ "$HTTP" == "201" ]] || die "create client failed (HTTP $HTTP)"
print "create client: 201"
# Resolve the actual secret (in case the server overrode it).
CID=$(curl -s "${AUTH[@]}" \
"$BASE/admin/realms/$REALM/clients?clientId=$CLIENT_ID" | jq -r '.[0].id')
SECRET=$(curl -s "${AUTH[@]}" \
"$BASE/admin/realms/$REALM/clients/$CID/client-secret" | jq -r '.value')
print "client secret: $SECRET"
# --- get an ID token for the user ------------------------------------------
print "\n== getting ID token =="
ID_TOKEN=$(curl -s "$BASE/realms/$REALM/protocol/openid-connect/token" \
-d grant_type=password -d username="$ADMIN_USER" -d password="$ADMIN_PASS" \
-d client_id="$CLIENT_ID" -d client_secret="$SECRET" \
-d scope="openid email" | jq -r '.id_token // empty')
[[ -n "$ID_TOKEN" ]] || die "login failed (no id_token; is 'openid' scope mapped?)"
print "ID token OK"
# --- happy path: exchange ID token -> ID-JAG -------------------------------
print "\n== exchanging ID token for an ID-JAG =="
RESP=$(curl -s -u "$CLIENT_ID:$SECRET" "$BASE/realms/$REALM/protocol/openid-connect/token" \
-d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
-d requested_token_type=urn:ietf:params:oauth:token-type:id-jag \
-d subject_token="$ID_TOKEN" \
-d subject_token_type=urn:ietf:params:oauth:token-type:id_token \
-d audience=https://auth.chat.example \
-d resource=https://mcp.chat.example/mcp \
-d scope="chat.read chat.history")
print "$RESP" | jq .
JAG=$(print "$RESP" | jq -r '.access_token // empty')
[[ -n "$JAG" ]] || die "no ID-JAG returned (is the 'identity-assertion-jwt' feature enabled?)"
print "\n-- ID-JAG header --"
jwt_decode "$JAG" 1
print -- "-- ID-JAG payload --"
jwt_decode "$JAG" 2
print "\n== advertised grant profiles =="
curl -s "$BASE/realms/$REALM/.well-known/openid-configuration" \
| jq '.authorization_grant_profiles_supported'
# --- negative: audience not allow-listed -> 403 ----------------------------
print "\n== negative: non-allow-listed audience (expect 403) =="
curl -s -o /dev/null -w 'not-allowed audience: %{http_code}\n' \
-u "$CLIENT_ID:$SECRET" "$BASE/realms/$REALM/protocol/openid-connect/token" \
-d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
-d requested_token_type=urn:ietf:params:oauth:token-type:id-jag \
-d subject_token="$ID_TOKEN" \
-d subject_token_type=urn:ietf:params:oauth:token-type:id_token \
-d audience=evil.example
# --- negative: wrong subject_token_type -> 400 invalid_request -------------
print "\n== negative: wrong subject_token_type (expect 400 invalid_request) =="
curl -s -u "$CLIENT_ID:$SECRET" "$BASE/realms/$REALM/protocol/openid-connect/token" \
-d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
-d requested_token_type=urn:ietf:params:oauth:token-type:id-jag \
-d subject_token="$ID_TOKEN" \
-d subject_token_type=urn:ietf:params:oauth:token-type:access_token \
-d audience=auth.chat.example | jq '{error, error_description}'
# --- negative: scope outside the allow-list -> 400 invalid_scope -----------
print "\n== negative: non-allow-listed scope (expect 400 invalid_scope) =="
curl -s -u "$CLIENT_ID:$SECRET" "$BASE/realms/$REALM/protocol/openid-connect/token" \
-d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
-d requested_token_type=urn:ietf:params:oauth:token-type:id-jag \
-d subject_token="$ID_TOKEN" \
-d subject_token_type=urn:ietf:params:oauth:token-type:id_token \
-d audience=auth.chat.example \
-d scope="chat.read chat.delete" | jq '{error, error_description}'
print "\nDone."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment