Apache Camel: Camel-Keycloak: Cross-Realm Token Acceptance Bypass in KeycloakSecurityPolicy
Description
Cross-Realm Token Acceptance Bypass in KeycloakSecurityPolicy Apache Camel Keycloak component.
The Camel-Keycloak KeycloakSecurityPolicy does not validate the iss (issuer) claim of JWT tokens against the configured realm. A token issued by one Keycloak realm is silently accepted by a policy configured for a completely different realm, breaking tenant isolation. This issue affects Apache Camel: from 4.15.0 before 4.18.0.
Users are recommended to upgrade to version 4.18.0, which fixes the issue.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Camel-Keycloak KeycloakSecurityPolicy fails to validate JWT issuer, allowing cross-realm token acceptance and breaking tenant isolation.
Vulnerability
The Apache Camel Keycloak component's KeycloakSecurityPolicy does not validate the iss (issuer) claim of JWT tokens against the configured realm. When no public key is explicitly provided (the default configuration, the token is only decoded from Base64 without signature verification or issuer check [1][4]. This means a token issued by one Keycloak realm is silently accepted by a policy configured for a completely different realm, breaking tenant isolation [1][2].
Exploitation
An attacker can exploit this by obtaining a valid JWT from any Keycloak realm and presenting it to a Camel route protected by KeycloakSecurityPolicy configured for a different realm. Since the policy only checks role names inside the token payload and does not verify the token's origin, if the role name matches across realms—common in multi-tenant setups—the request is accepted [4]. No authentication bypass is needed; the attacker simply uses a token from a realm they have access to.
Impact
In multi-tenant applications where each tenant maps to a separate Keycloak realm, a user from tenant A can access routes protected for tenant B. This leads to cross-tenant data access, privilege escalation when different realms have different role configurations, and complete bypass of realm-based security isolation [4].
Mitigation
The issue affects Apache Camel versions 4.15.0 through 4.17.0. Users are recommended to upgrade to version 4.18.0, which includes the fix [1][1][3]. The commit c1ed776e3a4fa23d15acf4b9a48fdf758d4316ff addresses the missing issuer validation [2]. No workarounds are documented.
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.apache.camel:camel-keycloakMaven | >= 4.15.0, < 4.18.0 | 4.18.0 |
Affected products
2- Apache Software Foundation/Apache Camelv5Range: 4.15.0
Patches
1c1ed776e3a4fCAMEL-22854 - Camel-Keycloak: KeycloakSecurityPolicy does not validate token issuer (#20819)
7 files changed · +429 −65
components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakPublicKeyResolver.java+173 −0 added@@ -0,0 +1,173 @@ +/* + * 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.component.keycloak.security; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Resolves and caches public keys from Keycloak's JWKS endpoint for JWT signature verification. + */ +public class KeycloakPublicKeyResolver { + private static final Logger LOG = LoggerFactory.getLogger(KeycloakPublicKeyResolver.class); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private final String serverUrl; + private final String realm; + private final Map<String, PublicKey> keyCache = new ConcurrentHashMap<>(); + private volatile long lastRefreshTime = 0; + private static final long CACHE_REFRESH_INTERVAL_MS = 300_000; // 5 minutes + + public KeycloakPublicKeyResolver(String serverUrl, String realm) { + this.serverUrl = serverUrl; + this.realm = realm; + } + + /** + * Gets the public key for verifying JWT signatures. Keys are cached and refreshed periodically. + * + * @param kid the key ID from the JWT header (optional, uses first key if null) + * @return the public key + * @throws IOException if fetching keys fails + */ + public PublicKey getPublicKey(String kid) throws IOException { + // Check if we need to refresh the cache + long now = System.currentTimeMillis(); + if (keyCache.isEmpty() || (now - lastRefreshTime) > CACHE_REFRESH_INTERVAL_MS) { + refreshKeys(); + } + + if (kid != null && keyCache.containsKey(kid)) { + return keyCache.get(kid); + } + + // If no kid specified or not found, return the first available key + if (!keyCache.isEmpty()) { + return keyCache.values().iterator().next(); + } + + throw new IOException("No public keys available from Keycloak JWKS endpoint"); + } + + /** + * Refreshes the public keys from the JWKS endpoint. + */ + public synchronized void refreshKeys() throws IOException { + String jwksUrl = String.format("%s/realms/%s/protocol/openid-connect/certs", serverUrl, realm); + LOG.debug("Fetching public keys from: {}", jwksUrl); + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpGet request = new HttpGet(jwksUrl); + + String responseBody = httpClient.execute(request, response -> { + int statusCode = response.getCode(); + if (statusCode != 200) { + throw new IOException("Failed to fetch JWKS: HTTP " + statusCode); + } + return EntityUtils.toString(response.getEntity()); + }); + + parseJwks(responseBody); + lastRefreshTime = System.currentTimeMillis(); + LOG.debug("Successfully loaded {} public keys from JWKS endpoint", keyCache.size()); + } + } + + @SuppressWarnings("unchecked") + private void parseJwks(String jwksJson) throws IOException { + Map<String, Object> jwks = OBJECT_MAPPER.readValue(jwksJson, Map.class); + List<Map<String, Object>> keys = (List<Map<String, Object>>) jwks.get("keys"); + + if (keys == null || keys.isEmpty()) { + throw new IOException("No keys found in JWKS response"); + } + + keyCache.clear(); + for (Map<String, Object> keyData : keys) { + String kty = (String) keyData.get("kty"); + String kid = (String) keyData.get("kid"); + String use = (String) keyData.get("use"); + + // Only process RSA keys used for signatures + if ("RSA".equals(kty) && (use == null || "sig".equals(use))) { + try { + PublicKey publicKey = parseRsaPublicKey(keyData); + if (kid != null) { + keyCache.put(kid, publicKey); + } + } catch (Exception e) { + LOG.warn("Failed to parse RSA key with kid '{}': {}", kid, e.getMessage()); + } + } + } + + if (keyCache.isEmpty()) { + throw new IOException("No valid RSA signature keys found in JWKS response"); + } + } + + private PublicKey parseRsaPublicKey(Map<String, Object> keyData) + throws NoSuchAlgorithmException, InvalidKeySpecException { + String n = (String) keyData.get("n"); + String e = (String) keyData.get("e"); + + if (n == null || e == null) { + throw new IllegalArgumentException("RSA key missing n or e component"); + } + + BigInteger modulus = new BigInteger(1, Base64.getUrlDecoder().decode(n)); + BigInteger exponent = new BigInteger(1, Base64.getUrlDecoder().decode(e)); + + RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return keyFactory.generatePublic(spec); + } + + /** + * Returns the expected issuer URL for this realm. + * + * @return the issuer URL + */ + public String getExpectedIssuer() { + return serverUrl + "/realms/" + realm; + } + + /** + * Clears the key cache. + */ + public void clearCache() { + keyCache.clear(); + lastRefreshTime = 0; + } +}
components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityHelper.java+35 −11 modified@@ -39,19 +39,43 @@ private KeycloakSecurityHelper() { // Utility class } - public static AccessToken parseAccessToken(String tokenString) throws VerificationException { - return parseAccessToken(tokenString, null); - } + /** + * Parses and fully verifies an access token including signature and issuer 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") + * @return the verified access token + * @throws VerificationException if verification fails (invalid signature, wrong issuer, expired, etc.) + */ + public static AccessToken parseAndVerifyAccessToken(String tokenString, PublicKey publicKey, String expectedIssuer) + throws VerificationException { + if (publicKey == null) { + throw new VerificationException("Public key is required for secure token verification"); + } + if (expectedIssuer == null || expectedIssuer.isEmpty()) { + throw new VerificationException("Expected issuer is required for secure token verification"); + } + + TokenVerifier<AccessToken> verifier = TokenVerifier.create(tokenString, AccessToken.class) + .publicKey(publicKey) + .withChecks( + TokenVerifier.SUBJECT_EXISTS_CHECK, + new TokenVerifier.RealmUrlCheck(expectedIssuer)); - public static AccessToken parseAccessToken(String tokenString, PublicKey publicKey) throws VerificationException { - if (publicKey != null) { - return TokenVerifier.create(tokenString, AccessToken.class) - .publicKey(publicKey) - .verify() - .getToken(); - } else { - return TokenVerifier.create(tokenString, AccessToken.class).getToken(); + AccessToken token = verifier.verify().getToken(); + + // Additional explicit issuer check for defense in depth + String actualIssuer = token.getIssuer(); + if (!expectedIssuer.equals(actualIssuer)) { + LOG.error("SECURITY: Token issuer mismatch - expected '{}' but got '{}'", expectedIssuer, actualIssuer); + throw new VerificationException( + String.format("Token issuer mismatch: expected '%s' but got '%s'", expectedIssuer, actualIssuer)); } + + LOG.debug("Token successfully verified for issuer: {}", expectedIssuer); + return token; } public static Set<String> extractRoles(AccessToken token, String realm, String clientId) {
components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityPolicy.java+56 −0 modified@@ -85,6 +85,19 @@ public class KeycloakSecurityPolicy implements AuthorizationPolicy { private Keycloak keycloakClient; private KeycloakTokenIntrospector tokenIntrospector; + private KeycloakPublicKeyResolver publicKeyResolver; + /** + * Enable issuer validation to ensure tokens are issued by the expected realm. When enabled (default), tokens with + * an issuer that does not match the configured serverUrl and realm will be rejected. This prevents cross-realm + * token injection attacks in multi-tenant environments. + */ + private boolean validateIssuer = true; + /** + * Enable automatic fetching of public keys from the Keycloak JWKS endpoint for signature verification. When enabled + * (default), public keys are automatically fetched and cached from + * {serverUrl}/realms/{realm}/protocol/openid-connect/certs. This ensures token signatures are properly verified. + */ + private boolean autoFetchPublicKey = true; public KeycloakSecurityPolicy() { this.requiredRoles = ""; @@ -119,6 +132,10 @@ public void beforeWrap(Route route, NamedNode definition) { if (useTokenIntrospection && tokenIntrospector == null) { initializeTokenIntrospector(); } + // Initialize public key resolver for signature and issuer validation + if (autoFetchPublicKey && publicKeyResolver == null) { + initializePublicKeyResolver(); + } } @Override @@ -153,6 +170,16 @@ private void initializeTokenIntrospector() { introspectionCacheEnabled, introspectionCacheTtl); } + private void initializePublicKeyResolver() { + if (serverUrl == null || realm == null) { + throw new IllegalArgumentException( + "Server URL and realm are required for public key resolution"); + } + publicKeyResolver = new KeycloakPublicKeyResolver(serverUrl, realm); + LOG.info("Initialized public key resolver for realm '{}' - issuer validation is {}", + realm, validateIssuer ? "enabled" : "disabled"); + } + // Getters and setters public String getServerUrl() { return serverUrl; @@ -373,4 +400,33 @@ public boolean isPreferPropertyOverHeader() { public void setPreferPropertyOverHeader(boolean preferPropertyOverHeader) { this.preferPropertyOverHeader = preferPropertyOverHeader; } + + public boolean isValidateIssuer() { + return validateIssuer; + } + + public void setValidateIssuer(boolean validateIssuer) { + this.validateIssuer = validateIssuer; + } + + public boolean isAutoFetchPublicKey() { + return autoFetchPublicKey; + } + + public void setAutoFetchPublicKey(boolean autoFetchPublicKey) { + this.autoFetchPublicKey = autoFetchPublicKey; + } + + public KeycloakPublicKeyResolver getPublicKeyResolver() { + return publicKeyResolver; + } + + /** + * Returns the expected issuer URL for this policy's realm. + * + * @return the expected issuer URL (e.g., "http://localhost:8080/realms/myrealm") + */ + public String getExpectedIssuer() { + return serverUrl + "/realms/" + realm; + } }
components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessor.java+85 −15 modified@@ -16,9 +16,11 @@ */ package org.apache.camel.component.keycloak.security; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; import java.util.Base64; import java.util.Set; @@ -27,6 +29,7 @@ import org.apache.camel.Processor; import org.apache.camel.support.processor.DelegateProcessor; import org.apache.camel.util.ObjectHelper; +import org.keycloak.common.VerificationException; import org.keycloak.representations.AccessToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -157,7 +160,8 @@ private void validateTokenBinding(Exchange exchange, String headerToken, String if (storedSubject != null) { try { // Parse token to extract subject (without full validation - just for binding check) - AccessToken accessToken = KeycloakSecurityHelper.parseAccessToken(headerToken); + // Full verification happens later in validateRoles/validatePermissions + AccessToken accessToken = org.keycloak.TokenVerifier.create(headerToken, AccessToken.class).getToken(); String currentSubject = accessToken.getSubject(); if (!storedSubject.equals(currentSubject)) { @@ -226,16 +230,16 @@ private void validateRoles(String accessToken, Exchange exchange) throws Excepti throw new CamelAuthorizationException("Token is not active (may be revoked or expired)", exchange); } + // Validate issuer from introspection result if enabled + if (policy.isValidateIssuer()) { + validateIssuerFromIntrospection(introspectionResult, exchange); + } + userRoles = KeycloakSecurityHelper.extractRolesFromIntrospection( introspectionResult, policy.getRealm(), policy.getClientId()); } else { - // Use local JWT parsing - AccessToken token; - if (ObjectHelper.isEmpty(policy.getPublicKey())) { - token = KeycloakSecurityHelper.parseAccessToken(accessToken); - } else { - token = KeycloakSecurityHelper.parseAccessToken(accessToken, policy.getPublicKey()); - } + // Use local JWT parsing with secure verification + AccessToken token = parseAndVerifyToken(accessToken, exchange); userRoles = KeycloakSecurityHelper.extractRoles(token, policy.getRealm(), policy.getClientId()); } @@ -260,6 +264,72 @@ private void validateRoles(String accessToken, Exchange exchange) throws Excepti } } + /** + * Parses and verifies the access token with full signature and issuer validation. Requires either auto-fetch public + * key or a manually configured public key. + */ + private AccessToken parseAndVerifyToken(String accessToken, Exchange exchange) throws Exception { + KeycloakPublicKeyResolver resolver = policy.getPublicKeyResolver(); + String expectedIssuer = policy.getExpectedIssuer(); + PublicKey publicKey = null; + + // Get public key from auto-fetch resolver or manual configuration + if (policy.isAutoFetchPublicKey() && resolver != null) { + try { + publicKey = resolver.getPublicKey(null); + } catch (IOException e) { + LOG.error("Failed to fetch public key from JWKS endpoint: {}", e.getMessage()); + throw new CamelAuthorizationException("Failed to fetch public key for token verification", exchange, e); + } + } else if (!ObjectHelper.isEmpty(policy.getPublicKey())) { + publicKey = policy.getPublicKey(); + } + + // Verify token with public key and issuer validation + if (publicKey != null) { + try { + return KeycloakSecurityHelper.parseAndVerifyAccessToken(accessToken, publicKey, expectedIssuer); + } catch (VerificationException e) { + LOG.error("Token verification failed: {}", e.getMessage()); + throw new CamelAuthorizationException("Token verification failed: " + e.getMessage(), exchange, e); + } + } + + // No public key available - this is a configuration error + LOG.error("SECURITY: No public key available for token verification. " + + "Enable autoFetchPublicKey or configure a publicKey manually."); + throw new CamelAuthorizationException( + "Token verification failed: no public key available. " + + "Enable autoFetchPublicKey or configure a publicKey.", + exchange); + } + + /** + * Validates the issuer from an introspection result. + */ + private void validateIssuerFromIntrospection( + KeycloakTokenIntrospector.IntrospectionResult introspectionResult, Exchange exchange) + throws CamelAuthorizationException { + String expectedIssuer = policy.getExpectedIssuer(); + Object issuerClaim = introspectionResult.getClaim("iss"); + + if (issuerClaim == null) { + LOG.warn("Token introspection result does not contain issuer claim"); + return; + } + + String actualIssuer = issuerClaim.toString(); + if (!expectedIssuer.equals(actualIssuer)) { + LOG.error("SECURITY: Token issuer mismatch from introspection - expected '{}' but got '{}'", + expectedIssuer, actualIssuer); + throw new CamelAuthorizationException( + String.format("Token issuer mismatch: expected '%s' but got '%s'", expectedIssuer, actualIssuer), + exchange); + } + + LOG.debug("Issuer validation from introspection successful: {}", expectedIssuer); + } + private void validatePermissions(String accessToken, Exchange exchange) throws Exception { try { Set<String> userPermissions; @@ -274,15 +344,15 @@ private void validatePermissions(String accessToken, Exchange exchange) throws E throw new CamelAuthorizationException("Token is not active (may be revoked or expired)", exchange); } + // Validate issuer from introspection result if enabled + if (policy.isValidateIssuer()) { + validateIssuerFromIntrospection(introspectionResult, exchange); + } + userPermissions = KeycloakSecurityHelper.extractPermissionsFromIntrospection(introspectionResult); } else { - // Use local JWT parsing - AccessToken token; - if (ObjectHelper.isEmpty(policy.getPublicKey())) { - token = KeycloakSecurityHelper.parseAccessToken(accessToken); - } else { - token = KeycloakSecurityHelper.parseAccessToken(accessToken, policy.getPublicKey()); - } + // Use local JWT parsing with secure verification + AccessToken token = parseAndVerifyToken(accessToken, exchange); userPermissions = KeycloakSecurityHelper.extractPermissions(token); }
components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityHelperTest.java+23 −6 modified@@ -132,9 +132,10 @@ void testIsTokenActive() { } @Test - void testParseAccessTokenWithPublicKey() { - // Test that verification fails with wrong public key + void testParseAndVerifyAccessTokenWithInvalidToken() { + // Test that verification fails with invalid token String invalidToken = "invalid.jwt.token"; + String expectedIssuer = "http://localhost:8080/realms/test"; try { KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); @@ -143,20 +144,36 @@ void testParseAccessTokenWithPublicKey() { PublicKey publicKey = keyPair.getPublic(); assertThrows(VerificationException.class, () -> { - KeycloakSecurityHelper.parseAccessToken(invalidToken, publicKey); + KeycloakSecurityHelper.parseAndVerifyAccessToken(invalidToken, publicKey, expectedIssuer); }); } catch (Exception e) { fail("Failed to generate test keys: " + e.getMessage()); } } @Test - void testParseAccessTokenWithNullKey() { + void testParseAndVerifyAccessTokenWithNullKey() { String invalidToken = "invalid.jwt.token"; + String expectedIssuer = "http://localhost:8080/realms/test"; - // Should not throw exception with null key, just parse without verification + // Should throw exception with null key assertThrows(VerificationException.class, () -> { - KeycloakSecurityHelper.parseAccessToken(invalidToken, null); + KeycloakSecurityHelper.parseAndVerifyAccessToken(invalidToken, null, expectedIssuer); + }); + } + + @Test + void testParseAndVerifyAccessTokenWithNullIssuer() throws Exception { + String invalidToken = "invalid.jwt.token"; + + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + KeyPair keyPair = keyGen.generateKeyPair(); + PublicKey publicKey = keyPair.getPublic(); + + // Should throw exception with null issuer + assertThrows(VerificationException.class, () -> { + KeycloakSecurityHelper.parseAndVerifyAccessToken(invalidToken, publicKey, null); }); }
components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityIT.java+37 −23 modified@@ -180,9 +180,11 @@ void testKeycloakSecurityPolicyWithPublicKeyVerification() { PublicKey publicKey = getPublicKeyFromKeycloak(); assertNotNull(publicKey); - // Test that parseToken works correctly with public key verification + // Test that parseToken works correctly with public key and issuer verification + String expectedIssuer = keycloakUrl + "/realms/" + realm; try { - org.keycloak.representations.AccessToken token = KeycloakSecurityHelper.parseAccessToken(adminToken, publicKey); + org.keycloak.representations.AccessToken token = KeycloakSecurityHelper.parseAndVerifyAccessToken( + adminToken, publicKey, expectedIssuer); assertNotNull(token); assertNotNull(token.getSubject()); @@ -195,11 +197,12 @@ void testKeycloakSecurityPolicyWithPublicKeyVerification() { } catch (Exception e) { // Public key verification might fail due to key mismatch - this is actually expected - // The main test is that we can successfully call parseAccessToken with a public key + // The main test is that we can successfully call parseAndVerifyAccessToken with a public key assertNotNull(e.getMessage()); assertTrue(e.getMessage().contains("Invalid token signature") || e.getMessage().contains("verification") || - e.getMessage().contains("signature")); + e.getMessage().contains("signature") || + e.getMessage().contains("issuer")); } // Test with public key-enabled policy route @@ -229,39 +232,43 @@ void testKeycloakSecurityPolicyWithWrongPublicKey() { } @Test - void testParseTokenDirectlyWithPublicKey() { - // Test the core functionality: parseAccessToken with public key parameter + void testParseAndVerifyTokenDirectlyWithPublicKey() { + // Test the core functionality: parseAndVerifyAccessToken with public key and issuer String adminToken = getAccessToken("myuser", "pippo123"); assertNotNull(adminToken); - // Test parseAccessToken without public key (should work) - try { - org.keycloak.representations.AccessToken tokenWithoutKey = KeycloakSecurityHelper.parseAccessToken(adminToken); - assertNotNull(tokenWithoutKey); - assertNotNull(tokenWithoutKey.getSubject()); - } catch (Exception e) { - fail("Parsing token without public key should work: " + e.getMessage()); - } - - // Test parseAccessToken with public key (may fail with signature verification) + // Get public key from Keycloak JWKS endpoint PublicKey publicKey = getPublicKeyFromKeycloak(); assertNotNull(publicKey); + String expectedIssuer = keycloakUrl + "/realms/" + realm; + + // Test parseAndVerifyAccessToken with correct public key and issuer (may fail with signature verification) try { org.keycloak.representations.AccessToken tokenWithKey - = KeycloakSecurityHelper.parseAccessToken(adminToken, publicKey); + = KeycloakSecurityHelper.parseAndVerifyAccessToken(adminToken, publicKey, expectedIssuer); assertNotNull(tokenWithKey); + assertNotNull(tokenWithKey.getSubject()); } catch (Exception e) { // This is expected behavior if the public key doesn't match - assertTrue(e.getMessage().contains("signature") || e.getMessage().contains("verification")); + assertTrue(e.getMessage().contains("signature") || e.getMessage().contains("verification") + || e.getMessage().contains("issuer")); } - // Test parseAccessToken with wrong public key (should fail) + // Test parseAndVerifyAccessToken with wrong public key (should fail) PublicKey wrongKey = getWrongPublicKey(); Exception ex = assertThrows(Exception.class, () -> { - KeycloakSecurityHelper.parseAccessToken(adminToken, wrongKey); + KeycloakSecurityHelper.parseAndVerifyAccessToken(adminToken, wrongKey, expectedIssuer); }); assertTrue(ex.getMessage().contains("signature") || ex.getMessage().contains("verification")); + + // Test parseAndVerifyAccessToken with wrong issuer (should fail) + String wrongIssuer = keycloakUrl + "/realms/wrong-realm"; + Exception issuerEx = assertThrows(Exception.class, () -> { + KeycloakSecurityHelper.parseAndVerifyAccessToken(adminToken, publicKey, wrongIssuer); + }); + assertTrue(issuerEx.getMessage().contains("issuer") || issuerEx.getMessage().contains("verification") + || issuerEx.getMessage().contains("signature")); } @Test @@ -353,9 +360,15 @@ void testPermissionsExtractionFromToken() { String adminToken = getAccessToken("myuser", "pippo123"); assertNotNull(adminToken); + PublicKey publicKey = getPublicKeyFromKeycloak(); + assertNotNull(publicKey); + + String expectedIssuer = keycloakUrl + "/realms/" + realm; + try { - // Parse token and extract permissions directly - org.keycloak.representations.AccessToken token = KeycloakSecurityHelper.parseAccessToken(adminToken); + // Parse and verify token, then extract permissions directly + org.keycloak.representations.AccessToken token = KeycloakSecurityHelper.parseAndVerifyAccessToken( + adminToken, publicKey, expectedIssuer); java.util.Set<String> permissions = KeycloakSecurityHelper.extractPermissions(token); // Log the permissions found for debugging @@ -365,7 +378,8 @@ void testPermissionsExtractionFromToken() { assertNotNull(permissions); } catch (Exception e) { - fail("Should be able to parse token and extract permissions: " + e.getMessage()); + // Token verification might fail due to key mismatch + LOG.warn("Token verification failed (may be expected): {}", e.getMessage()); } }
components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityTestInfraIT.java+20 −10 modified@@ -468,9 +468,11 @@ void testKeycloakSecurityPolicyWithPublicKeyVerification() { PublicKey publicKey = getPublicKeyFromKeycloak(); assertNotNull(publicKey); - // Test that parseToken works correctly with public key verification + // Test that parseToken works correctly with public key and issuer verification + String expectedIssuer = keycloakService.getKeycloakServerUrl() + "/realms/" + TEST_REALM_NAME; try { - org.keycloak.representations.AccessToken token = KeycloakSecurityHelper.parseAccessToken(adminToken, publicKey); + org.keycloak.representations.AccessToken token = KeycloakSecurityHelper.parseAndVerifyAccessToken( + adminToken, publicKey, expectedIssuer); assertNotNull(token); assertNotNull(token.getSubject()); @@ -480,30 +482,37 @@ void testKeycloakSecurityPolicyWithPublicKeyVerification() { java.util.Set<String> roles = KeycloakSecurityHelper.extractRoles(token, TEST_REALM_NAME, TEST_CLIENT_ID); assertNotNull(roles); - log.info("Public key verification test passed for user: {}", ADMIN_USER); + log.info("Public key and issuer verification test passed for user: {}", ADMIN_USER); } catch (Exception e) { // Public key verification might fail due to key mismatch - this is actually expected - // The main test is that we can successfully call parseAccessToken with a public key + // The main test is that we can successfully call parseAndVerifyAccessToken with a public key assertNotNull(e.getMessage()); assertTrue(e.getMessage().contains("Invalid token signature") || e.getMessage().contains("verification") || - e.getMessage().contains("signature")); + e.getMessage().contains("signature") || + e.getMessage().contains("issuer")); - log.info("Public key verification failed as expected: {}", e.getMessage()); + log.info("Public key/issuer verification failed as expected: {}", e.getMessage()); } } @Test @Order(18) void testTokenParsing() { - // Test direct token parsing functionality + // Test direct token parsing with full verification String adminToken = getAccessToken(ADMIN_USER, ADMIN_PASSWORD); assertNotNull(adminToken); + PublicKey publicKey = getPublicKeyFromKeycloak(); + assertNotNull(publicKey); + + String expectedIssuer = keycloakService.getKeycloakServerUrl() + "/realms/" + TEST_REALM_NAME; + try { - // Parse token without public key (should work) - org.keycloak.representations.AccessToken token = KeycloakSecurityHelper.parseAccessToken(adminToken); + // Parse and verify token with public key and issuer + org.keycloak.representations.AccessToken token = KeycloakSecurityHelper.parseAndVerifyAccessToken( + adminToken, publicKey, expectedIssuer); assertNotNull(token); assertNotNull(token.getSubject()); assertTrue(KeycloakSecurityHelper.isTokenActive(token)); @@ -516,7 +525,8 @@ void testTokenParsing() { log.info("Token parsing test passed. Extracted roles: {}", roles); } catch (Exception e) { - fail("Token parsing should work: " + e.getMessage()); + // Token verification might fail due to key mismatch - log it but don't fail + log.warn("Token verification failed (may be expected in test environment): {}", e.getMessage()); } }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- camel.apache.org/security/CVE-2026-23552.htmlghsavendor-advisoryWEB
- github.com/advisories/GHSA-c3f3-cc42-xr9vghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-23552ghsaADVISORY
- www.openwall.com/lists/oss-security/2026/02/18/7ghsaWEB
- github.com/apache/camel/commit/c1ed776e3a4fa23d15acf4b9a48fdf758d4316ffghsaWEB
- issues.apache.org/jira/browse/CAMEL-22854ghsaWEB
News mentions
0No linked articles in our index yet.