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

acosentino pushed a commit to branch CAMEL-22724
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 8a14b037bdbdbb8ce393b54145caa287c22940ba
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Nov 25 15:08:39 2025 +0100

    CAMEL-22724 - Camel-Keycloak: Enhance component with configurable token 
source priority and binding validation
    
    Signed-off-by: Andrea Cosentino <[email protected]>
---
 .../security/KeycloakSecurityConstants.java        |  15 +
 .../keycloak/security/KeycloakSecurityPolicy.java  |  40 ++
 .../security/KeycloakSecurityProcessor.java        | 145 ++++++-
 .../security/KeycloakTokenBindingSecurityIT.java   | 450 +++++++++++++++++++++
 4 files changed, 639 insertions(+), 11 deletions(-)

diff --git 
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityConstants.java
 
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityConstants.java
index adf70f6da432..09bb78763fc8 100644
--- 
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityConstants.java
+++ 
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityConstants.java
@@ -27,6 +27,21 @@ public final class KeycloakSecurityConstants {
     public static final String USER_ROLES_HEADER = "CamelKeycloakUserRoles";
     public static final String USER_ROLES_PROPERTY = "CamelKeycloakUserRoles";
 
+    /**
+     * Session ID property for binding tokens to specific sessions (prevents 
session fixation)
+     */
+    public static final String SESSION_ID_PROPERTY = "CamelKeycloakSessionId";
+
+    /**
+     * Token thumbprint property for validating token integrity (SHA-256 hash)
+     */
+    public static final String TOKEN_THUMBPRINT_PROPERTY = 
"CamelKeycloakTokenThumbprint";
+
+    /**
+     * Token subject (user ID) for binding validation
+     */
+    public static final String TOKEN_SUBJECT_PROPERTY = 
"CamelKeycloakTokenSubject";
+
     private KeycloakSecurityConstants() {
         // Utility class
     }
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 6b31f3c4b287..5d199c4d0a0d 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
@@ -66,6 +66,22 @@ public class KeycloakSecurityPolicy implements 
AuthorizationPolicy {
      * Time-to-live for cached introspection results in seconds. Default is 60 
seconds.
      */
     private long introspectionCacheTtl = 60;
+    /**
+     * Enable validation that tokens are bound to specific sessions. When 
enabled, tokens from headers must match the
+     * session-bound token to prevent session fixation attacks. Default is 
true for security.
+     */
+    private boolean validateTokenBinding = true;
+    /**
+     * Allow token retrieval from HTTP headers. When disabled, tokens can only 
come from exchange properties. Disabling
+     * this prevents attackers from injecting tokens via headers. Default is 
true for backward compatibility, but should
+     * be set to false in production environments where tokens are set 
programmatically.
+     */
+    private boolean allowTokenFromHeader = true;
+    /**
+     * Prefer tokens from exchange properties over headers. When true, if a 
token exists in both property and header,
+     * the property value is used. This prevents header-based token override 
attacks. Default is true for security.
+     */
+    private boolean preferPropertyOverHeader = true;
 
     private Keycloak keycloakClient;
     private KeycloakTokenIntrospector tokenIntrospector;
@@ -333,4 +349,28 @@ public class KeycloakSecurityPolicy implements 
AuthorizationPolicy {
     public KeycloakTokenIntrospector getTokenIntrospector() {
         return tokenIntrospector;
     }
+
+    public boolean isValidateTokenBinding() {
+        return validateTokenBinding;
+    }
+
+    public void setValidateTokenBinding(boolean validateTokenBinding) {
+        this.validateTokenBinding = validateTokenBinding;
+    }
+
+    public boolean isAllowTokenFromHeader() {
+        return allowTokenFromHeader;
+    }
+
+    public void setAllowTokenFromHeader(boolean allowTokenFromHeader) {
+        this.allowTokenFromHeader = allowTokenFromHeader;
+    }
+
+    public boolean isPreferPropertyOverHeader() {
+        return preferPropertyOverHeader;
+    }
+
+    public void setPreferPropertyOverHeader(boolean preferPropertyOverHeader) {
+        this.preferPropertyOverHeader = preferPropertyOverHeader;
+    }
 }
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 1787ee6746be..9faec6822884 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
@@ -16,6 +16,10 @@
  */
 package org.apache.camel.component.keycloak.security;
 
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
 import java.util.Set;
 
 import org.apache.camel.CamelAuthorizationException;
@@ -69,26 +73,145 @@ public class KeycloakSecurityProcessor extends 
DelegateProcessor {
         }
     }
 
-    private String getAccessToken(Exchange exchange) {
-        // Try to get token from header first
-        String token = 
exchange.getIn().getHeader(KeycloakSecurityConstants.ACCESS_TOKEN_HEADER, 
String.class);
+    private String getAccessToken(Exchange exchange) throws Exception {
+        // Get token from exchange property (application-controlled, TRUSTED)
+        String propertyToken = 
exchange.getProperty(KeycloakSecurityConstants.ACCESS_TOKEN_PROPERTY, 
String.class);
+        String headerToken = null;
 
-        if (token == null) {
-            // Try to get from Authorization header
-            String authHeader = exchange.getIn().getHeader("Authorization", 
String.class);
-            if (authHeader != null && authHeader.startsWith("Bearer ")) {
-                token = authHeader.substring(7);
+        // Get token from headers only if allowed by policy
+        if (policy.isAllowTokenFromHeader()) {
+            headerToken = 
exchange.getIn().getHeader(KeycloakSecurityConstants.ACCESS_TOKEN_HEADER, 
String.class);
+
+            if (headerToken == null) {
+                // Try to get from Authorization header
+                String authHeader = 
exchange.getIn().getHeader("Authorization", String.class);
+                if (authHeader != null && authHeader.startsWith("Bearer ")) {
+                    headerToken = authHeader.substring(7);
+                }
+            }
+        }
+
+        // Determine which token to use based on policy
+        String token;
+        boolean isHeaderSource = false;
+
+        if (policy.isPreferPropertyOverHeader()) {
+            // SECURE DEFAULT: Property (trusted) takes precedence over header 
(untrusted)
+            if (propertyToken != null) {
+                token = propertyToken;
+                LOG.debug("Using token from exchange property (preferred 
source)");
+            } else if (headerToken != null) {
+                token = headerToken;
+                isHeaderSource = true;
+                LOG.warn("Using token from HTTP header - this may be a 
security risk. "
+                         + "Consider setting tokens via exchange properties 
instead to prevent token injection attacks.");
+            } else {
+                token = null;
+            }
+        } else {
+            // LEGACY MODE (LESS SECURE): Header takes precedence - maintains 
backward compatibility
+            if (headerToken != null) {
+                token = headerToken;
+                isHeaderSource = true;
+                LOG.warn("Token from header takes precedence over property - 
this may allow token override attacks. "
+                         + "Consider setting preferPropertyOverHeader=true for 
better security.");
+            } else if (propertyToken != null) {
+                token = propertyToken;
+                LOG.debug("Using token from exchange property");
+            } else {
+                token = null;
             }
         }
 
-        if (token == null) {
-            // Try to get from exchange property
-            token = 
exchange.getProperty(KeycloakSecurityConstants.ACCESS_TOKEN_PROPERTY, 
String.class);
+        // Validate token binding if enabled and token came from headers
+        if (token != null && policy.isValidateTokenBinding() && 
isHeaderSource) {
+            validateTokenBinding(exchange, token, propertyToken);
         }
 
         return token;
     }
 
+    /**
+     * Validates that a token from headers matches the session-bound token to 
prevent session fixation attacks. This
+     * method performs three levels of validation: 1. Token exact match - 
header token must match property token if both
+     * exist 2. Subject validation - token subject (user ID) must match stored 
subject 3. Thumbprint validation - token
+     * integrity check via SHA-256 hash
+     *
+     * @param  exchange                    the current exchange
+     * @param  headerToken                 token retrieved from HTTP headers
+     * @param  propertyToken               token stored in exchange properties 
(session-bound)
+     * @throws CamelAuthorizationException if token binding validation fails
+     */
+    private void validateTokenBinding(Exchange exchange, String headerToken, 
String propertyToken)
+            throws Exception {
+
+        // Level 1: Exact token match validation
+        if (propertyToken != null && !propertyToken.equals(headerToken)) {
+            LOG.error("SECURITY: Token binding validation failed - header 
token does not match session-bound property token");
+            throw new CamelAuthorizationException(
+                    "Token mismatch detected - possible session fixation or 
token injection attack", exchange);
+        }
+
+        // Level 2: Subject (user ID) validation
+        String storedSubject = 
exchange.getProperty(KeycloakSecurityConstants.TOKEN_SUBJECT_PROPERTY, 
String.class);
+        if (storedSubject != null) {
+            try {
+                // Parse token to extract subject (without full validation - 
just for binding check)
+                AccessToken accessToken = 
KeycloakSecurityHelper.parseAccessToken(headerToken);
+                String currentSubject = accessToken.getSubject();
+
+                if (!storedSubject.equals(currentSubject)) {
+                    LOG.error("SECURITY: Token subject mismatch - expected 
user '{}' but token is for user '{}'",
+                            storedSubject, currentSubject);
+                    throw new CamelAuthorizationException(
+                            "Token subject mismatch - token does not belong to 
current session user", exchange);
+                }
+
+                LOG.debug("Token subject validation successful - subject '{}' 
matches session", storedSubject);
+            } catch (Exception e) {
+                if (e instanceof CamelAuthorizationException) {
+                    throw e;
+                }
+                LOG.error("SECURITY: Failed to validate token subject 
binding", e);
+                throw new CamelAuthorizationException(
+                        "Token binding validation failed - unable to parse 
token", exchange,
+                        e);
+            }
+        }
+
+        // Level 3: Token thumbprint (integrity) validation
+        String storedThumbprint
+                = 
exchange.getProperty(KeycloakSecurityConstants.TOKEN_THUMBPRINT_PROPERTY, 
String.class);
+        if (storedThumbprint != null) {
+            String currentThumbprint = calculateTokenThumbprint(headerToken);
+            if (!storedThumbprint.equals(currentThumbprint)) {
+                LOG.error(
+                        "SECURITY: Token thumbprint mismatch - token has been 
tampered with, replaced, or belongs to different session");
+                throw new CamelAuthorizationException(
+                        "Token integrity check failed - possible token 
tampering or replacement attack", exchange);
+            }
+            LOG.debug("Token thumbprint validation successful - token 
integrity verified");
+        }
+    }
+
+    /**
+     * Calculates a SHA-256 thumbprint of the token for integrity validation. 
This provides a cryptographic fingerprint
+     * that can detect if a token has been tampered with or replaced.
+     *
+     * @param  token                 the access token string
+     * @return                       Base64-encoded SHA-256 hash of the token
+     * @throws IllegalStateException if SHA-256 algorithm is not available 
(should never happen)
+     */
+    private String calculateTokenThumbprint(String token) {
+        try {
+            MessageDigest digest = MessageDigest.getInstance("SHA-256");
+            byte[] hash = 
digest.digest(token.getBytes(StandardCharsets.UTF_8));
+            return Base64.getEncoder().encodeToString(hash);
+        } catch (NoSuchAlgorithmException e) {
+            throw new IllegalStateException("SHA-256 algorithm not available - 
this should never happen", e);
+        }
+    }
+
     private void validateRoles(String accessToken, Exchange exchange) throws 
Exception {
         try {
             Set<String> userRoles;
diff --git 
a/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakTokenBindingSecurityIT.java
 
b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakTokenBindingSecurityIT.java
new file mode 100644
index 000000000000..2ad15e060cff
--- /dev/null
+++ 
b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakTokenBindingSecurityIT.java
@@ -0,0 +1,450 @@
+/*
+ * 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.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Form;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+
+import org.apache.camel.CamelAuthorizationException;
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.infra.keycloak.services.KeycloakService;
+import org.apache.camel.test.infra.keycloak.services.KeycloakServiceFactory;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.admin.client.KeycloakBuilder;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.admin.client.resource.UserResource;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Integration test for Keycloak token binding security features.
+ * <p>
+ * This test validates security enhancements that prevent token injection 
attacks: - Token source priority (property vs
+ * header) - Token binding validation - Session fixation prevention - Attack 
scenario blocking
+ */
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class KeycloakTokenBindingSecurityIT extends CamelTestSupport {
+
+    private static final Logger log = 
LoggerFactory.getLogger(KeycloakTokenBindingSecurityIT.class);
+
+    @RegisterExtension
+    static KeycloakService keycloakService = 
KeycloakServiceFactory.createService();
+
+    // Test data - use unique names
+    private static final String TEST_REALM_NAME = "token-binding-realm-" + 
UUID.randomUUID().toString().substring(0, 8);
+    private static final String TEST_CLIENT_ID = "token-binding-client-" + 
UUID.randomUUID().toString().substring(0, 8);
+    private static String TEST_CLIENT_SECRET = null;
+
+    // Test users
+    private static final String ADMIN_USER = "admin-" + 
UUID.randomUUID().toString().substring(0, 8);
+    private static final String ADMIN_PASSWORD = "admin123";
+    private static final String NORMAL_USER = "user-" + 
UUID.randomUUID().toString().substring(0, 8);
+    private static final String NORMAL_PASSWORD = "user123";
+    private static final String ATTACKER_USER = "attacker-" + 
UUID.randomUUID().toString().substring(0, 8);
+    private static final String ATTACKER_PASSWORD = "attacker123";
+
+    // Test roles
+    private static final String ADMIN_ROLE = "admin";
+    private static final String USER_ROLE = "user";
+
+    private static Keycloak keycloakAdminClient;
+    private static RealmResource realmResource;
+
+    @BeforeAll
+    static void setupKeycloakRealm() {
+        log.info("Setting up Keycloak realm for token binding tests");
+
+        keycloakAdminClient = KeycloakBuilder.builder()
+                .serverUrl(keycloakService.getKeycloakServerUrl())
+                .realm(keycloakService.getKeycloakRealm())
+                .username(keycloakService.getKeycloakUsername())
+                .password(keycloakService.getKeycloakPassword())
+                .clientId("admin-cli")
+                .build();
+
+        createTestRealm();
+        realmResource = keycloakAdminClient.realm(TEST_REALM_NAME);
+        createTestClient();
+        createTestRoles();
+        createTestUsers();
+        assignRolesToUsers();
+
+        log.info("Keycloak realm setup completed: {}", TEST_REALM_NAME);
+    }
+
+    @AfterAll
+    static void cleanupKeycloakRealm() {
+        if (keycloakAdminClient != null) {
+            try {
+                keycloakAdminClient.realm(TEST_REALM_NAME).remove();
+                log.info("Deleted test realm: {}", TEST_REALM_NAME);
+            } catch (Exception e) {
+                log.warn("Failed to cleanup test realm: {}", e.getMessage());
+            } finally {
+                keycloakAdminClient.close();
+            }
+        }
+    }
+
+    private static void createTestRealm() {
+        RealmRepresentation realm = new RealmRepresentation();
+        realm.setId(TEST_REALM_NAME);
+        realm.setRealm(TEST_REALM_NAME);
+        realm.setDisplayName("Token Binding Security Test Realm");
+        realm.setEnabled(true);
+        realm.setAccessTokenLifespan(3600);
+        keycloakAdminClient.realms().create(realm);
+    }
+
+    private static void createTestClient() {
+        ClientRepresentation client = new ClientRepresentation();
+        client.setClientId(TEST_CLIENT_ID);
+        client.setName("Token Binding Test Client");
+        client.setDescription("Client for token binding security testing");
+        client.setEnabled(true);
+        client.setPublicClient(false);
+        client.setDirectAccessGrantsEnabled(true);
+        client.setStandardFlowEnabled(true);
+        client.setFullScopeAllowed(true);
+
+        Response response = realmResource.clients().create(client);
+        if (response.getStatus() == 201) {
+            String clientId
+                    = 
response.getHeaderString("Location").substring(response.getHeaderString("Location").lastIndexOf('/')
 + 1);
+            ClientResource clientResource = 
realmResource.clients().get(clientId);
+            TEST_CLIENT_SECRET = clientResource.getSecret().getValue();
+            log.info("Created test client: {} with secret", TEST_CLIENT_ID);
+        } else {
+            throw new RuntimeException("Failed to create client. Status: " + 
response.getStatus());
+        }
+        response.close();
+    }
+
+    private static void createTestRoles() {
+        RoleRepresentation adminRole = new RoleRepresentation();
+        adminRole.setName(ADMIN_ROLE);
+        realmResource.roles().create(adminRole);
+
+        RoleRepresentation userRole = new RoleRepresentation();
+        userRole.setName(USER_ROLE);
+        realmResource.roles().create(userRole);
+    }
+
+    private static void createTestUsers() {
+        createUser(ADMIN_USER, ADMIN_PASSWORD, "Admin", "User", ADMIN_USER + 
"@test.com");
+        createUser(NORMAL_USER, NORMAL_PASSWORD, "Normal", "User", NORMAL_USER 
+ "@test.com");
+        createUser(ATTACKER_USER, ATTACKER_PASSWORD, "Attacker", "User", 
ATTACKER_USER + "@test.com");
+    }
+
+    private static void createUser(String username, String password, String 
firstName, String lastName, String email) {
+        UserRepresentation user = new UserRepresentation();
+        user.setUsername(username);
+        user.setFirstName(firstName);
+        user.setLastName(lastName);
+        user.setEmail(email);
+        user.setEnabled(true);
+        user.setEmailVerified(true);
+
+        Response response = realmResource.users().create(user);
+        if (response.getStatus() == 201) {
+            String userId
+                    = 
response.getHeaderString("Location").substring(response.getHeaderString("Location").lastIndexOf('/')
 + 1);
+            UserResource userResource = realmResource.users().get(userId);
+
+            CredentialRepresentation credential = new 
CredentialRepresentation();
+            credential.setType(CredentialRepresentation.PASSWORD);
+            credential.setValue(password);
+            credential.setTemporary(false);
+            userResource.resetPassword(credential);
+            log.info("Created user: {} with password", username);
+        } else {
+            throw new RuntimeException("Failed to create user: " + username + 
". Status: " + response.getStatus());
+        }
+        response.close();
+    }
+
+    private static void assignRolesToUsers() {
+        assignRoleToUser(ADMIN_USER, ADMIN_ROLE);
+        assignRoleToUser(NORMAL_USER, USER_ROLE);
+        assignRoleToUser(ATTACKER_USER, USER_ROLE);
+    }
+
+    private static void assignRoleToUser(String username, String roleName) {
+        List<UserRepresentation> users = 
realmResource.users().search(username);
+        if (!users.isEmpty()) {
+            UserResource userResource = 
realmResource.users().get(users.get(0).getId());
+            RoleRepresentation role = 
realmResource.roles().get(roleName).toRepresentation();
+            userResource.roles().realmLevel().add(Arrays.asList(role));
+        }
+    }
+
+    @Override
+    protected RoutesBuilder createRouteBuilder() throws Exception {
+        return new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                // Route 1: Secure default - property preferred over header
+                KeycloakSecurityPolicy securePolicy = new 
KeycloakSecurityPolicy();
+                
securePolicy.setServerUrl(keycloakService.getKeycloakServerUrl());
+                securePolicy.setRealm(TEST_REALM_NAME);
+                securePolicy.setClientId(TEST_CLIENT_ID);
+                securePolicy.setClientSecret(TEST_CLIENT_SECRET);
+                securePolicy.setPreferPropertyOverHeader(true);
+                securePolicy.setAllowTokenFromHeader(true);
+                securePolicy.setValidateTokenBinding(true);
+
+                from("direct:secure-default")
+                        .policy(securePolicy)
+                        .transform().constant("Access granted - secure 
default");
+
+                // Route 2: Maximum security - headers disabled
+                KeycloakSecurityPolicy maxSecurityPolicy = new 
KeycloakSecurityPolicy();
+                
maxSecurityPolicy.setServerUrl(keycloakService.getKeycloakServerUrl());
+                maxSecurityPolicy.setRealm(TEST_REALM_NAME);
+                maxSecurityPolicy.setClientId(TEST_CLIENT_ID);
+                maxSecurityPolicy.setClientSecret(TEST_CLIENT_SECRET);
+                maxSecurityPolicy.setAllowTokenFromHeader(false);
+
+                from("direct:max-security")
+                        .policy(maxSecurityPolicy)
+                        .transform().constant("Access granted - max security");
+
+                // Route 3: Admin-only route
+                KeycloakSecurityPolicy adminPolicy = new 
KeycloakSecurityPolicy();
+                
adminPolicy.setServerUrl(keycloakService.getKeycloakServerUrl());
+                adminPolicy.setRealm(TEST_REALM_NAME);
+                adminPolicy.setClientId(TEST_CLIENT_ID);
+                adminPolicy.setClientSecret(TEST_CLIENT_SECRET);
+                adminPolicy.setRequiredRoles(Arrays.asList(ADMIN_ROLE));
+                adminPolicy.setPreferPropertyOverHeader(true);
+
+                from("direct:admin-only")
+                        .policy(adminPolicy)
+                        .transform().constant("Admin access granted");
+            }
+        };
+    }
+
+    @Test
+    @Order(1)
+    void testKeycloakServiceConfiguration() {
+        assertNotNull(keycloakService.getKeycloakServerUrl());
+        assertNotNull(TEST_CLIENT_SECRET);
+    }
+
+    @Test
+    @Order(10)
+    void testPropertyTokenWorks() {
+        String normalToken = getAccessToken(NORMAL_USER, NORMAL_PASSWORD);
+
+        String result = template.send("direct:secure-default", exchange -> {
+            
exchange.setProperty(KeycloakSecurityConstants.ACCESS_TOKEN_PROPERTY, 
normalToken);
+            exchange.getIn().setBody("test");
+        }).getMessage().getBody(String.class);
+
+        assertEquals("Access granted - secure default", result);
+        log.info("✓ Property token works");
+    }
+
+    @Test
+    @Order(11)
+    void testHeaderTokenWorks() {
+        String normalToken = getAccessToken(NORMAL_USER, NORMAL_PASSWORD);
+
+        String result = template.requestBodyAndHeader("direct:secure-default", 
"test",
+                KeycloakSecurityConstants.ACCESS_TOKEN_HEADER, normalToken, 
String.class);
+
+        assertEquals("Access granted - secure default", result);
+        log.info("✓ Header token works");
+    }
+
+    @Test
+    @Order(12)
+    void testPropertyPreferredOverHeader() {
+        String normalToken = getAccessToken(NORMAL_USER, NORMAL_PASSWORD);
+        String attackerToken = getAccessToken(ATTACKER_USER, 
ATTACKER_PASSWORD);
+
+        String result = template.send("direct:secure-default", exchange -> {
+            
exchange.setProperty(KeycloakSecurityConstants.ACCESS_TOKEN_PROPERTY, 
normalToken);
+            
exchange.getIn().setHeader(KeycloakSecurityConstants.ACCESS_TOKEN_HEADER, 
attackerToken);
+            exchange.getIn().setBody("test");
+        }).getMessage().getBody(String.class);
+
+        assertEquals("Access granted - secure default", result);
+        log.info("✓ Property token preferred over header token");
+    }
+
+    @Test
+    @Order(13)
+    void testInvalidHeaderIgnoredWhenPropertyValid() {
+        String normalToken = getAccessToken(NORMAL_USER, NORMAL_PASSWORD);
+
+        String result = template.send("direct:secure-default", exchange -> {
+            
exchange.setProperty(KeycloakSecurityConstants.ACCESS_TOKEN_PROPERTY, 
normalToken);
+            
exchange.getIn().setHeader(KeycloakSecurityConstants.ACCESS_TOKEN_HEADER, 
"invalid.token");
+            exchange.getIn().setBody("test");
+        }).getMessage().getBody(String.class);
+
+        assertEquals("Access granted - secure default", result);
+        log.info("✓ Invalid header ignored when property valid");
+    }
+
+    @Test
+    @Order(20)
+    void testHeaderRejectedWhenHeadersDisabled() {
+        String normalToken = getAccessToken(NORMAL_USER, NORMAL_PASSWORD);
+
+        CamelExecutionException ex = 
assertThrows(CamelExecutionException.class, () -> {
+            template.requestBodyAndHeader("direct:max-security", "test",
+                    KeycloakSecurityConstants.ACCESS_TOKEN_HEADER, 
normalToken, String.class);
+        });
+
+        assertTrue(ex.getCause() instanceof CamelAuthorizationException);
+        log.info("✓ Header correctly rejected when headers disabled");
+    }
+
+    @Test
+    @Order(21)
+    void testPropertyWorksWhenHeadersDisabled() {
+        String normalToken = getAccessToken(NORMAL_USER, NORMAL_PASSWORD);
+
+        String result = template.send("direct:max-security", exchange -> {
+            
exchange.setProperty(KeycloakSecurityConstants.ACCESS_TOKEN_PROPERTY, 
normalToken);
+            exchange.getIn().setBody("test");
+        }).getMessage().getBody(String.class);
+
+        assertEquals("Access granted - max security", result);
+        log.info("✓ Property works when headers disabled");
+    }
+
+    @Test
+    @Order(30)
+    void testPropertyTokenUsedNotHeader() {
+        String normalToken = getAccessToken(NORMAL_USER, NORMAL_PASSWORD);
+        String adminToken = getAccessToken(ADMIN_USER, ADMIN_PASSWORD);
+
+        String result = template.send("direct:secure-default", exchange -> {
+            
exchange.setProperty(KeycloakSecurityConstants.ACCESS_TOKEN_PROPERTY, 
normalToken);
+            
exchange.getIn().setHeader(KeycloakSecurityConstants.ACCESS_TOKEN_HEADER, 
adminToken);
+            exchange.getIn().setBody("test");
+        }).getMessage().getBody(String.class);
+
+        assertEquals("Access granted - secure default", result);
+        log.info("✓ Property token preferred - attack vector mitigated");
+    }
+
+    @Test
+    @Order(31)
+    void testAttackScenario_SessionHijacking() {
+        String victimToken = getAccessToken(NORMAL_USER, NORMAL_PASSWORD);
+        String attackerToken = getAccessToken(ATTACKER_USER, 
ATTACKER_PASSWORD);
+
+        String result = template.send("direct:secure-default", exchange -> {
+            
exchange.setProperty(KeycloakSecurityConstants.ACCESS_TOKEN_PROPERTY, 
victimToken);
+            
exchange.getIn().setHeader(KeycloakSecurityConstants.ACCESS_TOKEN_HEADER, 
attackerToken);
+            exchange.getIn().setBody("hijack attempt");
+        }).getMessage().getBody(String.class);
+
+        assertEquals("Access granted - secure default", result);
+        log.info("✓ BLOCKED: Session hijacking prevented");
+    }
+
+    @Test
+    @Order(32)
+    void testAuthorizationHeaderFormat() {
+        String normalToken = getAccessToken(NORMAL_USER, NORMAL_PASSWORD);
+
+        String result = template.requestBodyAndHeader("direct:secure-default", 
"test",
+                "Authorization", "Bearer " + normalToken, String.class);
+
+        assertEquals("Access granted - secure default", result);
+        log.info("✓ Authorization Bearer header works");
+    }
+
+    @Test
+    @Order(33)
+    void testNoTokenRejected() {
+        CamelExecutionException ex = 
assertThrows(CamelExecutionException.class, () -> {
+            template.sendBody("direct:secure-default", "test");
+        });
+
+        assertTrue(ex.getCause() instanceof CamelAuthorizationException);
+        assertTrue(ex.getCause().getMessage().contains("Access token not 
found"));
+        log.info("✓ Request without token correctly rejected");
+    }
+
+    private String getAccessToken(String username, String password) {
+        try (Client client = ClientBuilder.newClient()) {
+            String tokenUrl = keycloakService.getKeycloakServerUrl() + 
"/realms/" + TEST_REALM_NAME
+                              + "/protocol/openid-connect/token";
+
+            Form form = new Form()
+                    .param("grant_type", "password")
+                    .param("client_id", TEST_CLIENT_ID)
+                    .param("client_secret", TEST_CLIENT_SECRET)
+                    .param("username", username)
+                    .param("password", password);
+
+            try (Response response = client.target(tokenUrl)
+                    .request(MediaType.APPLICATION_JSON)
+                    .post(Entity.entity(form, 
MediaType.APPLICATION_FORM_URLENCODED))) {
+
+                if (response.getStatus() == 200) {
+                    @SuppressWarnings("unchecked")
+                    Map<String, Object> tokenResponse = 
response.readEntity(Map.class);
+                    return (String) tokenResponse.get("access_token");
+                } else {
+                    String errorBody = response.readEntity(String.class);
+                    log.error("Failed to obtain token for user {}. Status: {}, 
Response: {}", username,
+                            response.getStatus(), errorBody);
+                    throw new RuntimeException(
+                            "Failed to obtain access token for " + username + 
". Status: " + response.getStatus() + ", Error: "
+                                               + errorBody);
+                }
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("Error obtaining access token for " + 
username, e);
+        }
+    }
+}

Reply via email to