Spring til indhold
DAkeycloakcimdoauthmcpopen-source

Jeg måtte patche Keycloak for at få CIMD til at virke

DCR virkede, men jeg ville have den rigtige CIMD-løsning. Det krævede to kirurgiske Keycloak-patches og tests mod Claude og ChatGPT.

Scoret for

DCR virkede. Jeg kunne have stoppet der.

Claude og ChatGPT kunne registrere OAuth-klienter automatisk i Keycloak og gennemføre login til min remote MCP-server. Men hver test efterlod endnu en klientregistrering, og den aktuelle MCP-specifikation beskriver CIMD som den foretrukne løsning, når klient og authorization server ikke kender hinanden på forhånd. DCR er fallbacken til backwards compatibility og særlige behov.

Jeg ville ikke bygge den permanente løsning på fallbacken.

Det valg endte med et custom Keycloak-image, to præcise Java-patches, en håndfuld upstream-issues og virkelige authorize-requests fra både ChatGPT og Claude. Det var mere arbejde end at skrive “brug bare DCR”. Det var også sådan, vi fandt ud af, hvad der faktisk skulle fixes.

Dette er den tekniske fortsættelse til Da min MCP-server mødte Claude og ChatGPT. Det første indlæg handler om platformoplevelsen og de tre Agentics MCP-servere. Her går jeg helt ned i Keycloak-laget.

Hvorfor jeg ikke stoppede ved DCR

Dynamic Client Registration gør præcis det, navnet siger. En ukendt klient sender sine metadata til authorization serverens registration_endpoint, og serveren opretter en client record og returnerer et client_id.

Det er en vigtig fallback, og det fik vores første Claude-flow til at virke. Men i et offentligt remote MCP-produkt har Claude, ChatGPT og andre store klienter allerede en stabil offentlig identitet. Med CIMD kan de bruge en HTTPS-URL som client_id, og authorization serveren kan hente og validere metadata fra URL’en.

Den aktuelle MCP authorization-specifikation formulerer prioriteten ret tydeligt:

  • authorization servers og MCP-klienter SHOULD understøtte CIMD;
  • de MAY understøtte DCR;
  • DCR beskrives som fallback til backwards compatibility eller særlige krav.

Min modstand mod DCR var derfor ikke, at protokollen er ugyldig. Det var, at den gav mig den forkerte permanente driftsmodel: flere client records, mere registration-state og en åben registration-flade, selv når klienten allerede kunne identificere sig med et offentligt dokument.

Jeg ville have URL-baseret client identity som standard og beholde DCR som fallback.

Den grå tekst, der startede det

Jeg kendte ikke CIMD, før ChatGPT viste muligheden som utilgængelig i connector-opsætningen.

human prompt
3 lines2026-07-17 14:50
ChatGPT will dynamically register an OAuth client using the Registration URL in the OAuth endpoints section. CIMD is unavailable because the server did not advertise CIMD support. is this something we can do? how is that advertised?

Keycloak havde netop fået eksperimentel understøttelse af OAuth Client ID Metadata Documents. Vi opgraderede til 26.7, aktiverede cimd både ved image-build og runtime og tilføjede en client policy med:

  • condition: client-id-uri;
  • executor: client-id-metadata-document;
  • kun HTTPS som URI scheme;
  • eksplicitte trusted domains.

Det følger den model, Keycloak selv beskriver i guiden Integrating with Model Context Protocol: feature flaget annoncerer capability, mens policy og executor faktisk behandler et URL-formateret client_id.

Build- og runtime-featurelisten skulle matche. Vores AppHost satte derfor:

KC_FEATURES=opentelemetry,cimd

Og Docker-imaget blev bygget med:

RUN /opt/keycloak/bin/kc.sh build \
  --db=${KC_DB} \
  --health-enabled=true \
  --features=opentelemetry,cimd

Da discovery-dokumentet begyndte at returnere client_id_metadata_document_supported: true, skiftede ChatGPT straks fra DCR til CIMD.

Det var her den rigtige test begyndte.

Første fejl: ChatGPT havde et felt, Keycloak ikke kendte

ChatGPT sendte et URL-baseret client_id. Keycloak hentede dokumentet, men authorize-flowet fejlede med Client metadata fetch failed.

Loggen viste en Jackson UnrecognizedPropertyException. ChatGPT-dokumentet indeholdt metadatafeltet token_endpoint_auth_methods_supported, som Keycloaks OIDCClientRepresentation ikke kendte. I stedet for at ignorere feltet afviste Keycloak hele dokumentet.

Det felt er også en vigtig forskel mellem klienterne. ChatGPT satte ikke det entydige singularfelt token_endpoint_auth_method. Dokumentet annoncerede i stedet både none og private_key_jwt i pluralfeltet og publicerede en jwks_uri. Derfor er det misvisende at kalde ChatGPT-klienten entydigt public på samme måde som Claude-klienten.

