This is an automated email from the ASF dual-hosted git repository.
Croway pushed a commit to branch camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.22.x by this push:
new c9e69aae7b37 CAMEL-24448: camel-keycloak - reject an introspection
result that carries no issuer (#25827)
c9e69aae7b37 is described below
commit c9e69aae7b374cc38d28c1c8dca744c1ba4d6718
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Aug 28 07:22:18 2026 +0200
CAMEL-24448: camel-keycloak - reject an introspection result that carries
no issuer (#25827)
Reject a token-introspection response that carries no issuer, so the
keycloak component does not accept an introspection result whose issuer cannot
be validated.
Co-authored-by: Claude Opus 4.8 <[email protected]>
---
.../security/KeycloakSecurityProcessor.java | 16 ++++--
.../security/KeycloakSecurityProcessorTest.java | 67 ++++++++++++++++++++++
2 files changed, 79 insertions(+), 4 deletions(-)
diff --git
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessor.java
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessor.java
index fb2fd7d34f3e..39f3e56a8730 100644
---
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessor.java
+++
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessor.java
@@ -375,7 +375,11 @@ public class KeycloakSecurityProcessor extends
DelegateProcessor {
}
/**
- * Validates the issuer from an introspection result.
+ * Validates the issuer from an introspection result. A token whose "iss"
claim is missing is rejected: issuer
+ * validation is opt-in, so an operator who turned it on is asking for
tokens from other issuers to be refused, and
+ * a response that carries no issuer is not evidence that the token came
from the expected one. RFC 7662 makes "iss"
+ * optional in an introspection response, so this is reachable wherever
the introspection endpoint is a broker, a
+ * gateway or a minimal implementation rather than the realm that issued
the token.
*/
private void validateIssuerFromIntrospection(
KeycloakTokenIntrospector.IntrospectionResult introspectionResult,
Exchange exchange)
@@ -384,8 +388,12 @@ public class KeycloakSecurityProcessor extends
DelegateProcessor {
Object issuerClaim = introspectionResult.getClaim("iss");
if (issuerClaim == null) {
- LOG.warn("Token introspection result does not contain issuer
claim");
- return;
+ LOG.error("SECURITY: Token introspection result does not contain
an issuer claim, expected '{}'",
+ expectedIssuer);
+ throw new CamelAuthorizationException(
+ String.format("Token issuer missing: expected '%s' but the
introspection result carries no issuer",
+ expectedIssuer),
+ exchange);
}
String actualIssuer = issuerClaim.toString();
@@ -401,7 +409,7 @@ public class KeycloakSecurityProcessor extends
DelegateProcessor {
}
/**
- * Validates the audience from an introspection result. Unlike issuer
validation, a token whose "aud" claim is
+ * Validates the audience from an introspection result. As with issuer
validation, a token whose "aud" claim is
* missing is rejected: the whole point of this check is to reject tokens
that were not issued for this policy's
* client(s).
*/
diff --git
a/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessorTest.java
b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessorTest.java
index c13a39723e0b..837ddd22c4d9 100644
---
a/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessorTest.java
+++
b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessorTest.java
@@ -121,6 +121,73 @@ class KeycloakSecurityProcessorTest {
assertFalse(routeReached.get(), "Route body must not be reached for an
inactive token");
}
+ /**
+ * Issuer validation is opt-in, so an operator who enabled it is asking
for tokens from other issuers to be refused.
+ * RFC 7662 makes "iss" optional in an introspection response, so an
introspection endpoint that is a broker, a
+ * gateway, or a minimal implementation can return an active token with no
issuer - which is not evidence that the
+ * token came from the expected one.
+ */
+ @Test
+ void testActiveIntrospectionWithoutIssuerClaimRejected() throws Exception {
+ KeycloakTokenIntrospector introspector =
introspectorReturning(Map.of("active", true));
+
+ KeycloakSecurityPolicy policy = introspectionPolicy(introspector);
+ policy.setValidateIssuer(true);
+
+ AtomicBoolean routeReached = new AtomicBoolean(false);
+ KeycloakSecurityProcessor processor = new KeycloakSecurityProcessor(e
-> routeReached.set(true), policy);
+
+ CamelAuthorizationException e
+ = assertThrows(CamelAuthorizationException.class, () ->
processor.process(bearer("x")));
+ assertTrue(e.getMessage().contains("Token issuer missing"),
"unexpected message: " + e.getMessage());
+ assertFalse(routeReached.get(), "Route body must not be reached for a
token with no issuer");
+ }
+
+ @Test
+ void testActiveIntrospectionWithTheExpectedIssuerAccepted() throws
Exception {
+ // getExpectedIssuer() is derived as serverUrl + "/realms/" + realm
+ String issuer = "http://localhost:8080/realms/test-realm";
+ KeycloakTokenIntrospector introspector =
introspectorReturning(Map.of("active", true, "iss", issuer));
+
+ KeycloakSecurityPolicy policy = introspectionPolicy(introspector);
+ policy.setValidateIssuer(true);
+
+ AtomicBoolean routeReached = new AtomicBoolean(false);
+ KeycloakSecurityProcessor processor = new KeycloakSecurityProcessor(e
-> routeReached.set(true), policy);
+
+ processor.process(bearer("x"));
+ assertTrue(routeReached.get(), "Route body must be reached for a token
from the expected issuer");
+ }
+
+ private static KeycloakTokenIntrospector introspectorReturning(Map<String,
Object> claims) {
+ return new KeycloakTokenIntrospector(
+ "http://localhost:8080", "test-realm", "test-client",
"test-secret", (TokenCache) null) {
+ @Override
+ public IntrospectionResult introspect(String token) {
+ return new IntrospectionResult(claims);
+ }
+ };
+ }
+
+ private static KeycloakSecurityPolicy
introspectionPolicy(KeycloakTokenIntrospector introspector) {
+ KeycloakSecurityPolicy policy = new KeycloakSecurityPolicy() {
+ @Override
+ public boolean isUseTokenIntrospection() {
+ return true;
+ }
+
+ @Override
+ public KeycloakTokenIntrospector getTokenIntrospector() {
+ return introspector;
+ }
+ };
+ policy.setServerUrl("http://localhost:8080");
+ policy.setRealm("test-realm");
+ policy.setClientId("test-client");
+ policy.setClientSecret("test-secret");
+ return policy;
+ }
+
@Test
void testTokenMissingExpectedAudienceRejectedLocalJwt() throws Exception {
String issuer = "http://localhost:8080/realms/test-realm";