Docs/Decisions/ADR-0005 Dual-Issuer JWT

ADR-0005: Issuer validation separated from key fetching, with additional issuers

Status
accepted
Decided
September 2026
Deciders
Platform Engineering
Motivating failure
A host move of the original hosted deployment: every authenticated API call returned 401 while browser logins appeared to work

Context

Keycloak pins its hostname (KC_HOSTNAME), so every token it issues carries the public issuer, e.g. iss = https://auth.<your-domain>/realms/agentic-ai (or http://localhost:8180/realms/agentic-ai locally). Core, however, reaches Keycloak on an internal name — compose DNS http://keycloak:8080 or an in-cluster Service — never through the public edge.

Spring's default withIssuerLocation(issuerUri) auto-configuration conflates two things: where to fetch the JWKS and which issuer to validate tokens against. Point it at the internal host and every real token is rejected; point it at the public hostname and core must hairpin through the public edge, where a CDN or WAF may block non-browser clients.

When the hosted deployment moved hosts, core's issuer was set to an internal address with no additional issuer. Result: every authenticated API call returned 401 while browser logins appeared to work — the browser talks to Keycloak directly and never exercises core's issuer check.

Decision

sequenceDiagram participant B as Browser participant K as Keycloak (auth.your-domain) participant C as core B->>K: OIDC login K-->>B: JWT iss = PUBLIC_AUTH_URL/realms/agentic-ai B->>C: API call + JWT C->>C: JWKS via http://keycloak:8080 (internal) C->>C: accept iss in { KEYCLOAK_ISSUER_URI, KEYCLOAK_ADDITIONAL_ISSUERS }

Alternatives considered

OptionWhy it lost
Single issuer, JWKS from the issuer URLForces core's key fetch through the public edge and makes every token validation depend on it.
Unpin Keycloak's hostname (let iss follow the request URL)Tokens would carry whichever hostname was hit, breaking validation consistency — contrary to Keycloak's own production guidance.

Consequences

Positive

Negative / costs

Standing rule
The issuer core accepts must include exactly the URL Keycloak stamps (PUBLIC_AUTH_URL/realms/agentic-ai). Add any other issuer — such as the old one during a domain change — to KEYCLOAK_ADDITIONAL_ISSUERS. Keep the JWKS URL internal. Symptom to remember: browser logins look fine while every API call fails.

Evidence & references