You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ac...@apache.org on 2026/08/03 11:32:31 UTC

(camel) branch camel-4.18.x updated: [backport camel-4.18.x] CAMEL-24319: camel-keycloak - add optional token type (typ) and authorized party (azp) validation (#25259) (#25307)

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

oscerd 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 895b7647b9b7 [backport camel-4.18.x] CAMEL-24319: camel-keycloak - add optional token type (typ) and authorized party (azp) validation (#25259) (#25307)
895b7647b9b7 is described below

commit 895b7647b9b7516adda0b6c0095284c13f6a6554
Author: Andrea Cosentino <an...@gmail.com>
AuthorDate: Mon Aug 3 13:32:24 2026 +0200

    [backport camel-4.18.x] CAMEL-24319: camel-keycloak - add optional token type (typ) and authorized party (azp) validation (#25259) (#25307)
    
    Opt-in expectedTokenTypes + expectedAuthorizedParty on KeycloakSecurityPolicy,
    applied on all three token-validation paths (authenticateToken, validateRoles,
    validatePermissions) for both local JWT verification and token introspection.
    Disabled by default (non-breaking). The documentation page is main-only.
    
    (cherry picked from commit bb4922598c39fef67b39f3a56b6b17b1a0e5b0c8)
    
    Co-authored-by: Claude Opus 4.8 <no...@anthropic.com>
---
 .../keycloak/security/KeycloakSecurityHelper.java  | 55 ++++++++++++
 .../keycloak/security/KeycloakSecurityPolicy.java  | 68 +++++++++++++++
 .../security/KeycloakSecurityProcessor.java        | 81 +++++++++++++++++-
 .../security/KeycloakSecurityHelperTest.java       | 97 ++++++++++++++++++++++
 .../security/KeycloakSecurityProcessorTest.java    | 79 ++++++++++++++++++
 5 files changed, 379 insertions(+), 1 deletion(-)

diff --git a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityHelper.java b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityHelper.java
index 39549c1c6034..a274f9ce9406 100644
--- a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityHelper.java
+++ b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityHelper.java
@@ -89,6 +89,36 @@ public final class KeycloakSecurityHelper {
     public static AccessToken parseAndVerifyAccessToken(
             String tokenString, PublicKey publicKey, String expectedIssuer, List<String> expectedAudiences)
             throws VerificationException {
+        return parseAndVerifyAccessToken(tokenString, publicKey, expectedIssuer, expectedAudiences, null, null);
+    }
+
+    /**
+     * Parses and fully verifies an access token including signature, issuer and, optionally, audience, token type
+     * ({@code typ}) and authorized party ({@code azp}) validation. This is the recommended method for secure token
+     * validation.
+     *
+     * @param  tokenString             the JWT token string
+     * @param  publicKey               the public key for signature verification
+     * @param  expectedIssuer          the expected issuer URL (e.g., "http://localhost:8080/realms/myrealm")
+     * @param  expectedAudiences       the expected audiences; when non-empty, the token's "aud" claim must contain
+     *                                 every one of them (matching Keycloak's own
+     *                                 {@link TokenVerifier#audience(String...)} check). Pass null or an empty list to
+     *                                 skip audience validation.
+     * @param  expectedTokenTypes      the accepted token types; when non-empty, the token's {@code typ} claim must be
+     *                                 one of them. Guards against token-type confusion (e.g. an ID or refresh token
+     *                                 presented where an access token is expected). Pass null or an empty list to skip
+     *                                 token-type validation.
+     * @param  expectedAuthorizedParty the expected authorized party; when non-empty, the token's {@code azp} claim must
+     *                                 equal it. Ensures the token was issued for the expected client. Pass null or an
+     *                                 empty string to skip authorized-party validation.
+     * @return                         the verified access token
+     * @throws VerificationException   if verification fails (invalid signature, wrong issuer, expired, missing/wrong
+     *                                 audience, wrong token type, wrong authorized party, etc.)
+     */
+    public static AccessToken parseAndVerifyAccessToken(
+            String tokenString, PublicKey publicKey, String expectedIssuer, List<String> expectedAudiences,
+            List<String> expectedTokenTypes, String expectedAuthorizedParty)
+            throws VerificationException {
         if (publicKey == null) {
             throw new VerificationException("Public key is required for secure token verification");
         }
@@ -119,6 +149,31 @@ public final class KeycloakSecurityHelper {
                     String.format("Token issuer mismatch: expected '%s' but got '%s'", expectedIssuer, actualIssuer));
         }
 
+        // Optional token type (typ) allow-list — guards against token-type confusion, e.g. an ID or refresh token
+        // being presented where an access token is expected.
+        if (expectedTokenTypes != null && !expectedTokenTypes.isEmpty()) {
+            String actualType = token.getType();
+            if (actualType == null || !expectedTokenTypes.contains(actualType)) {
+                LOG.error("SECURITY: Token type mismatch - expected one of {} but got '{}'",
+                        expectedTokenTypes, actualType);
+                throw new VerificationException(
+                        String.format("Token type mismatch: expected one of %s but got '%s'",
+                                expectedTokenTypes, actualType));
+            }
+        }
+
+        // Optional authorized party (azp) check — ensures the token was issued for the expected client.
+        if (expectedAuthorizedParty != null && !expectedAuthorizedParty.isEmpty()) {
+            String actualAzp = token.getIssuedFor();
+            if (!expectedAuthorizedParty.equals(actualAzp)) {
+                LOG.error("SECURITY: Token authorized party (azp) mismatch - expected '{}' but got '{}'",
+                        expectedAuthorizedParty, actualAzp);
+                throw new VerificationException(
+                        String.format("Token authorized party mismatch: expected '%s' but got '%s'",
+                                expectedAuthorizedParty, actualAzp));
+            }
+        }
+
         LOG.debug("Token successfully verified for issuer: {}", expectedIssuer);
         return token;
     }
diff --git a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityPolicy.java b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityPolicy.java
index 17781c45ec21..55d76ea17b9c 100644
--- a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityPolicy.java
+++ b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityPolicy.java
@@ -106,6 +106,21 @@ public class KeycloakSecurityPolicy implements AuthorizationPolicy {
      */
     private String expectedAudience;
 
+    /**
+     * Comma-separated list of accepted token types ({@code typ} claim). When set, a token whose {@code typ} is not one
+     * of the configured values is rejected. This guards against token-type confusion, e.g. an ID token or refresh token
+     * being presented where an access token is expected. Disabled by default for backward compatibility. Example:
+     * "Bearer"
+     */
+    private String expectedTokenTypes;
+
+    /**
+     * Expected authorized party ({@code azp} claim). When set, a token whose {@code azp} does not equal the configured
+     * value is rejected, ensuring the token was issued for the expected client. Disabled by default for backward
+     * compatibility. Example: "my-client"
+     */
+    private String expectedAuthorizedParty;
+
     public KeycloakSecurityPolicy() {
         this.requiredRoles = "";
         this.requiredPermissions = "";
@@ -470,4 +485,57 @@ public class KeycloakSecurityPolicy implements AuthorizationPolicy {
                 .filter(s -> !s.isEmpty())
                 .collect(Collectors.toList());
     }
+
+    /**
+     * Gets the accepted token type(s) as a comma-separated string.
+     *
+     * @return comma-separated token types (e.g., "Bearer"), or null if not configured
+     */
+    public String getExpectedTokenTypes() {
+        return expectedTokenTypes;
+    }
+
+    /**
+     * Sets the accepted token type(s) as a comma-separated string. When set, a token whose "typ" claim is not one of
+     * the configured values is rejected.
+     *
+     * @param expectedTokenTypes comma-separated token types (e.g., "Bearer")
+     */
+    public void setExpectedTokenTypes(String expectedTokenTypes) {
+        this.expectedTokenTypes = expectedTokenTypes;
+    }
+
+    /**
+     * Gets the accepted token types as a list.
+     *
+     * @return list of accepted token types, or an empty list if not configured
+     */
+    public List<String> getExpectedTokenTypesAsList() {
+        if (ObjectHelper.isEmpty(expectedTokenTypes)) {
+            return Collections.emptyList();
+        }
+        return Arrays.stream(expectedTokenTypes.split(","))
+                .map(String::trim)
+                .filter(s -> !s.isEmpty())
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Gets the expected authorized party ({@code azp}).
+     *
+     * @return the expected authorized party, or null if not configured
+     */
+    public String getExpectedAuthorizedParty() {
+        return expectedAuthorizedParty;
+    }
+
+    /**
+     * Sets the expected authorized party ({@code azp}). When set, a token whose "azp" claim does not equal the
+     * configured value is rejected.
+     *
+     * @param expectedAuthorizedParty the expected authorized party (e.g., "my-client")
+     */
+    public void setExpectedAuthorizedParty(String expectedAuthorizedParty) {
+        this.expectedAuthorizedParty = expectedAuthorizedParty;
+    }
 }
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 df9f958c4871..6ccf59c39ddd 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
@@ -114,6 +114,14 @@ public class KeycloakSecurityProcessor extends DelegateProcessor {
             if (!policy.getExpectedAudienceAsList().isEmpty()) {
                 validateAudienceFromIntrospection(introspectionResult, exchange);
             }
+
+            if (!policy.getExpectedTokenTypesAsList().isEmpty()) {
+                validateTokenTypeFromIntrospection(introspectionResult, exchange);
+            }
+
+            if (!ObjectHelper.isEmpty(policy.getExpectedAuthorizedParty())) {
+                validateAuthorizedPartyFromIntrospection(introspectionResult, exchange);
+            }
         } else {
             parseAndVerifyToken(accessToken, exchange);
         }
@@ -283,6 +291,15 @@ public class KeycloakSecurityProcessor extends DelegateProcessor {
                     validateAudienceFromIntrospection(introspectionResult, exchange);
                 }
 
+                // Validate token type / authorized party from introspection result if configured
+                if (!policy.getExpectedTokenTypesAsList().isEmpty()) {
+                    validateTokenTypeFromIntrospection(introspectionResult, exchange);
+                }
+
+                if (!ObjectHelper.isEmpty(policy.getExpectedAuthorizedParty())) {
+                    validateAuthorizedPartyFromIntrospection(introspectionResult, exchange);
+                }
+
                 userRoles = KeycloakSecurityHelper.extractRolesFromIntrospection(
                         introspectionResult, policy.getRealm(), policy.getClientId());
             } else {
@@ -339,7 +356,8 @@ public class KeycloakSecurityProcessor extends DelegateProcessor {
         if (publicKey != null) {
             try {
                 return KeycloakSecurityHelper.parseAndVerifyAccessToken(
-                        accessToken, publicKey, expectedIssuer, policy.getExpectedAudienceAsList());
+                        accessToken, publicKey, expectedIssuer, policy.getExpectedAudienceAsList(),
+                        policy.getExpectedTokenTypesAsList(), policy.getExpectedAuthorizedParty());
             } catch (VerificationException e) {
                 LOG.error("Token verification failed: {}", e.getMessage());
                 throw new CamelAuthorizationException("Token verification failed: " + e.getMessage(), exchange, e);
@@ -415,6 +433,57 @@ public class KeycloakSecurityProcessor extends DelegateProcessor {
         LOG.debug("Audience validation from introspection successful: {}", expectedAudiences);
     }
 
+    /**
+     * Validates the token type ({@code typ}) from an introspection result. When token-type validation is configured, a
+     * token whose {@code typ} is missing or not among the accepted values is rejected — guarding against token-type
+     * confusion (e.g. an ID or refresh token presented where an access token is expected).
+     */
+    private void validateTokenTypeFromIntrospection(
+            KeycloakTokenIntrospector.IntrospectionResult introspectionResult, Exchange exchange)
+            throws CamelAuthorizationException {
+        List<String> expectedTokenTypes = policy.getExpectedTokenTypesAsList();
+        // Use the JWT "typ" claim (token category, e.g. Bearer/Refresh/ID), which Keycloak forwards on its
+        // introspection response — not the RFC 7662 "token_type" field, which is the OAuth token type ("Bearer")
+        // and does not distinguish access from refresh/ID tokens, i.e. it cannot express what this check validates.
+        Object typeClaim = introspectionResult.getClaim("typ");
+        String actualType = typeClaim instanceof String s ? s : null;
+
+        if (actualType == null || !expectedTokenTypes.contains(actualType)) {
+            LOG.error("SECURITY: Token type mismatch from introspection - expected one of {} but got '{}'",
+                    expectedTokenTypes, actualType);
+            throw new CamelAuthorizationException(
+                    String.format("Token type mismatch: expected one of %s but got '%s'",
+                            expectedTokenTypes, actualType),
+                    exchange);
+        }
+
+        LOG.debug("Token type validation from introspection successful: {}", expectedTokenTypes);
+    }
+
+    /**
+     * Validates the authorized party ({@code azp}) from an introspection result. When authorized-party validation is
+     * configured, a token whose {@code azp} does not equal the expected value is rejected — ensuring the token was
+     * issued for the expected client.
+     */
+    private void validateAuthorizedPartyFromIntrospection(
+            KeycloakTokenIntrospector.IntrospectionResult introspectionResult, Exchange exchange)
+            throws CamelAuthorizationException {
+        String expectedAuthorizedParty = policy.getExpectedAuthorizedParty();
+        Object azpClaim = introspectionResult.getClaim("azp");
+        String actualAzp = azpClaim instanceof String s ? s : null;
+
+        if (!expectedAuthorizedParty.equals(actualAzp)) {
+            LOG.error("SECURITY: Token authorized party (azp) mismatch from introspection - expected '{}' but got '{}'",
+                    expectedAuthorizedParty, actualAzp);
+            throw new CamelAuthorizationException(
+                    String.format("Token authorized party mismatch: expected '%s' but got '%s'",
+                            expectedAuthorizedParty, actualAzp),
+                    exchange);
+        }
+
+        LOG.debug("Authorized party validation from introspection successful: {}", expectedAuthorizedParty);
+    }
+
     private void validatePermissions(String accessToken, Exchange exchange) throws Exception {
         try {
             Set<String> userPermissions;
@@ -439,6 +508,16 @@ public class KeycloakSecurityProcessor extends DelegateProcessor {
                     validateAudienceFromIntrospection(introspectionResult, exchange);
                 }
 
+                // Validate token type and authorized party too, otherwise these checks could be bypassed by
+                // configuring only permissions (which skips authenticateToken/validateRoles) with introspection.
+                if (!policy.getExpectedTokenTypesAsList().isEmpty()) {
+                    validateTokenTypeFromIntrospection(introspectionResult, exchange);
+                }
+
+                if (!ObjectHelper.isEmpty(policy.getExpectedAuthorizedParty())) {
+                    validateAuthorizedPartyFromIntrospection(introspectionResult, exchange);
+                }
+
                 userPermissions = KeycloakSecurityHelper.extractPermissionsFromIntrospection(introspectionResult);
             } else {
                 // Use local JWT parsing with secure verification
diff --git a/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityHelperTest.java b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityHelperTest.java
index 90639e8374d1..539417472e9b 100644
--- a/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityHelperTest.java
+++ b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityHelperTest.java
@@ -224,6 +224,103 @@ public class KeycloakSecurityHelperTest {
         assertEquals(expectedIssuer, verified.getIssuer());
     }
 
+    @Test
+    void testParseAndVerifyAccessTokenAcceptsMatchingTokenType() throws Exception {
+        String expectedIssuer = "http://localhost:8080/realms/test";
+
+        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+        keyGen.initialize(2048);
+        KeyPair keyPair = keyGen.generateKeyPair();
+
+        AccessToken token = new AccessToken();
+        token.issuer(expectedIssuer);
+        token.subject("user-123");
+        token.exp(System.currentTimeMillis() / 1000 + 3600);
+        token.type("Bearer");
+
+        String signed = new JWSBuilder()
+                .type("JWT")
+                .jsonContent(token)
+                .rsa256(keyPair.getPrivate());
+
+        AccessToken verified = KeycloakSecurityHelper.parseAndVerifyAccessToken(
+                signed, keyPair.getPublic(), expectedIssuer, null, List.of("Bearer"), null);
+        assertEquals("user-123", verified.getSubject());
+    }
+
+    @Test
+    void testParseAndVerifyAccessTokenRejectsWrongTokenType() throws Exception {
+        String expectedIssuer = "http://localhost:8080/realms/test";
+
+        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+        keyGen.initialize(2048);
+        KeyPair keyPair = keyGen.generateKeyPair();
+
+        // An ID token presented where an access token (typ=Bearer) is expected.
+        AccessToken token = new AccessToken();
+        token.issuer(expectedIssuer);
+        token.subject("user-123");
+        token.exp(System.currentTimeMillis() / 1000 + 3600);
+        token.type("ID");
+
+        String signed = new JWSBuilder()
+                .type("JWT")
+                .jsonContent(token)
+                .rsa256(keyPair.getPrivate());
+
+        assertThrows(VerificationException.class,
+                () -> KeycloakSecurityHelper.parseAndVerifyAccessToken(
+                        signed, keyPair.getPublic(), expectedIssuer, null, List.of("Bearer"), null));
+    }
+
+    @Test
+    void testParseAndVerifyAccessTokenAcceptsMatchingAuthorizedParty() throws Exception {
+        String expectedIssuer = "http://localhost:8080/realms/test";
+
+        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+        keyGen.initialize(2048);
+        KeyPair keyPair = keyGen.generateKeyPair();
+
+        AccessToken token = new AccessToken();
+        token.issuer(expectedIssuer);
+        token.subject("user-123");
+        token.exp(System.currentTimeMillis() / 1000 + 3600);
+        token.issuedFor("my-client");
+
+        String signed = new JWSBuilder()
+                .type("JWT")
+                .jsonContent(token)
+                .rsa256(keyPair.getPrivate());
+
+        AccessToken verified = KeycloakSecurityHelper.parseAndVerifyAccessToken(
+                signed, keyPair.getPublic(), expectedIssuer, null, null, "my-client");
+        assertEquals("user-123", verified.getSubject());
+    }
+
+    @Test
+    void testParseAndVerifyAccessTokenRejectsWrongAuthorizedParty() throws Exception {
+        String expectedIssuer = "http://localhost:8080/realms/test";
+
+        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+        keyGen.initialize(2048);
+        KeyPair keyPair = keyGen.generateKeyPair();
+
+        AccessToken token = new AccessToken();
+        token.issuer(expectedIssuer);
+        token.subject("user-123");
+        token.exp(System.currentTimeMillis() / 1000 + 3600);
+        token.issuedFor("other-client");
+
+        String signed = new JWSBuilder()
+                .type("JWT")
+                .jsonContent(token)
+                .rsa256(keyPair.getPrivate());
+
+        assertThrows(VerificationException.class,
+                () -> KeycloakSecurityHelper.parseAndVerifyAccessToken(
+                        signed, keyPair.getPublic(), expectedIssuer, null, null, "my-client"));
+    }
+
     @Test
     void testExtractKeyIdReturnsKidFromHeader() throws Exception {
         KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
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 c3c1c2f8f269..c13a39723e0b 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
@@ -372,4 +372,83 @@ class KeycloakSecurityProcessorTest {
         assertFalse(routeReached.get(),
                 "Route body must not be reached when the token has the required permission but not the expected audience");
     }
+
+    @Test
+    void testTokenWrongTokenTypeRejectedWithRequiredPermissionsIntrospection() throws Exception {
+        // Token satisfies the required permission but carries the wrong token type: validatePermissions() must
+        // still run the token-type check on the introspection path, otherwise it could be bypassed with a
+        // permissions-only configuration.
+        KeycloakTokenIntrospector introspector = new KeycloakTokenIntrospector(
+                "http://localhost:8080", "test-realm", "test-client", "test-secret", (TokenCache) null) {
+            @Override
+            public IntrospectionResult introspect(String token) {
+                return new IntrospectionResult(Map.of("active", true, "scope", "read", "typ", "Refresh"));
+            }
+        };
+
+        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");
+        policy.setValidateIssuer(false);
+        policy.setExpectedTokenTypes("Bearer");
+        policy.setRequiredPermissions("read");
+
+        AtomicBoolean routeReached = new AtomicBoolean(false);
+        KeycloakSecurityProcessor processor = new KeycloakSecurityProcessor(e -> routeReached.set(true), policy);
+
+        assertThrows(CamelAuthorizationException.class, () -> processor.process(bearer("x")));
+        assertFalse(routeReached.get(),
+                "Route body must not be reached when the token has the required permission but the wrong token type");
+    }
+
+    @Test
+    void testTokenWrongAuthorizedPartyRejectedWithRequiredPermissionsIntrospection() throws Exception {
+        // Token satisfies the required permission but carries the wrong authorized party (azp): validatePermissions()
+        // must still run the azp check on the introspection path.
+        KeycloakTokenIntrospector introspector = new KeycloakTokenIntrospector(
+                "http://localhost:8080", "test-realm", "test-client", "test-secret", (TokenCache) null) {
+            @Override
+            public IntrospectionResult introspect(String token) {
+                return new IntrospectionResult(Map.of("active", true, "scope", "read", "azp", "attacker-client"));
+            }
+        };
+
+        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");
+        policy.setValidateIssuer(false);
+        policy.setExpectedAuthorizedParty("expected-client");
+        policy.setRequiredPermissions("read");
+
+        AtomicBoolean routeReached = new AtomicBoolean(false);
+        KeycloakSecurityProcessor processor = new KeycloakSecurityProcessor(e -> routeReached.set(true), policy);
+
+        assertThrows(CamelAuthorizationException.class, () -> processor.process(bearer("x")));
+        assertFalse(routeReached.get(),
+                "Route body must not be reached when the token has the required permission but the wrong authorized party");
+    }
 }