CAMEL-24497: camel-undertow-spring-security-starter - validate the JWT issuer and audience - #1910
Conversation
…T issuer and audience jwtDecoderByIssuerUri built the decoder with NimbusJwtDecoder.withJwkSetUri and installed only a claim-set converter, so the default validator applied: signature and timestamps were checked, the iss claim was not, and the configured clientId was used only to build the ClientRegistration - never bound to the token. Every client of a realm is served by the same signing key, so a token minted for any other client of that realm satisfied the signature check and was accepted. The decoder now installs JwtValidators.createDefaultWithIssuer for the realm the client registration already points at, plus an audience validator binding the token to the configured clientId through its aud or azp claim. The audience check can be turned off with camel.security.undertow.keycloak.validate-audience=false for deployments that rely on tokens minted for a different client. Adds the first tests to this starter, which had no test module. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Andrea Cosentino <ancosen@gmail.com>
davsclaus
left a comment
There was a problem hiding this comment.
Reviewed against this repo's rule files (.oss-ai-helper-rules/). Nice fix — closes a real gap: jwtDecoderByIssuerUri() previously checked signature and timestamps only, never the iss claim, and never bound the token to the configured client, so a token minted for a different client of the same realm would pass. Verified locally: checked out the branch and ran mvn verify in the module — build succeeds, all 9 new tests pass, and the checked-in generated files (undertow-spring-security.json, the starter doc page) exactly match what the build regenerates, so no drift there. CI is green and reviewers are already requested.
One question below, otherwise no blocking issues found from a conventions/build/test standpoint. This review does not replace CodeRabbit/Sourcery/SonarCloud or a substantive security review — please still get a human sign-off on the crypto/security-sensitive logic.
This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.
| /** | ||
| * The issuer the provider stamps into the {@code iss} claim of the tokens it mints. | ||
| */ | ||
| public abstract String getIssuerUri() throws URISyntaxException; |
There was a problem hiding this comment.
Making getIssuerUri() abstract on this public class is source-breaking for any external subclass of AbstractProviderConfiguration — it won't compile against the new starter version until it implements this method. This class already has a precedent for avoiding exactly that: getJwtAuthenticationConverter() below (line 60) is deliberately non-abstract with a default throw new IllegalArgumentException("Not implemented"). Was making this one abstract intentional, or would following the existing default-throw pattern be safer for backwards compatibility? (Impact is likely low today since only KeycloakProviderConfiguration exists and UndertowSpringSecurityCustomizer only wires up keycloak, but flagging per this project's "maintain backwards compatibility for public APIs" standard.)
davsclaus
left a comment
There was a problem hiding this comment.
Approving per maintainer instruction after review. Build verified locally (mvn verify, 9/9 tests pass) and generated docs/metadata match. See prior review comment for a non-blocking question about the new abstract getIssuerUri() method.
This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.
UndertowSpringSecurityCustomizer.jwtDecoderByIssuerUri()built the decoder from the JWK set URI andinstalled only a claim-set converter:
Despite the bean name, no
setJwtValidatorcall was made, so the default validator applied: signature andtimestamps were checked, the
issclaim was not, and the configuredclientIdwas used only to build theClientRegistration— never bound to the token audience. Spring Security's ownwithIssuerLocationpathinstalls an issuer validator; this construction path does not.
Why the audience check is the substantive part. Pinning the JWKS URI already ties tokens to the realm, so
issuer validation alone adds little — every client of that realm shares the same signing key. A token minted
for a different service of the same realm therefore validated here. Binding the token to the configured
resource audience is what closes that.
Change
JwtAudienceValidatorrequires the token'saudclaim to contain the configured client id. Theazpclaimidentifies the requesting client and does not substitute for the resource audience. Keycloak may require an
Audience protocol mapper to add the service client to access tokens.
The issuer is derived from the same
url+realmIdthe client registration already resolves, via a sharedrealmUri()helper, so the issuer is by construction the prefix of the JWK set URI (asserted in a test).Behaviour change and opt-out
This is deliberate: a deployment presenting tokens minted for a different audience will now be rejected.
For anyone relying on that:
camel.security.undertow.keycloak.validate-audience = falseDocumented in
intro.adoc, and it surfaces in config metadata. Worth an upgrade-guide entry inapache/camelwhen this lands.
Tests
This starter had no test module; this adds one along with
spring-boot-starter-test(test scope only,outside the generated dependency block).
JwtAudienceValidatorTest— accepted viaaud, accepted when one of several audiences, rejected for anotherclient of the same realm, rejected without an audience, and rejected when
azpmatches butaudtargetsanother service.
KeycloakIssuerUriTest— issuer derivation, path on the configured URL ignored, issuer is the prefix of thegenerated JWK set URI, and audience validation defaults on.
Not addressed here
CAMEL-24497 also notes that the registration of the non-static inner
@EnableWebSecurity SecurityConfigurationis statically unconfirmed, and suggests a test asserting the filter chain actually installs. That is not added
here; it needs a Spring context with a reachable provider and is separate from the unit coverage above.
Codex on behalf of Federico Mariani.