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

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


The following commit(s) were added to refs/heads/camel-4.18.x by this push:
     new aba0f7551e4f CAMEL-24448: camel-keycloak - reject an introspection 
result that carries no issuer (#25827)
aba0f7551e4f is described below

commit aba0f7551e4f63e1f37498454870a49ccdaae727
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 6ccf59c39ddd..38da7d5d2ce4 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
@@ -374,7 +374,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)
@@ -383,8 +387,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();
@@ -400,7 +408,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";;

Reply via email to