Skip to content

docs(keycloak): make the quick start actually runnable - #190

Open
adityamparikh wants to merge 4 commits into
apache:mainfrom
adityamparikh:docs/keycloak-quickstart-runnable
Open

docs(keycloak): make the quick start actually runnable#190
adityamparikh wants to merge 4 commits into
apache:mainfrom
adityamparikh:docs/keycloak-quickstart-runnable

Conversation

@adityamparikh

@adityamparikh adityamparikh commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Problem

docs/security/keycloak.md's Quick Start cannot be completed as written. Following it verbatim gives you a server that exits 1 printing nothing but the Spring banner — and once past that, every tool call is rejected.

Three independent gaps, all reproduced against Keycloak 26.0:

1. Step 2 had no commands

# 2. Configure Keycloak (see detailed steps below)
# - Create realm: solr-mcp
# - Create client: solr-mcp-client

A comment, so it reads as skippable. It is not. HttpSecurityConfiguration wires OAuth2 only when an issuer is set, and McpServerOAuth2Configurer builds its NimbusJwtDecoder eagerly during init() — so an OAUTH2_ISSUER_URI pointing at a realm that does not exist aborts context refresh:

Error creating bean with name 'securityFilterChain':
  Unable to resolve the Configuration with the provided Issuer of
  "http://localhost:8180/realms/solr-mcp"

Note the asymmetry that makes this sharp: an issuer left empty starts fine (the chain still returns 401/403). Only a configured-but-unresolvable issuer kills startup.

2. No audience mapper — so every tool call 401s

HttpSecurityConfiguration sets validateAudienceClaim(true) against the resource indicator from resourcePath("/mcp"). Keycloak does not populate aud on its own; a token from the documented client carries only:

"aud": "account"

Result:

401 WWW-Authenticate: Bearer error="invalid_token",
    error_description="... The aud claim is not valid"

docs/security/http.md already documents that Keycloak needs an Audience protocol mapper — keycloak.md just never applied it. This PR adds it to both the scripted and console paths and cross-links http.md.

3. Test users could not get a token at all

The documented user creation omits first/last name, which Keycloak's default user profile requires:

{"error":"invalid_grant","error_description":"Account is not fully set up"}

Two distinct causes produce this identical message, and the doc only ever covered the first:

Cause Mechanism
Blank firstName / lastName declarative user-profile validation
Pending required action, usually UPDATE_PASSWORD required-action gate

The second is the easier one to hit, because the admin console defaults Temporary to ON when setting a password — and step 6 of Creating Test Users said "disable Temporary" without ever saying what breaks if you don't. Neither cause is resolvable through the password grant, which has no browser leg to render the "complete your profile" or "update your password" page; both accounts sign in fine through a browser flow, so the failure looks like it only affects curl.

What changed

  • Step 2 is now runnable kcadm commands — realm, public client, audience mapper, user, password.
  • New step 4 proves security is actually on. An anonymous tools/list succeeds by design (/mcp is permitted at the HTTP layer; authorization is per-tool via @PreAuthorize), so only a tools/call distinguishes a secured server from an unsecured one. Both outcomes are shown.
  • Console instructions gain the audience mapper, the first/last name fields, and a note that Direct access grants is what makes the existing password-grant curl work.
  • Troubleshooting gains the four failure modes actually encountered, each with its real symptom text.
  • Account is not fully set up now documents both causes — with one command that reports profile fields and requiredActions together, a table of the four blocking required actions, and console + REST remedies for clearing them. The Temporary trap is called out at the Creating Test Users step and on the REST provisioning example (which correctly keeps "temporary": true — it is the right default for a real user, just not for a scripted one).

Verification

The whole Quick Start was executed verbatim against Keycloak 26.0 from a freshly deleted and recreated realm. Recorded outputs are the real ones:

--- unauthenticated:
{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"Access Denied"}],"isError":true}}
--- authenticated:
{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"[\"books\",\"films\",\"streaming-shows\"]"}],"isError":false}}

Audience mapper effect confirmed by removing it and re-testing: aud drops to account and the same call returns 401 "The aud claim is not valid".

The required-action cause was reproduced separately against the same Keycloak 26.0 instance: an account with firstName/lastName populated and emailVerified: true still failed the password grant while carrying requiredActions: ["UPDATE_PASSWORD"], and succeeded once that action was cleared — nothing else changed. firstName and lastName were confirmed required: true via GET /admin/realms/solr-mcp/users/profile rather than assumed.

Docs-only — no source or test changes.

Related

Diagnosing this surfaced a separate bug that made the failure invisible: logback.xml shadows logback-spring.xml, so HTTP mode logs nothing and the startup exception above never reaches the console. Fixed in #189. The troubleshooting section here mentions the LOGGING_CONFIG=classpath:logback-spring.xml workaround so this doc stands alone whether or not #189 has merged.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bbs8w62uwcx12ZE8E2xg8P

adityamparikh and others added 4 commits August 29, 2026 21:46
The quick start could not be completed as written. Following it verbatim
produced a server that exits 1 printing nothing but the Spring banner, and even
after getting past that, every tool call was rejected. Three separate gaps:

1. Step 2 was a comment -- "# 2. Configure Keycloak (see detailed steps below)"
   -- with no commands, so it reads as optional. It is not: the MCP server
   builds its NimbusJwtDecoder eagerly while the Spring context is created, so
   an OAUTH2_ISSUER_URI pointing at a realm that does not exist aborts startup.
   (An empty issuer starts fine, which makes the set-but-wrong case the sharp
   edge.) Replaced with runnable kcadm commands.

2. No audience mapper. HttpSecurityConfiguration sets validateAudienceClaim(true)
   against the resource indicator from resourcePath("/mcp"), and Keycloak does
   not populate "aud" on its own -- it emits only "account". Every tool call
   therefore failed with 401 'The aud claim is not valid'. docs/security/http.md
   documented this requirement; keycloak.md never applied it. Added the mapper
   to both the scripted and console paths, and cross-linked http.md.

3. Test users had no first/last name. Keycloak's default user profile requires
   them, and the token endpoint rejects such a user with the distinctly
   unhelpful "Account is not fully set up".

Also:

* Add a step 4 that proves security is actually on. An anonymous tools/list
  succeeds by design -- /mcp is permitted at the HTTP layer and authorization is
  enforced per tool via @PreAuthorize -- so only a tools/call distinguishes a
  secured server from an unsecured one. Both outcomes are shown.
* Note that directAccessGrantsEnabled is what makes the existing password-grant
  curl work, and that step 3 also starts Solr via Docker Compose.
* Expand Troubleshooting with the four failure modes actually hit, each with its
  real symptom text: silent exit 1, "Unable to resolve the Configuration with
  the provided Issuer", "The aud claim is not valid", and "Account is not fully
  set up".

Every command in the Quick Start was executed against Keycloak 26.0 from a
freshly created realm, and the recorded outputs are the real ones: the
unauthenticated call returns "Access Denied" with isError true, and the
authenticated call returns the collection list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bbs8w62uwcx12ZE8E2xg8P
Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
…is not fully set up"

The existing entry attributes this error solely to a blank first/last name.
A pending required action on the account produces the identical message
through a different mechanism, and it is the easier one to hit: the admin
console defaults "Temporary" to ON when setting a password, which attaches
an UPDATE_PASSWORD action. Verified against Keycloak 26.0 — clearing that
action on an otherwise complete account is what makes the password grant
succeed.

- Rewrite the Troubleshooting entry to cover both causes, explain why the
  password grant specifically fails (no browser leg to resolve either
  condition), and give one command that checks profile fields and
  requiredActions together.
- Add a table of the four blocking required actions with their causes.
- Give both the console and REST remedies for clearing pending actions.
- Note the Temporary trap at the "Creating Test Users" step and on the REST
  provisioning example, which correctly keeps "temporary": true.
- Add an account-readiness check to Useful Commands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019bEjWDY1fi5PY4RrTa6LYm
Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
Builds on the runnable quick start with the client side of the flow, plus two
corrections found while verifying the guide against the code.

- Add "Configuring a Spring AI MCP Client": a confidential client using the
  client_credentials grant with an audience mapper, the
  spring.ai.mcp.client.streamable-http transport properties, and the
  McpSyncHttpClientRequestCustomizer bean that attaches the bearer token.
  Property names and the customizer signature were read from the resolved
  Spring AI 1.1.7 and MCP SDK 0.18.2 artifacts rather than assumed. Notes the
  two silent traps: `endpoint` already defaults to /mcp, so `url` must be the
  bare origin, and the prefix is streamable-http rather than the deprecated sse.

- Remove the "Resource Server Client" instructions. The MCP server is a pure
  resource server: HttpSecurityConfiguration consumes only
  spring.security.oauth2.resourceserver.jwt.issuer-uri and validates via JWKS,
  so it needs no Keycloak client of its own.

- Fix the JWT decode snippets. JWT payloads are unpadded base64url, so
  `cut -d. -f2 | base64 -d` fails on real Keycloak tokens and, with stderr
  suppressed, silently feeds nothing to jq — a parse error that reads as a
  malformed token when the token is fine.

Also documents the two startup ordering constraints (eager NimbusJwtDecoder
resolution, Keycloak readiness polling), the audience-mapper requirement, and
the silent bootRun failure caused by logback.xml shadowing logback-spring.xml.

Verified against Keycloak 26.0 and a running server: the quick start client and
user creation blocks both succeed, and a gated tools/call returns the tool
result with a valid token and "Access Denied" without one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011VuxVJU4FuPBPkb8ye7oTF
Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
The knobs table listed the default as the Auth0 placeholder
`https://your-auth0-domain.auth0.com/`, and said the server fails to start if
that URL is unreachable — implying a stock HTTP-mode server cannot boot without
an IdP. Neither part is accurate.

application-http.properties sets:

    spring.security.oauth2.resourceserver.jwt.issuer-uri=${OAUTH2_ISSUER_URI:}

so the default is empty, and HttpSecurityConfiguration gates the OAuth2 wiring
on StringUtils.hasText(issuerUrl). An empty value means no bearer-token
validator is installed while the filter chain still gates every non-permitAll
endpoint with 401/403 — which is what lets an unconfigured native-http image
start at all.

The startup failure is real but conditional: once the issuer is non-empty it is
resolved eagerly during context refresh, so an unreachable URL aborts the boot.
The table now states both halves.

Companion to the eager-resolution note in keycloak.md, which links here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011VuxVJU4FuPBPkb8ye7oTF
Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant