You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by fm...@apache.org on 2026/09/09 14:56:00 UTC

(camel-spring-boot) branch camel-spring-boot-4.22.x updated: CAMEL-24497: camel-undertow-spring-security-starter - validate the JWT issuer and audience (#1910)

This is an automated email from the ASF dual-hosted git repository.

Croway pushed a commit to branch camel-spring-boot-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel-spring-boot.git


The following commit(s) were added to refs/heads/camel-spring-boot-4.22.x by this push:
     new 948f059d1a7 CAMEL-24497: camel-undertow-spring-security-starter - validate the JWT issuer and audience (#1910)
948f059d1a7 is described below

commit 948f059d1a78be09c980ad9d1fe4143b949ad771
Author: Andrea Cosentino <an...@gmail.com>
AuthorDate: Fri Aug 28 14:05:51 2026 +0200

    CAMEL-24497: camel-undertow-spring-security-starter - validate the JWT issuer and audience (#1910)
    
    * CAMEL-24497: camel-undertow-spring-security-starter - validate the JWT 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) <no...@anthropic.com>
    Signed-off-by: Andrea Cosentino <an...@gmail.com>
    
    * CAMEL-24497: Require the configured JWT audience
    
    ---------
    
    Signed-off-by: Andrea Cosentino <an...@gmail.com>
    Co-authored-by: Claude Opus 5 (1M context) <no...@anthropic.com>
    Co-authored-by: Croway <fe...@gmail.com>
    (cherry picked from commit 90448850a2b73c6d75d089bf0805bbf2e6836e61)
---
 .../camel-undertow-spring-security-starter/pom.xml |  7 ++
 .../src/main/doc/intro.adoc                        | 18 +++++
 .../src/main/docs/undertow-spring-security.json    |  7 ++
 .../undertow/spring/boot/JwtAudienceValidator.java | 53 ++++++++++++++
 .../boot/UndertowSpringSecurityCustomizer.java     | 22 ++++++
 .../providers/AbstractProviderConfiguration.java   | 19 +++++
 .../providers/KeycloakProviderConfiguration.java   | 11 ++-
 .../spring/boot/JwtAudienceValidatorTest.java      | 85 ++++++++++++++++++++++
 .../spring/boot/KeycloakIssuerUriTest.java         | 64 ++++++++++++++++
 .../pages/starters/undertow-spring-security.adoc   | 21 +++++-
 10 files changed, 305 insertions(+), 2 deletions(-)

diff --git a/components-starter/camel-undertow-spring-security-starter/pom.xml b/components-starter/camel-undertow-spring-security-starter/pom.xml
index 0429fcc85cd..8e6ccab6e3c 100644
--- a/components-starter/camel-undertow-spring-security-starter/pom.xml
+++ b/components-starter/camel-undertow-spring-security-starter/pom.xml
@@ -56,6 +56,13 @@
       <artifactId>spring-boot-starter-security</artifactId>
       <version>${spring-boot-version}</version>
     </dependency>
+    <!-- Test dependencies -->
+    <dependency>
+      <groupId>org.springframework.boot</groupId>
+      <artifactId>spring-boot-starter-test</artifactId>
+      <version>${spring-boot-version}</version>
+      <scope>test</scope>
+    </dependency>
     <!--START OF GENERATED CODE-->
     <dependency>
       <groupId>org.apache.camel.springboot</groupId>
diff --git a/components-starter/camel-undertow-spring-security-starter/src/main/doc/intro.adoc b/components-starter/camel-undertow-spring-security-starter/src/main/doc/intro.adoc
index e748d4b2ae0..a9046a2d389 100644
--- a/components-starter/camel-undertow-spring-security-starter/src/main/doc/intro.adoc
+++ b/components-starter/camel-undertow-spring-security-starter/src/main/doc/intro.adoc
@@ -1,3 +1,21 @@
 Spring Boot auto-configuration for Camel Undertow with Spring Security.
 
 This starter secures Camel HTTP endpoints served by the Undertow component using Spring Security. It supports OAuth2/OpenID Connect providers (such as Keycloak) for authentication and authorization of incoming HTTP requests to Camel routes.
+
+== Token validation
+
+Incoming JWTs are validated against the configured provider on three points: the standard timestamp checks,
+the `iss` claim (which must match the configured realm), and the audience — the token must carry the
+configured `clientId` in its `aud` claim. Keycloak may require an Audience protocol mapper to add the service
+client to access tokens.
+
+The audience check matters because every client of a realm is served by the same signing key. Without it, a
+token minted for any other client in the realm — including a low-trust public one — satisfies the signature
+check and is accepted here.
+
+It can be turned off if an existing deployment relies on tokens minted for a different client:
+
+[source,properties]
+----
+camel.security.undertow.keycloak.validate-audience = false
+----
diff --git a/components-starter/camel-undertow-spring-security-starter/src/main/docs/undertow-spring-security.json b/components-starter/camel-undertow-spring-security-starter/src/main/docs/undertow-spring-security.json
index f012f2dfab8..304c56c315b 100644
--- a/components-starter/camel-undertow-spring-security-starter/src/main/docs/undertow-spring-security.json
+++ b/components-starter/camel-undertow-spring-security-starter/src/main/docs/undertow-spring-security.json
@@ -43,6 +43,13 @@
       "description": "Name of the attribute, which will be used as username.",
       "sourceType": "org.apache.camel.undertow.spring.boot.providers.KeycloakProviderConfiguration",
       "defaultValue": "preferred_username"
+    },
+    {
+      "name": "camel.security.undertow.keycloak.validate-audience",
+      "type": "java.lang.Boolean",
+      "description": "Whether an incoming token must carry the configured client id in its aud claim. Every client of a realm shares the signing key, so with this disabled a token minted for any other client of the same realm is accepted.",
+      "sourceType": "org.apache.camel.undertow.spring.boot.providers.KeycloakProviderConfiguration",
+      "defaultValue": true
     }
   ],
   "hints": [],