Sammenligning af de faktiske CIMD-oplysninger fra ChatGPT og Claude.

“Connector-specific” og “shared” beskriver kun formen på deres client_id. ChatGPT genererer en metadata-URL til den konkrete connector, eksempelvis https://chatgpt.com/oauth/{connector-id}/client.json. Claude bruger én delt URL for sin MCP-klient: https://claude.ai/oauth/mcp-oauth-client-metadata. Begge er stabile HTTPS-identiteter, som Keycloak kan hente; forskellen er, om URL’en er per connector eller fælles for Claude.

Token-autentificeringen adskiller sig også:

  • none betyder, at klienten ikke beviser sin identitet med en client secret eller kryptografisk signatur ved token endpointet. Det er en public client, og PKCE beskytter authorization-code-flowet.
  • private_key_jwt betyder, at klienten kan signere en JWT med sin private key. Keycloak kan verificere signaturen via klientens jwks_uri.
  • ChatGPT-dokumentet annoncerer begge som muligheder. Det betyder ikke, at ChatGPT bruger begge i samme token exchange, og log-linjerne viste ikke entydigt, hvilken metode det succesfulde flow valgte.
  • Claude vælger eksplicit none, så den klient er entydigt public.

Det var ikke kun et CIMD-problem. Den ældre Keycloak-issue #14946 beskriver samme grundfejl for DCR: et ukendt JSON-felt får registration til at fejle, selvom RFC 7591 kræver, at authorization serveren ignorerer client metadata, den ikke forstår.

Jeg ville ikke bare omskrive ChatGPT-dokumentet i en proxy. Det ville skjule interop-fejlen og binde løsningen til én klient. Vi skulle bevise den mindste korrekte ændring i Keycloak.

Patchen var én annotation:

@JsonIgnoreProperties(ignoreUnknown = true)
public class OIDCClientRepresentation {
    // upstream class
}

Men jeg ville heller ikke forke hele Keycloak-distributionen for én annotation.

Et Docker-image med kirurgiske class overlays

Vores keycloak/Dockerfile bruger et multi-stage build til at patche præcis den klasse, vi har brug for:

  1. Start fra den eksakte Keycloak-version.
  2. Hent den tilsvarende .java-fil fra Keycloaks tag.
  3. Tilføj den lille source-patch med sed.
  4. Verificér med grep, at patchen faktisk matchede.
  5. Kompilér kun klassen med javac mod Keycloaks egne runtime-JARs.
  6. Læg den kompilerede class tilbage i den relevante JAR med jar uf.
  7. Læg den patchede JAR oven på før det normale optimerede kc.sh build.

Det betyder, at vi stadig bygger på det officielle image. Patchen er synlig, lille og bundet til en bestemt upstream-version. Hvis upstream ændrer kildekoden, fejler vores grep-guards og buildet stopper i stedet for lydløst at producere noget andet.

Flowet fra virkelig reproduktion til minimal patch, live proof og upstream.

Den første patch ændrede ChatGPT-testen fra HTTP 400 til en rigtig login-side. Men CIMD-executoren validerer alle URL-felter i dokumentet, ikke kun client_id og redirect URI. ChatGPT refererede også til sit logo på persistent.oaistatic.com, så vores allowlist skulle indeholde OpenAI's legitime asset-domæner.

Det er ikke en irriterende formalitet. CIMD får authorization serveren til at hente et fremmed dokument. Trusted-domain-regler, størrelsesgrænser, HTTPS og SSRF-beskyttelse er en central del af sikkerhedsmodellen.

Da de var korrekte, viste Keycloak-events hele ChatGPT-flowet:

CLIENT_REGISTER  client=https://chatgpt.com/oauth/…/client.json  err=-
LOGIN            client=https://chatgpt.com/oauth/…/client.json  err=-
CODE_TO_TOKEN    client=https://chatgpt.com/oauth/…/client.json  err=-

Ingen DCR. URL’en var klientens identitet.

Anden fejl: Claudes dokument var en public client med jwt-bearer

Næste morgen testede jeg den personlige MCP-server fra Claude Desktop. Keycloak-loggen viste:

client_id=https://claude.ai/oauth/mcp-oauth-client-metadata

Claude understøttede altså også CIMD. Men authorize-requesten endte i invalid_request.

Claudes dokument beskrev en public client med token_endpoint_auth_method: none og inkluderede samtidig urn:ietf:params:oauth:grant-type:jwt-bearer i grant_types. Keycloaks DescriptionConverter aktiverede grantet og kastede derefter en exception, fordi Keycloak ikke ville aktivere det for en public client:

