docs(keycloak): make the quick start actually runnable - #190
Open
adityamparikh wants to merge 4 commits into
Open
docs(keycloak): make the quick start actually runnable#190adityamparikh wants to merge 4 commits into
adityamparikh wants to merge 4 commits into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
A comment, so it reads as skippable. It is not.
HttpSecurityConfigurationwires OAuth2 only when an issuer is set, andMcpServerOAuth2Configurerbuilds itsNimbusJwtDecodereagerly duringinit()— so anOAUTH2_ISSUER_URIpointing at a realm that does not exist aborts context refresh: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
HttpSecurityConfigurationsetsvalidateAudienceClaim(true)against the resource indicator fromresourcePath("/mcp"). Keycloak does not populateaudon its own; a token from the documented client carries only:Result:
docs/security/http.mdalready documents that Keycloak needs an Audience protocol mapper —keycloak.mdjust never applied it. This PR adds it to both the scripted and console paths and cross-linkshttp.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:
firstName/lastNameUPDATE_PASSWORDThe 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
kcadmcommands — realm, public client, audience mapper, user, password.tools/listsucceeds by design (/mcpis permitted at the HTTP layer; authorization is per-tool via@PreAuthorize), so only atools/calldistinguishes a secured server from an unsecured one. Both outcomes are shown.curlwork.Account is not fully set upnow documents both causes — with one command that reports profile fields andrequiredActionstogether, 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:
Audience mapper effect confirmed by removing it and re-testing:
auddrops toaccountand the same call returns401 "The aud claim is not valid".The required-action cause was reproduced separately against the same Keycloak 26.0 instance: an account with
firstName/lastNamepopulated andemailVerified: truestill failed the password grant while carryingrequiredActions: ["UPDATE_PASSWORD"], and succeeded once that action was cleared — nothing else changed.firstNameandlastNamewere confirmedrequired: trueviaGET /admin/realms/solr-mcp/users/profilerather than assumed.Docs-only — no source or test changes.
Related
Diagnosing this surfaced a separate bug that made the failure invisible:
logback.xmlshadowslogback-spring.xml, so HTTP mode logs nothing and the startup exception above never reaches the console. Fixed in #189. The troubleshooting section here mentions theLOGGING_CONFIG=classpath:logback-spring.xmlworkaround so this doc stands alone whether or not #189 has merged.🤖 Generated with Claude Code
https://claude.ai/code/session_01Bbs8w62uwcx12ZE8E2xg8P