diff --git a/components-starter/camel-undertow-spring-security-starter/src/main/java/org/apache/camel/undertow/spring/boot/JwtAudienceValidator.java b/components-starter/camel-undertow-spring-security-starter/src/main/java/org/apache/camel/undertow/spring/boot/JwtAudienceValidator.java
new file mode 100644
index 00000000000..d87a285361e
--- /dev/null
+++ b/components-starter/camel-undertow-spring-security-starter/src/main/java/org/apache/camel/undertow/spring/boot/JwtAudienceValidator.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.undertow.spring.boot;
+
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.OAuth2TokenValidator;
+import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
+import org.springframework.security.oauth2.jwt.Jwt;
+
+import java.util.List;
+
+/**
+ * Checks that a JWT was actually issued for the configured client.
+ * <p/>
+ * A signature check alone only proves the token came from the realm; every client of that realm shares it. Without
+ * this validator a token minted for any other client in the same realm is accepted here, so the token is bound to the
+ * configured client id through the {@code aud} claim.
+ */
+class JwtAudienceValidator implements OAuth2TokenValidator<Jwt> {
+
+    private final String clientId;
+
+    JwtAudienceValidator(String clientId) {
+        this.clientId = clientId;
+    }
+
+    @Override
+    public OAuth2TokenValidatorResult validate(Jwt token) {
+        final List<String> audience = token.getAudience();
+        if (audience != null && audience.contains(clientId)) {
+            return OAuth2TokenValidatorResult.success();
+        }
+        return OAuth2TokenValidatorResult.failure(new OAuth2Error(
+                OAuth2ErrorCodes.INVALID_TOKEN,
+                "The token audience does not contain the configured client id: '" + clientId + "'",
+                "https://datatracker.ietf.org/doc/html/rfc9068#section-4"));
+    }
+}
diff --git a/components-starter/camel-undertow-spring-security-starter/src/main/java/org/apache/camel/undertow/spring/boot/UndertowSpringSecurityCustomizer.java b/components-starter/camel-undertow-spring-security-starter/src/main/java/org/apache/camel/undertow/spring/boot/UndertowSpringSecurityCustomizer.java
index 19d42874344..7320061ba6e 100644
--- a/components-starter/camel-undertow-spring-security-starter/src/main/java/org/apache/camel/undertow/spring/boot/UndertowSpringSecurityCustomizer.java
+++ b/components-starter/camel-undertow-spring-security-starter/src/main/java/org/apache/camel/undertow/spring/boot/UndertowSpringSecurityCustomizer.java
@@ -42,7 +42,11 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
 import org.springframework.security.oauth2.client.registration.ClientRegistration;
 import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
 import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