JWT authorization grant cannot be enabled in a public client

Den hurtige workaround var oplagt: pre-register en særskilt claude-desktop-klient og indtast dens ID manuelt. Det ville springe CIMD over og få min egen test videre.

Det var ikke den løsning, jeg ville bygge.

human prompt
1 lines2026-07-18 08:08
i dont care about shortcome fixes, do the proper thing. We should do the CIMD support.

Vi stoppede og undersøgte først, om Claude-dokumentet faktisk var forkert. jwt-bearer kunne ligne confidential-client authentication, men dokumentet beskrev authorization grant-varianten fra RFC 7523. Public clients kan godt annoncere den. Hvis Keycloak ikke vil tilbyde den grant-type, kan den beholde den deaktiveret og acceptere resten af den gyldige metadata.

Det viste sig at være den kendte Keycloak-fejl #50362, “Claude Desktop authentication rejected by Keycloak”, med draft-fix PR #50411.

Vores anden Dockerfile-stage patchede DescriptionConverter. Den erstattede den fatale exception med en eksplicit deaktivering:

setOidcGrantEnabled(
    client,
    OIDCConfigAttributes.JWT_AUTHORIZATION_GRANT_ENABLED,
    Boolean.FALSE
);

Patchen gav ikke Claude adgang til jwt-bearer. Den gjorde blot, at Keycloak kunne deaktivere det ikke-understøttede grant og fortsætte med resten af den gyldige registrering.

Bevis den samme request, ikke en pæn fixture

Et grønt Docker-build beviser ikke, at en patch virker.

Vi genbrugte Claudes eksakte authorize-URL fra det fejlede flow. Før patchen gav den 400 invalid_request. Efter build, retag og restart af kun Keycloak-resource gav den 200 og sign-in-siden.

Derefter gennemførte vi connector-flowet helt til den personlige MCP-server. OAuth kunne være korrekt, selvom den efterfølgende MCP-route stadig fejlede af en anden årsag; derfor fulgte vi kæden fra metadata-fetch til authorize, login, code exchange og det første autentificerede MCP-kald.

Det er en disciplin, jeg gerne vil beholde:

  • reproducér fejlen med en virkelig klient;
  • isolér det lag, der fejler;
  • lav den mindste patch;
  • replay den samme request;
  • verificér hele flowet bagefter.

Syntetiske metadatafixtures er gode tests, men de finder ikke nødvendigvis felterne og kombinationerne, som virkelige klienter publicerer.

En lokal patch må kunne trækkes tilbage

Mit mål er ikke at vedligeholde en privat Keycloak-fork for evigt.

Begge Dockerfile-stages har derfor en tydelig upstream-reference og en TODO om at blive fjernet, når en tilsvarende rettelse lander. Patchene er koblet til reproducerbare problemer og eksisterende issues, ikke til et diffust “Keycloak virker ikke”.

Den lokale patch har to funktioner:

  1. Den beviser, at den foreslåede rettelse faktisk løser ChatGPT- og Claude-flowet.
  2. Den giver konkret evidens til upstream-dialogen: klientdokument, stack trace, minimal kodeændring og før/efter-request.

Det er sådan, jeg helst bidrager tilbage til open source. Ikke ved at bygge en permanent workaround rundt om projektet, og heller ikke ved kun at skrive en issue. Først gør jeg fejlen reproducerbar. Så viser jeg en lille løsning, der løser fejlen uden at give klienten flere rettigheder. Til sidst kan patchen flyttes upstream, og Dockerfile-overlaget slettes.

Jeg har ikke brug for, at upstream følger min arkitektur. Jeg har brug for, at vores fund er præcise nok til, at maintainers kan vurdere dem.

Hvor det landede

Agentics’ Keycloak-image kan nu acceptere de virkelige CIMD-dokumenter, som ChatGPT og Claude publicerer:

  • CIMD er aktiveret som en eksperimentel Keycloak-feature;
  • client policy begrænser URL-baserede IDs til HTTPS og trusted domains;
  • ChatGPT's ukendte metadata vælter ikke hele parseren;
  • legitime asset-domæner er med i allowlisten;
  • Claudes unsupported public-client grant bliver deaktiveret uden at afvise klienten;
  • DCR kan stadig være fallback, men er ikke den foretrukne vej.

Det første indlæg sluttede med, at en remote MCP-server er mere end nogle tools over HTTP. Efter det her arbejde ville jeg tilføje én ting:

Den er også en integrationstest af internettets OAuth-implementationer.

Specifikationer bliver først virkelige, når ChatGPT's dokument, Claudes dokument og Keycloaks parser mødes i samme request. Det var dér, de to vigtigste fejl viste sig. Og det var dér, den rigtige løsning kunne bevises.