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

oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new bb4922598c39 CAMEL-24319: camel-keycloak - add optional token type 
(typ) and authorized party (azp) validation (#25259)
bb4922598c39 is described below

commit bb4922598c399f885107088a211e36107c727f2b
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Jul 31 12:54:13 2026 +0200

    CAMEL-24319: camel-keycloak - add optional token type (typ) and authorized 
party (azp) validation (#25259)
    
    Adds optional validation of the token type (typ) and authorized party (azp) 
claims to camel-keycloak, following the same opt-in gating as the CAMEL-23875 
audience validation. The checks are wired into all three enforcement paths — 
authenticateToken(), validateRoles() and validatePermissions() — including the 
permissions-only introspection path (useTokenIntrospection=true with only 
requiredPermissions), where gnodet's review found they had initially been 
missed and could be bypassed. D [...]
    
    Co-authored-by: Claude Opus 4.8 <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
---
 .../camel/catalog/docs/keycloak-security.adoc      | 28 +++++++
 .../src/main/docs/keycloak-security.adoc           | 28 +++++++
 .../keycloak/security/KeycloakSecurityHelper.java  | 55 ++++++++++++
 .../keycloak/security/KeycloakSecurityPolicy.java  | 68 +++++++++++++++
 .../security/KeycloakSecurityProcessor.java        | 81 +++++++++++++++++-
 .../security/KeycloakSecurityHelperTest.java       | 97 ++++++++++++++++++++++
 .../security/KeycloakSecurityProcessorTest.java    | 79 ++++++++++++++++++
 7 files changed, 435 insertions(+), 1 deletion(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/keycloak-security.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/keycloak-security.adoc
index d18ac18c1c56..1700046c008d 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/keycloak-security.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/keycloak-security.adoc
@@ -116,6 +116,34 @@ beans:
 `expectedAudience` accepts a comma-separated list (e.g., 
`"my-client,my-other-client"`) when a token must carry
 several audiences at once. Disabled by default for backward compatibility.
 
+=== Token Type and Authorized Party Validation
+
+By default, the security policy does not check the token's `typ` (token type) 
or `azp` (authorized party) claim.
+Two optional settings tighten this:
+
+* `expectedTokenTypes` - a comma-separated allow-list of accepted `typ` values 
(e.g., `"Bearer"`). When set, a
+token whose `typ` is not one of the configured values is rejected. This guards 
against token-type confusion,
+for example an ID token or refresh token being presented where an access token 
is expected.
+* `expectedAuthorizedParty` - the expected `azp` value (e.g., `"my-client"`). 
When set, a token whose `azp` does
+not equal the configured value is rejected, ensuring the token was issued for 
the expected client.
+
+Both checks apply to local JWT verification and to token introspection, and 
both are disabled by default for
+backward compatibility.
+
+[source,java]
+----
+KeycloakSecurityPolicy policy = new KeycloakSecurityPolicy(
+    "http://localhost:8080";, "my-realm", "my-client", "client-secret");
+
+// Only accept access tokens (typ=Bearer) issued for "my-client"
+policy.setExpectedTokenTypes("Bearer");
+policy.setExpectedAuthorizedParty("my-client");
+
+from("direct:protected")
+    .policy(policy)
+    .to("mock:result");
+----
+
 === Role-based Authorization
 
 [tabs]
diff --git a/components/camel-keycloak/src/main/docs/keycloak-security.adoc 
b/components/camel-keycloak/src/main/docs/keycloak-security.adoc
index d18ac18c1c56..1700046c008d 100644
--- a/components/camel-keycloak/src/main/docs/keycloak-security.adoc
+++ b/components/camel-keycloak/src/main/docs/keycloak-security.adoc
@@ -116,6 +116,34 @@ beans:
 `expectedAudience` accepts a comma-separated list (e.g., 
`"my-client,my-other-client"`) when a token must carry
 several audiences at once. Disabled by default for backward compatibility.
 
+=== Token Type and Authorized Party Validation
+
+By default, the security policy does not check the token's `typ` (token type) 
or `azp` (authorized party) claim.
+Two optional settings tighten this:
+
+* `expectedTokenTypes` - a comma-separated allow-list of accepted `typ` values 
(e.g., `"Bearer"`). When set, a
+token whose `typ` is not one of the configured values is rejected. This guards 
against token-type confusion,
+for example an ID token or refresh token being presented where an access token 
is expected.
+* `expectedAuthorizedParty` - the expected `azp` value (e.g., `"my-client"`). 
When set, a token whose `azp` does
+not equal the configured value is rejected, ensuring the token was issued for 
the expected client.
+
+Both checks apply to local JWT verification and to token introspection, and 
both are disabled by default for
+backward compatibility.
+
+[source,java]
+----
+KeycloakSecurityPolicy policy = new KeycloakSecurityPolicy(
+    "http://localhost:8080";, "my-realm", "my-client", "client-secret");
+
+// Only accept access tokens (typ=Bearer) issued for "my-client"
+policy.setExpectedTokenTypes("Bearer");
+policy.setExpectedAuthorizedParty("my-client");
+
+from("direct:protected")
+    .policy(policy)
+    .to("mock:result");
+----
+
 === Role-based Authorization
 
 [tabs]
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 f529de805074..02277592b477 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
@@ -90,6 +90,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");
         }
@@ -120,6 +150,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 943f3127079b..fb2fd7d34f3e 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
@@ -115,6 +115,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);
         }
@@ -284,6 +292,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 {
@@ -340,7 +357,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);
@@ -416,6 +434,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;
@@ -440,6 +509,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");
+    }
 }

Reply via email to