+import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
+import org.springframework.security.oauth2.core.OAuth2TokenValidator;
+import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtValidators;
 import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
 import org.springframework.security.web.SecurityFilterChain;
 
@@ -106,9 +110,27 @@ public class UndertowSpringSecurityCustomizer implements ComponentCustomizer {
         final String jwkSetUri = getClientRegistration().getProviderDetails().getJwkSetUri();
         final NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
         jwtDecoder.setClaimSetConverter(new KeycloakUsernameSubClaimAdapter(getProvider().getUserNameAttribute()));
+        // building the decoder from the JWK set URI pins the signing keys but installs no claim validation beyond
+        // timestamps, so bind the token to the configured issuer and client explicitly
+        jwtDecoder.setJwtValidator(jwtValidator());
         return jwtDecoder;
     }
 
+    private OAuth2TokenValidator<Jwt> jwtValidator() {
+        final String issuerUri;
+        try {
+            issuerUri = getProvider().getIssuerUri();
+        } catch (URISyntaxException e) {
+            throw new IllegalArgumentException("Provider url is not correct.", e);
+        }
+        final OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
+        if (!getProvider().isValidateAudience()) {
+            return withIssuer;
+        }
+        return new DelegatingOAuth2TokenValidator<>(withIssuer,
+                new JwtAudienceValidator(getClientRegistration().getClientId()));
+    }
+
     @Bean
     public ClientRegistrationRepository clientRegistrationRepository() {
         return new InMemoryClientRegistrationRepository(Collections.singletonList(getClientRegistration()));
diff --git a/components-starter/camel-undertow-spring-security-starter/src/main/java/org/apache/camel/undertow/spring/boot/providers/AbstractProviderConfiguration.java b/components-starter/camel-undertow-spring-security-starter/src/main/java/org/apache/camel/undertow/spring/boot/providers/AbstractProviderConfiguration.java
index 468e95b4247..dcc4e6526dc 100644
--- a/components-starter/camel-undertow-spring-security-starter/src/main/java/org/apache/camel/undertow/spring/boot/providers/AbstractProviderConfiguration.java
+++ b/components-starter/camel-undertow-spring-security-starter/src/main/java/org/apache/camel/undertow/spring/boot/providers/AbstractProviderConfiguration.java
@@ -32,12 +32,31 @@ public abstract class AbstractProviderConfiguration {
         keycloak
     }
 
+    /**
+     * Whether an incoming token must carry the configured client id in its aud claim. Every client of a realm shares
+     * the signing key, so with this disabled a token minted for any other client of the same realm is accepted.
+     */
+    private boolean validateAudience = true;
+
     abstract TYPE getType();
 
     public abstract ClientRegistration getClientRegistration() throws URISyntaxException;
 
     public abstract String getUserNameAttribute();
 
+    /**
+     * The issuer the provider stamps into the {@code iss} claim of the tokens it mints.
+     */
+    public abstract String getIssuerUri() throws URISyntaxException;
+
+    public boolean isValidateAudience() {
+        return validateAudience;
+    }
+
+    public void setValidateAudience(boolean validateAudience) {
+        this.validateAudience = validateAudience;
+    }
+
     public Converter<Jwt, ? extends AbstractAuthenticationToken> getJwtAuthenticationConverter() {
         throw new IllegalArgumentException("Not implemented");
     }
diff --git a/components-starter/camel-undertow-spring-security-starter/src/main/java/org/apache/camel/undertow/spring/boot/providers/KeycloakProviderConfiguration.java b/components-starter/camel-undertow-spring-security-starter/src/main/java/org/apache/camel/undertow/spring/boot/providers/KeycloakProviderConfiguration.java
index 37a322066f2..49e84b33b9a 100644
--- a/components-starter/camel-undertow-spring-security-starter/src/main/java/org/apache/camel/undertow/spring/boot/providers/KeycloakProviderConfiguration.java
+++ b/components-starter/camel-undertow-spring-security-starter/src/main/java/org/apache/camel/undertow/spring/boot/providers/KeycloakProviderConfiguration.java
@@ -60,9 +60,18 @@ public class KeycloakProviderConfiguration extends AbstractProviderConfiguration
         return new KeycloakJwtAuthenticationConverter();
     }
 
+    @Override
+    public String getIssuerUri() throws URISyntaxException {
+        return realmUri().toString();
+    }
+
+    private URI realmUri() throws URISyntaxException {
+        return new URI(getUrl()).resolve("/auth/realms/" + getRealmId());
+    }
+
     @Override
     public ClientRegistration getClientRegistration() throws URISyntaxException {
-        URI keycloakUri = new URI(getUrl()).resolve("/auth/realms/" + getRealmId() + "/protocol/openid-connect");
+        URI keycloakUri = URI.create(realmUri() + "/protocol/openid-connect");
         return ClientRegistration.withRegistrationId(getType().name()).clientId(getClientId())
                 .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                 .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}").scope("openid", "profile", "email")
diff --git a/components-starter/camel-undertow-spring-security-starter/src/test/java/org/apache/camel/undertow/spring/boot/JwtAudienceValidatorTest.java b/components-starter/camel-undertow-spring-security-starter/src/test/java/org/apache/camel/undertow/spring/boot/JwtAudienceValidatorTest.java
new file mode 100644
index 00000000000..1f09c62051c
--- /dev/null
+++ b/components-starter/camel-undertow-spring-security-starter/src/test/java/org/apache/camel/undertow/spring/boot/JwtAudienceValidatorTest.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.undertow.spring.boot;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
+import org.springframework.security.oauth2.jwt.Jwt;
+
+import java.time.Instant;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A token signed by the realm is not automatically a token meant for this client: every client of the realm shares the
+ * signing key, so the audience has to be checked explicitly.
+ */
+public class JwtAudienceValidatorTest {
+
+    private static final String CLIENT_ID = "camel-client";
+
+    private final JwtAudienceValidator validator = new JwtAudienceValidator(CLIENT_ID);
+
+    private static Jwt token(List<String> audience, String authorizedParty) {
+        Jwt.Builder builder = Jwt.withTokenValue("token")
+                .header("alg", "RS256")
+                .issuedAt(Instant.now())
+                .expiresAt(Instant.now().plusSeconds(300))
+                .claim("sub", "someone");
+        if (audience != null) {
+            builder.audience(audience);
+        }
+        if (authorizedParty != null) {
+            builder.claim("azp", authorizedParty);
+        }
+        return builder.build();
+    }
+
+    @Test
+    public void acceptsTokenWithThisClientInAudience() {
+        OAuth2TokenValidatorResult result = validator.validate(token(List.of(CLIENT_ID), null));
+        assertFalse(result.hasErrors(), "a token addressed to this client should be accepted");
+    }
+
+    @Test
+    public void rejectsMatchingAuthorizedPartyWhenAudienceTargetsAnotherService() {
+        OAuth2TokenValidatorResult result = validator.validate(token(List.of("account"), CLIENT_ID));
+        assertTrue(result.hasErrors(), "the authorized party must not override a mismatching audience");
+    }
+
+    @Test
+    public void rejectsTokenMintedForAnotherClientInTheSameRealm() {
+        OAuth2TokenValidatorResult result = validator.validate(token(List.of("other-spa"), "other-spa"));
+        assertTrue(result.hasErrors(), "a token issued to a different client of the same realm must be rejected");
+        assertTrue(result.getErrors().iterator().next().getDescription().contains(CLIENT_ID),
+                "the failure should name the expected client id");
+    }
+
+    @Test
+    public void rejectsTokenCarryingNoAudience() {
+        OAuth2TokenValidatorResult result = validator.validate(token(null, null));
+        assertTrue(result.hasErrors(), "a token without an audience must be rejected");
+    }
+
+    @Test
+    public void acceptsWhenThisClientIsOneOfSeveralAudiences() {
+        OAuth2TokenValidatorResult result = validator.validate(token(List.of("account", CLIENT_ID), "gateway"));
+        assertFalse(result.hasErrors(), "aud may legitimately carry several entries");
+    }
+}
diff --git a/components-starter/camel-undertow-spring-security-starter/src/test/java/org/apache/camel/undertow/spring/boot/KeycloakIssuerUriTest.java b/components-starter/camel-undertow-spring-security-starter/src/test/java/org/apache/camel/undertow/spring/boot/KeycloakIssuerUriTest.java
new file mode 100644
index 00000000000..9ca1c225ffe
--- /dev/null
+++ b/components-starter/camel-undertow-spring-security-starter/src/test/java/org/apache/camel/undertow/spring/boot/KeycloakIssuerUriTest.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.undertow.spring.boot;
+
+import org.apache.camel.undertow.spring.boot.providers.KeycloakProviderConfiguration;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The issuer used to validate the {@code iss} claim must be the realm the client registration already points at,
+ * otherwise every token would be rejected.
+ */
+public class KeycloakIssuerUriTest {
+
+    private static KeycloakProviderConfiguration provider(String url) {
+        KeycloakProviderConfiguration provider = new KeycloakProviderConfiguration();
+        provider.setUrl(url);
+        provider.setRealmId("my-realm");
+        provider.setClientId("camel-client");
+        return provider;
+    }
+
+    @Test
+    public void derivesTheRealmIssuer() throws Exception {
+        assertEquals("http://localhost:8080/auth/realms/my-realm", provider("http://localhost:8080").getIssuerUri());
+    }
+
+    @Test
+    public void ignoresAnyPathOnTheConfiguredUrl() throws Exception {
+        // the client registration resolves from the root too, so both must agree
+        assertEquals("https://sso.example.com/auth/realms/my-realm",
+                provider("https://sso.example.com/some/path").getIssuerUri());
+    }
+
+    @Test
+    public void issuerIsThePrefixOfTheJwkSetUri() throws Exception {
+        KeycloakProviderConfiguration provider = provider("http://localhost:8080");
+        String jwkSetUri = provider.getClientRegistration().getProviderDetails().getJwkSetUri();
+        assertTrue(jwkSetUri.startsWith(provider.getIssuerUri()),
+                "issuer " + provider.getIssuerUri() + " should prefix the jwk set uri " + jwkSetUri);
+        assertEquals("http://localhost:8080/auth/realms/my-realm/protocol/openid-connect/certs", jwkSetUri);
+    }
+
+    @Test
+    public void audienceValidationIsOnByDefault() {
+        assertTrue(provider("http://localhost:8080").isValidateAudience());
+    }
+}
diff --git a/docs/spring-boot/modules/ROOT/pages/starters/undertow-spring-security.adoc b/docs/spring-boot/modules/ROOT/pages/starters/undertow-spring-security.adoc
index 8c849fa8171..0303c75c2e3 100644
--- a/docs/spring-boot/modules/ROOT/pages/starters/undertow-spring-security.adoc
+++ b/docs/spring-boot/modules/ROOT/pages/starters/undertow-spring-security.adoc
@@ -7,6 +7,24 @@ Spring Boot auto-configuration for Camel Undertow with Spring Security.
 
 This starter secures Camel HTTP endpoints served by the Undertow component using Spring Security. It supports OAuth2/OpenID Connect providers (such as Keycloak) for authentication and authorization of incoming HTTP requests to Camel routes.
 
+== Token validation
+
+Incoming JWTs are validated against the configured provider on three points: the standard timestamp checks,
+the `iss` claim (which must match the configured realm), and the audience — the token must carry the
+configured `clientId` in its `aud` claim. Keycloak may require an Audience protocol mapper to add the service
+client to access tokens.
+
+The audience check matters because every client of a realm is served by the same signing key. Without it, a
+token minted for any other client in the realm — including a low-trust public one — satisfies the signature
+check and is accepted here.
+
+It can be turned off if an existing deployment relies on tokens minted for a different client:
+
+[source,properties]
+----
+camel.security.undertow.keycloak.validate-audience = false
+----
+
 == Maven coordinates
 
 [source,xml]
@@ -19,7 +37,7 @@ This starter secures Camel HTTP endpoints served by the Undertow component using
 
 == Spring Boot Auto-Configuration
 
-The starter supports 5 options, which are listed below.
+The starter supports 6 options, which are listed below.
 
 [width="100%",cols="2,5,^1,2",options="header"]
 |===
@@ -29,4 +47,5 @@ The starter supports 5 options, which are listed below.
 | camel.security.undertow.keycloak.realm-id | Realm id from the keycloak server used for authentication. |  | String
 | camel.security.undertow.keycloak.url | Url to keycloak server which will be used in spring security configuration. (Example "http://localhost:8080") |  | String
 | camel.security.undertow.keycloak.user-name-attribute | Name of the attribute, which will be used as username. | preferred_username | String
+| camel.security.undertow.keycloak.validate-audience | Whether an incoming token must carry the configured client id in its aud claim. Every client of a realm shares the signing key, so with this disabled a token minted for any other client of the same realm is accepted. | true | Boolean
 |===