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

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


The following commit(s) were added to refs/heads/main by this push:
     new f07b813a21 camel-keycloak - add test and native support for camel 4.17 
features in CEQ
f07b813a21 is described below

commit f07b813a21fdbff5633cec8b57e1b8d2295a1734
Author: JinyuChen97 <[email protected]>
AuthorDate: Fri Feb 27 13:00:22 2026 +0000

    camel-keycloak - add test and native support for camel 4.17 features in CEQ
    
    Fixes #8091
---
 extensions/keycloak/deployment/pom.xml             |   4 +
 .../keycloak/deployment/KeycloakProcessor.java     |  43 ++
 extensions/keycloak/runtime/pom.xml                |   4 +
 integration-tests/keycloak/pom.xml                 |  17 +
 .../it/KeycloakEvaluatePermissionResource.java     | 195 ++++++++++
 .../keycloak/it/KeycloakRouteBuilder.java          | 150 +++++++
 .../it/KeycloakSecurityPolicyResource.java         | 337 ++++++++++++++++
 .../keycloak/it/KeycloakUserResource.java          | 116 ++++++
 .../src/main/resources/application.properties      |   2 +
 .../it/KeycloakEvaluatePermissionTest.java         | 331 ++++++++++++++++
 .../it/KeycloakEvaluatePermissionTestIT.java       |  23 +-
 .../component/keycloak/it/KeycloakRoleTest.java    |  87 ++++-
 .../keycloak/it/KeycloakSecurityPolicyIT.java      |  24 +-
 .../keycloak/it/KeycloakSecurityPolicyTest.java    | 432 +++++++++++++++++++++
 .../it/KeycloakSecurityPolicyTestBase.java         |  36 ++
 .../component/keycloak/it/KeycloakTestBase.java    |  42 ++
 .../keycloak/it/KeycloakTestResource.java          |   4 +
 .../component/keycloak/it/KeycloakUserTest.java    | 164 +++++++-
 18 files changed, 1970 insertions(+), 41 deletions(-)

diff --git a/extensions/keycloak/deployment/pom.xml 
b/extensions/keycloak/deployment/pom.xml
index 23115671ef..58cc3e02d9 100644
--- a/extensions/keycloak/deployment/pom.xml
+++ b/extensions/keycloak/deployment/pom.xml
@@ -47,6 +47,10 @@
             <groupId>io.quarkus</groupId>
             
<artifactId>quarkus-keycloak-admin-resteasy-client-deployment</artifactId>
         </dependency>
+        <dependency>
+            <groupId>io.quarkus</groupId>
+            <artifactId>quarkus-caffeine-deployment</artifactId>
+        </dependency>
     </dependencies>
 
     <build>
diff --git 
a/extensions/keycloak/deployment/src/main/java/org/apache/camel/quarkus/component/keycloak/deployment/KeycloakProcessor.java
 
b/extensions/keycloak/deployment/src/main/java/org/apache/camel/quarkus/component/keycloak/deployment/KeycloakProcessor.java
index 85530325c0..8d5fb696a4 100644
--- 
a/extensions/keycloak/deployment/src/main/java/org/apache/camel/quarkus/component/keycloak/deployment/KeycloakProcessor.java
+++ 
b/extensions/keycloak/deployment/src/main/java/org/apache/camel/quarkus/component/keycloak/deployment/KeycloakProcessor.java
@@ -16,15 +16,28 @@
  */
 package org.apache.camel.quarkus.component.keycloak.deployment;
 
+import java.util.function.BooleanSupplier;
+
 import io.quarkus.deployment.annotations.BuildProducer;
 import io.quarkus.deployment.annotations.BuildStep;
 import io.quarkus.deployment.builditem.FeatureBuildItem;
+import 
io.quarkus.deployment.builditem.nativeimage.NativeImageSystemPropertyBuildItem;
+import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
 import 
io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem;
+import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem;
+import org.apache.camel.quarkus.core.deployment.util.CamelSupport;
+import org.keycloak.common.crypto.CryptoIntegration;
 import org.keycloak.common.util.BouncyIntegration;
 
+import static 
io.quarkus.caffeine.runtime.graal.CacheConstructorsFeature.REGISTER_RECORD_STATS_IMPLEMENTATIONS;
+
 class KeycloakProcessor {
 
     private static final String FEATURE = "camel-keycloak";
+    private static final String[] SERVICE_PROVIDER_SPIS = {
+            "org.keycloak.common.crypto.CryptoProvider",
+            
"org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider"
+    };
 
     @BuildStep
     FeatureBuildItem feature() {
@@ -34,6 +47,36 @@ class KeycloakProcessor {
     @BuildStep
     void 
runtimeInitializedClasses(BuildProducer<RuntimeInitializedClassBuildItem> 
runtimeInitializedClass) {
         runtimeInitializedClass.produce(new 
RuntimeInitializedClassBuildItem(BouncyIntegration.class.getName()));
+        runtimeInitializedClass.produce(new 
RuntimeInitializedClassBuildItem(CryptoIntegration.class.getName()));
+    }
+
+    @BuildStep
+    void registerServiceProviders(BuildProducer<ServiceProviderBuildItem> 
serviceProvider) {
+        for (String spi : SERVICE_PROVIDER_SPIS) {
+            
serviceProvider.produce(ServiceProviderBuildItem.allProvidersFromClassPath(spi));
+        }
+    }
+
+    @BuildStep
+    void registerForReflection(BuildProducer<ReflectiveClassBuildItem> 
reflectiveClass) {
+        reflectiveClass.produce(ReflectiveClassBuildItem.builder(
+                org.keycloak.jose.jws.JWSHeader.class,
+                org.keycloak.jose.jws.JWSInput.class,
+                
org.keycloak.authorization.client.representation.ServerConfiguration.class)
+                .methods().fields().build());
+    }
+
+    @BuildStep(onlyIf = CamelCaffeineStatsEnabled.class)
+    NativeImageSystemPropertyBuildItem registerRecordStatsImplementations() {
+        return new 
NativeImageSystemPropertyBuildItem(REGISTER_RECORD_STATS_IMPLEMENTATIONS, 
"true");
+    }
+
+    static final class CamelCaffeineStatsEnabled implements BooleanSupplier {
+        @Override
+        public boolean getAsBoolean() {
+            return 
CamelSupport.getOptionalConfigValue("camel.component.caffeine-cache.stats-enabled",
 boolean.class, false) ||
+                    
CamelSupport.getOptionalConfigValue("camel.component.caffeine-cache.statsEnabled",
 boolean.class, false);
+        }
     }
 
 }
diff --git a/extensions/keycloak/runtime/pom.xml 
b/extensions/keycloak/runtime/pom.xml
index e5ed3c2e1e..86731f5392 100644
--- a/extensions/keycloak/runtime/pom.xml
+++ b/extensions/keycloak/runtime/pom.xml
@@ -54,6 +54,10 @@
             <groupId>io.quarkus</groupId>
             <artifactId>quarkus-keycloak-admin-resteasy-client</artifactId>
         </dependency>
+        <dependency>
+            <groupId>io.quarkus</groupId>
+            <artifactId>quarkus-caffeine</artifactId>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/integration-tests/keycloak/pom.xml 
b/integration-tests/keycloak/pom.xml
index 93f628abe3..366f1fa07c 100644
--- a/integration-tests/keycloak/pom.xml
+++ b/integration-tests/keycloak/pom.xml
@@ -35,6 +35,10 @@
             <groupId>org.apache.camel.quarkus</groupId>
             <artifactId>camel-quarkus-keycloak</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.camel.quarkus</groupId>
+            <artifactId>camel-quarkus-direct</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.apache.camel.quarkus</groupId>
             <artifactId>camel-quarkus-mock</artifactId>
@@ -98,6 +102,19 @@
             </activation>
             <dependencies>
                 <!-- The following dependencies guarantee that this module is 
built after them. You can update them by running `mvn process-resources 
-Pformat -N` from the source tree root directory -->
+                <dependency>
+                    <groupId>org.apache.camel.quarkus</groupId>
+                    <artifactId>camel-quarkus-direct-deployment</artifactId>
+                    <version>${project.version}</version>
+                    <type>pom</type>
+                    <scope>test</scope>
+                    <exclusions>
+                        <exclusion>
+                            <groupId>*</groupId>
+                            <artifactId>*</artifactId>
+                        </exclusion>
+                    </exclusions>
+                </dependency>
                 <dependency>
                     <groupId>org.apache.camel.quarkus</groupId>
                     <artifactId>camel-quarkus-keycloak-deployment</artifactId>
diff --git 
a/integration-tests/keycloak/src/main/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakEvaluatePermissionResource.java
 
b/integration-tests/keycloak/src/main/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakEvaluatePermissionResource.java
new file mode 100644
index 0000000000..da927f7431
--- /dev/null
+++ 
b/integration-tests/keycloak/src/main/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakEvaluatePermissionResource.java
@@ -0,0 +1,195 @@
+/*
+ * 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.quarkus.component.keycloak.it;
+
+import java.util.Map;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import org.apache.camel.Exchange;
+import org.apache.camel.component.keycloak.KeycloakConstants;
+
+@Path("/keycloak/evaluate-permission")
+@ApplicationScoped
+public class KeycloakEvaluatePermissionResource extends 
KeycloakResourceSupport {
+
+    /**
+     * permissionsOnly mode: sets CamelKeycloakPermissionsOnly=true
+     * Returns Map with keys: permissions, permissionCount, granted
+     */
+    @Path("/permissions-only")
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response testEvaluatePermissionWithPermissionsOnly(
+            @QueryParam("clientId") String clientId,
+            @QueryParam("clientSecret") String clientSecret,
+            @QueryParam("accessToken") String accessToken,
+            @QueryParam("resourceNames") String resourceNames,
+            @QueryParam("scopes") String scopes) {
+
+        Exchange exchange = producerTemplate.send("direct:evaluatePermission", 
e -> {
+            e.getIn().setHeader("X-Authz-Client-Id", clientId);
+            e.getIn().setHeader("X-Authz-Client-Secret", clientSecret);
+            e.getIn().setHeader(KeycloakConstants.ACCESS_TOKEN, accessToken);
+            e.getIn().setHeader(KeycloakConstants.PERMISSIONS_ONLY, 
Boolean.TRUE);
+            if (resourceNames != null) {
+                
e.getIn().setHeader(KeycloakConstants.PERMISSION_RESOURCE_NAMES, resourceNames);
+            }
+            if (scopes != null) {
+                e.getIn().setHeader(KeycloakConstants.PERMISSION_SCOPES, 
scopes);
+            }
+        });
+
+        if (exchange.getException() != null) {
+            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+                    .entity(exchange.getException().getMessage())
+                    .build();
+        }
+        return Response.ok(exchange.getMessage().getBody(Map.class)).build();
+    }
+
+    /**
+     * RPT mode: PERMISSIONS_ONLY header deliberately omitted
+     * Returns Map with keys: token, tokenType, expiresIn, refreshToken, 
refreshExpiresIn, upgraded.
+     */
+    @Path("/rpt")
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response testEvaluatePermissionWithRPT(
+            @QueryParam("clientId") String clientId,
+            @QueryParam("clientSecret") String clientSecret,
+            @QueryParam("accessToken") String accessToken,
+            @QueryParam("resourceNames") String resourceNames) {
+
+        Exchange exchange = producerTemplate.send("direct:evaluatePermission", 
e -> {
+            e.getIn().setHeader("X-Authz-Client-Id", clientId);
+            e.getIn().setHeader("X-Authz-Client-Secret", clientSecret);
+            e.getIn().setHeader(KeycloakConstants.ACCESS_TOKEN, accessToken);
+            // if no PERMISSIONS_ONLY in header then go RPT
+            if (resourceNames != null) {
+                
e.getIn().setHeader(KeycloakConstants.PERMISSION_RESOURCE_NAMES, resourceNames);
+            }
+        });
+
+        if (exchange.getException() != null) {
+            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+                    .entity(exchange.getException().getMessage())
+                    .build();
+        }
+        return Response.ok(exchange.getMessage().getBody(Map.class)).build();
+    }
+
+    /**
+     * test evaluate permission with username and password
+     */
+    @Path("/username-password")
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response testEvaluatePermissionWithUsernameAndPassword(
+            @QueryParam("clientId") String clientId,
+            @QueryParam("clientSecret") String clientSecret,
+            @QueryParam("username") String username,
+            @QueryParam("password") String password,
+            @QueryParam("resourceNames") String resourceNames) {
+
+        Exchange exchange = 
producerTemplate.send("direct:evaluatePermissionUserPass", e -> {
+            e.getIn().setHeader("X-Authz-Client-Id", clientId);
+            e.getIn().setHeader("X-Authz-Client-Secret", clientSecret);
+            e.getIn().setHeader("X-Authz-Username", username);
+            e.getIn().setHeader("X-Authz-Password", password);
+            e.getIn().setHeader(KeycloakConstants.PERMISSIONS_ONLY, 
Boolean.TRUE);
+            if (resourceNames != null) {
+                
e.getIn().setHeader(KeycloakConstants.PERMISSION_RESOURCE_NAMES, resourceNames);
+            }
+        });
+
+        if (exchange.getException() != null) {
+            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+                    .entity(exchange.getException().getMessage())
+                    .build();
+        }
+        return Response.ok(exchange.getMessage().getBody(Map.class)).build();
+    }
+
+    /**
+     * test SUBJECT_TOKEN header configuration
+     */
+    @Path("/subject-token")
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response subjectToken(
+            @QueryParam("clientId") String clientId,
+            @QueryParam("clientSecret") String clientSecret,
+            @QueryParam("subjectToken") String subjectToken,
+            @QueryParam("resourceNames") String resourceNames) {
+
+        Exchange exchange = producerTemplate.send("direct:evaluatePermission", 
e -> {
+            e.getIn().setHeader("X-Authz-Client-Id", clientId);
+            e.getIn().setHeader("X-Authz-Client-Secret", clientSecret);
+            e.getIn().setHeader(KeycloakConstants.SUBJECT_TOKEN, subjectToken);
+            e.getIn().setHeader(KeycloakConstants.PERMISSIONS_ONLY, 
Boolean.TRUE);
+            if (resourceNames != null) {
+                
e.getIn().setHeader(KeycloakConstants.PERMISSION_RESOURCE_NAMES, resourceNames);
+            }
+        });
+
+        if (exchange.getException() != null) {
+            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+                    .entity(exchange.getException().getMessage())
+                    .build();
+        }
+        return Response.ok(exchange.getMessage().getBody(Map.class)).build();
+    }
+
+    /**
+     * test PERMISSION_AUDIENCE header configuration
+     */
+    @Path("/audience")
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response audience(
+            @QueryParam("clientId") String clientId,
+            @QueryParam("clientSecret") String clientSecret,
+            @QueryParam("accessToken") String accessToken,
+            @QueryParam("audience") String audience,
+            @QueryParam("resourceNames") String resourceNames) {
+
+        Exchange exchange = producerTemplate.send("direct:evaluatePermission", 
e -> {
+            e.getIn().setHeader("X-Authz-Client-Id", clientId);
+            e.getIn().setHeader("X-Authz-Client-Secret", clientSecret);
+            e.getIn().setHeader(KeycloakConstants.ACCESS_TOKEN, accessToken);
+            e.getIn().setHeader(KeycloakConstants.PERMISSION_AUDIENCE, 
audience);
+            e.getIn().setHeader(KeycloakConstants.PERMISSIONS_ONLY, 
Boolean.TRUE);
+            if (resourceNames != null) {
+                
e.getIn().setHeader(KeycloakConstants.PERMISSION_RESOURCE_NAMES, resourceNames);
+            }
+        });
+
+        if (exchange.getException() != null) {
+            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+                    .entity(exchange.getException().getMessage())
+                    .build();
+        }
+        return Response.ok(exchange.getMessage().getBody(Map.class)).build();
+    }
+}
diff --git 
a/integration-tests/keycloak/src/main/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakRouteBuilder.java
 
b/integration-tests/keycloak/src/main/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakRouteBuilder.java
new file mode 100644
index 0000000000..00fa81ae0e
--- /dev/null
+++ 
b/integration-tests/keycloak/src/main/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakRouteBuilder.java
@@ -0,0 +1,150 @@
+/*
+ * 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.quarkus.component.keycloak.it;
+
+import java.util.List;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.keycloak.security.KeycloakSecurityPolicy;
+import org.eclipse.microprofile.config.ConfigProvider;
+
+@ApplicationScoped
+public class KeycloakRouteBuilder extends RouteBuilder {
+    @Override
+    public void configure() {
+        // Values provided by KeycloakTestResource
+        var config = ConfigProvider.getConfig();
+
+        String serverUrl = config.getValue("keycloak.url", String.class);
+        String realm = config.getValue("test.realm", String.class);
+        String clientId = config.getValue("test.client.id", String.class);
+        String clientSecret = config.getValue("test.client.secret", 
String.class);
+
+        // Route 1: secure default
+        KeycloakSecurityPolicy secureDefaultPolicy = new 
KeycloakSecurityPolicy();
+        secureDefaultPolicy.setServerUrl(serverUrl);
+        secureDefaultPolicy.setRealm(realm);
+        secureDefaultPolicy.setClientId(clientId);
+        secureDefaultPolicy.setClientSecret(clientSecret);
+        secureDefaultPolicy.setValidateTokenBinding(true);
+        secureDefaultPolicy.setAllowTokenFromHeader(true);
+        secureDefaultPolicy.setPreferPropertyOverHeader(true);
+
+        from("direct:secure-default")
+                .routeId("secure-default")
+                .policy(secureDefaultPolicy)
+                .transform().constant("Access granted - secure default");
+
+        // Route 2: maximum security - headers disabled
+        KeycloakSecurityPolicy maxSecurityPolicy = new 
KeycloakSecurityPolicy();
+        maxSecurityPolicy.setServerUrl(serverUrl);
+        maxSecurityPolicy.setRealm(realm);
+        maxSecurityPolicy.setClientId(clientId);
+        maxSecurityPolicy.setClientSecret(clientSecret);
+        maxSecurityPolicy.setAllowTokenFromHeader(false);
+
+        from("direct:max-security")
+                .routeId("max-security")
+                .policy(maxSecurityPolicy)
+                .transform().constant("Access granted - max security");
+
+        // Route 3: legacy unsafe - prefer property disabled
+        KeycloakSecurityPolicy legacyPolicy = new KeycloakSecurityPolicy();
+        legacyPolicy.setServerUrl(serverUrl);
+        legacyPolicy.setRealm(realm);
+        legacyPolicy.setClientId(clientId);
+        legacyPolicy.setClientSecret(clientSecret);
+        legacyPolicy.setValidateTokenBinding(true);
+        legacyPolicy.setAllowTokenFromHeader(true);
+        legacyPolicy.setPreferPropertyOverHeader(false);
+
+        from("direct:legacy-unsafe")
+                .routeId("legacy-unsafe")
+                .policy(legacyPolicy)
+                .transform().constant("SHOULD NOT REACH HERE");
+
+        // Route 4: Admin-only route
+        KeycloakSecurityPolicy adminPolicy = new KeycloakSecurityPolicy();
+        adminPolicy.setServerUrl(serverUrl);
+        adminPolicy.setRealm(realm);
+        adminPolicy.setClientId(clientId);
+        adminPolicy.setClientSecret(clientSecret);
+        adminPolicy.setRequiredRoles(List.of("admin"));
+        adminPolicy.setPreferPropertyOverHeader(true);
+
+        from("direct:admin-only")
+                .routeId("admin-only")
+                .policy(adminPolicy)
+                .transform().constant("Admin access granted");
+
+        // 
-------------------EvaluatePermissionRoute-----------------------------
+
+        // Route 1: Evaluate permission with clientId and clientSecret
+        from("direct:evaluatePermission")
+                .routeId("evaluate-permission")
+                .toD("keycloak:authz"
+                        + "?serverUrl=" + serverUrl
+                        + "&realm=" + realm
+                        + "&clientId=${header.X-Authz-Client-Id}"
+                        + "&clientSecret=${header.X-Authz-Client-Secret}"
+                        + "&operation=evaluatePermission");
+
+        // Route 2: Evaluate permission with username/password
+        from("direct:evaluatePermissionUserPass")
+                .routeId("evaluate-permission-userpass")
+                .toD("keycloak:authz"
+                        + "?serverUrl=" + serverUrl
+                        + "&realm=" + realm
+                        + "&clientId=${header.X-Authz-Client-Id}"
+                        + "&clientSecret=${header.X-Authz-Client-Secret}"
+                        + "&username=${header.X-Authz-Username}"
+                        + "&password=${header.X-Authz-Password}"
+                        + "&operation=evaluatePermission");
+
+        // 
-------------------IntrospectionCacheRoute-----------------------------
+
+        // Route 1: introspection with ConcurrentHashMap cache (default)
+        KeycloakSecurityPolicy concurrentMapPolicy = new 
KeycloakSecurityPolicy();
+        concurrentMapPolicy.setServerUrl(serverUrl);
+        concurrentMapPolicy.setRealm(realm);
+        concurrentMapPolicy.setClientId(clientId);
+        concurrentMapPolicy.setClientSecret(clientSecret);
+        concurrentMapPolicy.setUseTokenIntrospection(true);
+        concurrentMapPolicy.setIntrospectionCacheTtl(60);
+        concurrentMapPolicy.setIntrospectionCacheEnabled(true);
+
+        from("direct:introspection-concurrent-map")
+                .routeId("introspection-concurrent-map")
+                .policy(concurrentMapPolicy)
+                .transform().constant("Access granted - concurrent map cache");
+
+        // Route 2: introspection with no cache
+        KeycloakSecurityPolicy noCachePolicy = new KeycloakSecurityPolicy();
+        noCachePolicy.setServerUrl(serverUrl);
+        noCachePolicy.setRealm(realm);
+        noCachePolicy.setClientId(clientId);
+        noCachePolicy.setClientSecret(clientSecret);
+        noCachePolicy.setUseTokenIntrospection(true);
+        noCachePolicy.setIntrospectionCacheEnabled(false);
+
+        from("direct:introspection-no-cache")
+                .routeId("introspection-no-cache")
+                .policy(noCachePolicy)
+                .transform().constant("Access granted - no cache");
+    }
+}
diff --git 
a/integration-tests/keycloak/src/main/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakSecurityPolicyResource.java
 
b/integration-tests/keycloak/src/main/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakSecurityPolicyResource.java
new file mode 100644
index 0000000000..57155b6c2f
--- /dev/null
+++ 
b/integration-tests/keycloak/src/main/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakSecurityPolicyResource.java
@@ -0,0 +1,337 @@
+/*
+ * 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.quarkus.component.keycloak.it;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import org.apache.camel.Exchange;
+import org.apache.camel.component.keycloak.security.KeycloakSecurityConstants;
+import org.apache.camel.component.keycloak.security.KeycloakTokenIntrospector;
+import org.apache.camel.component.keycloak.security.cache.TokenCache;
+import org.apache.camel.component.keycloak.security.cache.TokenCacheType;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+/**
+ * Keycloak REST resource for SecurityPolicy operations.
+ */
+@Path("/keycloak")
+@ApplicationScoped
+public class KeycloakSecurityPolicyResource extends KeycloakResourceSupport {
+
+    @ConfigProperty(name = "test.realm")
+    String testRealm;
+
+    @Path("/secure-policy/user-with-token-in-property")
+    @GET
+    @Produces(MediaType.TEXT_PLAIN)
+    public Response 
securityPolicyWithTokenInProperty(@QueryParam("propertyToken") String 
propertyToken) {
+        Exchange exchange = producerTemplate.send("direct:secure-default", e 
-> {
+            e.setProperty(KeycloakSecurityConstants.ACCESS_TOKEN_PROPERTY, 
propertyToken);
+        });
+
+        if (exchange.getException() != null) {
+            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+                    .entity(exchange.getException().getMessage())
+                    .build();
+        }
+        return Response.ok(exchange.getMessage().getBody()).build();
+    }
+
+    @Path("/secure-policy/user-with-token-in-header")
+    @GET
+    @Produces(MediaType.TEXT_PLAIN)
+    public Response securityPolicyWithTokenInHeader(@QueryParam("headerToken") 
String headerToken) {
+
+        Exchange exchange = producerTemplate.send("direct:secure-default", e 
-> {
+            e.getIn().setHeader(KeycloakSecurityConstants.ACCESS_TOKEN_HEADER, 
headerToken);
+        });
+
+        if (exchange.getException() != null) {
+            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+                    .entity(exchange.getException().getMessage())
+                    .build();
+        }
+
+        return Response.ok(exchange.getMessage().getBody()).build();
+    }
+
+    @Path("/secure-policy/user-with-token-in-property-and-header")
+    @GET
+    @Produces(MediaType.TEXT_PLAIN)
+    public Response 
securityPolicyWithPropertyPreferredOverHeader(@QueryParam("propertyToken") 
String propertyToken,
+            @QueryParam("headerToken") String headerToken) {
+
+        Exchange exchange = producerTemplate.send("direct:secure-default", e 
-> {
+            e.setProperty(KeycloakSecurityConstants.ACCESS_TOKEN_PROPERTY, 
propertyToken);
+            e.getIn().setHeader(KeycloakSecurityConstants.ACCESS_TOKEN_HEADER, 
headerToken);
+        });
+
+        if (exchange.getException() != null) {
+            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+                    .entity(exchange.getException().getMessage())
+                    .build();
+        }
+
+        return Response.ok(exchange.getMessage().getBody()).build();
+    }
+
+    @Path("/secure-policy/max-security")
+    @GET
+    @Produces(MediaType.TEXT_PLAIN)
+    public Response 
securityPolicyWithHeaderDisabled(@QueryParam("propertyToken") String 
propertyToken,
+            @QueryParam("headerToken") String headerToken) {
+        Exchange exchange;
+        if (propertyToken != null) {
+            exchange = producerTemplate.send("direct:max-security", e -> {
+                e.setProperty(KeycloakSecurityConstants.ACCESS_TOKEN_PROPERTY, 
propertyToken);
+            });
+        } else if (headerToken != null) {
+            exchange = producerTemplate.send("direct:max-security", e -> {
+                
e.getIn().setHeader(KeycloakSecurityConstants.ACCESS_TOKEN_HEADER, headerToken);
+            });
+        } else {
+            exchange = producerTemplate.send("direct:max-security", e -> {
+            });
+        }
+
+        if (exchange.getException() != null) {
+            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+                    .entity(exchange.getException().getMessage())
+                    .build();
+        }
+
+        return Response.ok(exchange.getMessage().getBody()).build();
+    }
+
+    @Path("/secure-policy/legacy-unsafe")
+    @GET
+    @Produces(MediaType.TEXT_PLAIN)
+    public Response 
securityPolicyWithLegacyUnsafe(@QueryParam("propertyToken") String 
propertyToken,
+            @QueryParam("headerToken") String headerToken) {
+
+        Exchange exchange = producerTemplate.send("direct:legacy-unsafe", e -> 
{
+            e.setProperty(KeycloakSecurityConstants.ACCESS_TOKEN_PROPERTY, 
propertyToken);
+            e.getIn().setHeader(KeycloakSecurityConstants.ACCESS_TOKEN_HEADER, 
headerToken);
+        });
+
+        if (exchange.getException() != null) {
+            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+                    .entity(exchange.getException().getMessage())
+                    .build();
+        }
+
+        return Response.ok(exchange.getMessage().getBody()).build();
+    }
+
+    @Path("/secure-policy/authorization-header-format")
+    @GET
+    @Produces(MediaType.TEXT_PLAIN)
+    public Response 
securityPolicyWithAuthorizationHeader(@QueryParam("headerToken") String 
headerToken) {
+
+        String result = 
producerTemplate.requestBodyAndHeader("direct:secure-default", "test",
+                "Authorization", "Bearer " + headerToken, String.class);
+
+        return Response.ok(result).build();
+    }
+
+    @Path("/secure-policy/admin-only")
+    @GET
+    @Produces(MediaType.TEXT_PLAIN)
+    public Response securityPolicyWithAdminOnly(@QueryParam("propertyToken") 
String propertyToken,
+            @QueryParam("headerToken") String headerToken) {
+
+        Exchange exchange = producerTemplate.send("direct:admin-only", e -> {
+            e.setProperty(KeycloakSecurityConstants.ACCESS_TOKEN_PROPERTY, 
propertyToken);
+            e.getIn().setHeader(KeycloakSecurityConstants.ACCESS_TOKEN_HEADER, 
headerToken);
+        });
+
+        if (exchange.getException() != null) {
+            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+                    .entity(exchange.getException().getMessage())
+                    .build();
+        }
+
+        return Response.ok(exchange.getMessage().getBody()).build();
+    }
+
+    @Path("/secure-policy/introspection-cache-concurrent-map")
+    @GET
+    @Produces(MediaType.TEXT_PLAIN)
+    public Response policyWithConcurrentMapCache(
+            @QueryParam("propertyToken") String propertyToken) {
+
+        Exchange exchange = 
producerTemplate.send("direct:introspection-concurrent-map", e -> {
+            e.setProperty(KeycloakSecurityConstants.ACCESS_TOKEN_PROPERTY, 
propertyToken);
+        });
+
+        if (exchange.getException() != null) {
+            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+                    .entity(exchange.getException().getMessage())
+                    .build();
+        }
+        return Response.ok(exchange.getMessage().getBody()).build();
+    }
+
+    @Path("/secure-policy/introspection-no-cache")
+    @GET
+    @Produces(MediaType.TEXT_PLAIN)
+    public Response policyWithNoCache(
+            @QueryParam("propertyToken") String propertyToken) {
+
+        Exchange exchange = 
producerTemplate.send("direct:introspection-no-cache", e -> {
+            e.setProperty(KeycloakSecurityConstants.ACCESS_TOKEN_PROPERTY, 
propertyToken);
+        });
+
+        if (exchange.getException() != null) {
+            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+                    .entity(exchange.getException().getMessage())
+                    .build();
+        }
+        return Response.ok(exchange.getMessage().getBody()).build();
+    }
+
+    @Path("/introspection-cache/introspector/concurrent-map")
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response introspectorWithConcurrentMap(
+            @QueryParam("accessToken") String accessToken,
+            @QueryParam("clientId") String clientId,
+            @QueryParam("clientSecret") String clientSecret) {
+
+        KeycloakTokenIntrospector introspector = buildIntrospector(clientId, 
clientSecret,
+                TokenCacheType.CONCURRENT_MAP,
+                60, 0, false);
+
+        try {
+            return buildIntrospectionResponse(introspector, accessToken);
+        } finally {
+            introspector.close();
+        }
+    }
+
+    @Path("/introspection-cache/introspector/caffeine")
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response introspectorWithCaffeine(
+            @QueryParam("accessToken") String accessToken,
+            @QueryParam("clientId") String clientId,
+            @QueryParam("clientSecret") String clientSecret) {
+
+        KeycloakTokenIntrospector introspector = buildIntrospector(clientId, 
clientSecret,
+                TokenCacheType.CAFFEINE,
+                60, 100, false);
+
+        try {
+            return buildIntrospectionResponse(introspector, accessToken);
+        } finally {
+            introspector.close();
+        }
+    }
+
+    @Path("/introspection-cache/introspector/none")
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response introspectorWithNoCache(
+            @QueryParam("accessToken") String accessToken,
+            @QueryParam("clientId") String clientId,
+            @QueryParam("clientSecret") String clientSecret) {
+
+        KeycloakTokenIntrospector introspector = buildIntrospector(clientId, 
clientSecret,
+                TokenCacheType.NONE,
+                60, 0, false);
+
+        try {
+            return buildIntrospectionResponse(introspector, accessToken);
+        } finally {
+            introspector.close();
+        }
+    }
+
+    @Path("/introspection-cache/introspector/caffeine-stats")
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response introspectorCaffeineStats(
+            @QueryParam("accessToken") String accessToken,
+            @QueryParam("clientId") String clientId,
+            @QueryParam("clientSecret") String clientSecret) {
+
+        KeycloakTokenIntrospector introspector = buildIntrospector(clientId, 
clientSecret,
+                TokenCacheType.CAFFEINE,
+                60, 100, true);
+
+        try {
+            introspector.introspect(accessToken);
+            introspector.introspect(accessToken);
+
+            TokenCache.CacheStats stats = introspector.getCacheStats();
+
+            Map<String, Object> result = new HashMap<>();
+            result.put("hitCount", stats.getHitCount());
+            result.put("missCount", stats.getMissCount());
+            result.put("evictionCount", stats.getEvictionCount());
+            result.put("hitRate", stats.getHitRate());
+            result.put("cacheSize", introspector.getCacheSize());
+
+            return Response.ok(result).build();
+        } catch (Exception e) {
+            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+                    .entity(e.getMessage())
+                    .build();
+        } finally {
+            introspector.close();
+        }
+    }
+
+    private KeycloakTokenIntrospector buildIntrospector(String clientId, 
String clientSecret, TokenCacheType cacheType,
+            long ttl, long maxSize, boolean recordStats) {
+        return new KeycloakTokenIntrospector(keycloakUrl, testRealm, clientId, 
clientSecret, cacheType, ttl, maxSize,
+                recordStats);
+    }
+
+    private Response buildIntrospectionResponse(
+            KeycloakTokenIntrospector introspector, String accessToken) {
+        try {
+            KeycloakTokenIntrospector.IntrospectionResult result = 
introspector.introspect(accessToken);
+
+            Map<String, Object> body = new HashMap<>();
+            body.put("active", result.isActive());
+            body.put("subject", result.getSubject());
+            body.put("cacheSize", introspector.getCacheSize());
+
+            TokenCache.CacheStats stats = introspector.getCacheStats();
+            if (stats != null) {
+                body.put("hitCount", stats.getHitCount());
+                body.put("missCount", stats.getMissCount());
+                body.put("hitRate", stats.getHitRate());
+            }
+
+            return Response.ok(body).build();
+        } catch (Exception e) {
+            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+                    .entity(e.getMessage())
+                    .build();
+        }
+    }
+}
diff --git 
a/integration-tests/keycloak/src/main/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakUserResource.java
 
b/integration-tests/keycloak/src/main/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakUserResource.java
index 62752ad95b..e244f8b690 100644
--- 
a/integration-tests/keycloak/src/main/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakUserResource.java
+++ 
b/integration-tests/keycloak/src/main/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakUserResource.java
@@ -24,6 +24,7 @@ import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.ws.rs.Consumes;
 import jakarta.ws.rs.DELETE;
 import jakarta.ws.rs.GET;
+import jakarta.ws.rs.HeaderParam;
 import jakarta.ws.rs.POST;
 import jakarta.ws.rs.PUT;
 import jakarta.ws.rs.Path;
@@ -102,6 +103,28 @@ public class KeycloakUserResource extends 
KeycloakResourceSupport {
         return Response.ok("User created successfully").build();
     }
 
+    @Path("/user/{realmName}")
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response bulkCreateUsers(
+            @PathParam("realmName") String realmName,
+            List<UserRepresentation> users) {
+
+        Map<String, Object> headers = new HashMap<>();
+        headers.put(KeycloakConstants.REALM_NAME, realmName);
+
+        headers.put(KeycloakConstants.USERS, users);
+
+        Object result = producerTemplate.requestBodyAndHeaders(
+                getKeycloakEndpoint() + "&operation=bulkCreateUsers",
+                null,
+                headers,
+                Map.class);
+
+        return Response.ok(result).build();
+    }
+
     @Path("/user/{realmName}/{username}")
     @GET
     @Produces(MediaType.APPLICATION_JSON)
@@ -162,6 +185,26 @@ public class KeycloakUserResource extends 
KeycloakResourceSupport {
         return Response.ok("User deleted successfully").build();
     }
 
+    @Path("/user/{realmName}")
+    @DELETE
+    @Produces(MediaType.APPLICATION_JSON)
+    @Consumes(MediaType.APPLICATION_JSON)
+    public Response bulkDeleteUsers(
+            @PathParam("realmName") String realmName,
+            List<String> userNameList) {
+
+        Map<String, Object> headers = new HashMap<>();
+        headers.put(KeycloakConstants.REALM_NAME, realmName);
+        headers.put(KeycloakConstants.USERNAMES, userNameList);
+
+        Object result = producerTemplate.requestBodyAndHeaders(
+                getKeycloakEndpoint() + "&operation=bulkDeleteUsers",
+                null,
+                headers);
+
+        return Response.ok(result).build();
+    }
+
     @Path("/user/{realmName}/{username}")
     @PUT
     @Consumes(MediaType.APPLICATION_JSON)
@@ -187,6 +230,31 @@ public class KeycloakUserResource extends 
KeycloakResourceSupport {
         return Response.ok("User updated successfully").build();
     }
 
+    @Path("/user/{realmName}")
+    @PUT
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response bulkUpdateUsers(
+            @PathParam("realmName") String realmName,
+            @HeaderParam("continueOnError") Boolean continueOnError,
+            List<UserRepresentation> users) {
+
+        Map<String, Object> headers = new HashMap<>();
+        headers.put(KeycloakConstants.REALM_NAME, realmName);
+        headers.put(KeycloakConstants.USERS, users);
+        if (continueOnError != null) {
+            headers.put(KeycloakConstants.CONTINUE_ON_ERROR, continueOnError);
+        }
+
+        Object result = producerTemplate.requestBodyAndHeaders(
+                getKeycloakEndpoint() + "&operation=bulkUpdateUsers",
+                null,
+                headers,
+                Map.class);
+
+        return Response.ok(result).build();
+    }
+
     @Path("/user/{realmName}/search")
     @GET
     @Produces(MediaType.APPLICATION_JSON)
@@ -234,6 +302,54 @@ public class KeycloakUserResource extends 
KeycloakResourceSupport {
         return Response.ok(result).build();
     }
 
+    @Path("/user-role/{realmName}/user/{username}")
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response assignRolesToUser(
+            @PathParam("realmName") String realmName,
+            @PathParam("username") String username,
+            List<String> roleNameList) {
+
+        String userId = getUserIdByUsername(realmName, username);
+
+        Map<String, Object> headers = new HashMap<>();
+        headers.put(KeycloakConstants.REALM_NAME, realmName);
+        headers.put(KeycloakConstants.USER_ID, userId);
+        headers.put(KeycloakConstants.ROLE_NAMES, roleNameList);
+
+        Object result = producerTemplate.requestBodyAndHeaders(
+                getKeycloakEndpoint() + "&operation=bulkAssignRolesToUser",
+                null,
+                headers,
+                Map.class);
+
+        return Response.ok(result).build();
+    }
+
+    @Path("/user-role/{realmName}/role/{roleName}")
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response assignRoleToUsers(
+            @PathParam("realmName") String realmName,
+            @PathParam("roleName") String roleName,
+            List<String> userNameList) {
+
+        Map<String, Object> headers = new HashMap<>();
+        headers.put(KeycloakConstants.REALM_NAME, realmName);
+        headers.put(KeycloakConstants.ROLE_NAME, roleName);
+        headers.put(KeycloakConstants.USERNAMES, userNameList);
+
+        Object result = producerTemplate.requestBodyAndHeaders(
+                getKeycloakEndpoint() + "&operation=bulkAssignRoleToUsers",
+                null,
+                headers,
+                Map.class);
+
+        return Response.ok(result).build();
+    }
+
     @Path("/user-role/{realmName}/{username}/{roleName}")
     @DELETE
     @Produces(MediaType.TEXT_PLAIN)
diff --git 
a/integration-tests/keycloak/src/main/resources/application.properties 
b/integration-tests/keycloak/src/main/resources/application.properties
index 09d7567767..026598a0a7 100644
--- a/integration-tests/keycloak/src/main/resources/application.properties
+++ b/integration-tests/keycloak/src/main/resources/application.properties
@@ -23,3 +23,5 @@ greenmail.container.image=${greenmail.container.image}
 # and keycloak-core contain overlapping packages
 # This is due the presence of the quarkus-test-keycloak-server dependency.
 quarkus.arc.ignored-split-packages=org.keycloak.*
+
+camel.component.caffeine-cache.stats-enabled = true
\ No newline at end of file
diff --git 
a/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakEvaluatePermissionTest.java
 
b/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakEvaluatePermissionTest.java
new file mode 100644
index 0000000000..a24e15e238
--- /dev/null
+++ 
b/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakEvaluatePermissionTest.java
@@ -0,0 +1,331 @@
+/*
+ * 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.quarkus.component.keycloak.it;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.http.ContentType;
+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.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.representations.idm.authorization.Logic;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import 
org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@QuarkusTest
+@QuarkusTestResource(KeycloakTestResource.class)
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class KeycloakEvaluatePermissionTest extends KeycloakTestBase {
+
+    private static String userToken;
+    private static final String RESOURCE_DOCUMENTS = "documents";
+    private static final String SCOPE_READ = "read";
+
+    @Test
+    @Order(1)
+    public void testSetup() {
+        KeycloakRealmLifecycle.createRealmWithSmtp(config("test.realm"));
+
+        // 1. Confidential client with Authorization Services enabled
+        ClientRepresentation authzClient = new ClientRepresentation();
+        authzClient.setClientId(TEST_CLIENT_ID);
+        authzClient.setSecret(TEST_CLIENT_SECRET);
+        authzClient.setPublicClient(false);
+        authzClient.setDirectAccessGrantsEnabled(true);
+        authzClient.setServiceAccountsEnabled(true);
+        authzClient.setAuthorizationServicesEnabled(true);
+        authzClient.setStandardFlowEnabled(false);
+
+        given()
+                .contentType(ContentType.JSON)
+                .body(authzClient)
+                .post("/keycloak/client/{realmName}/pojo", 
config("test.realm"))
+                .then()
+                .statusCode(anyOf(is(200), is(201)));
+
+        // 2. Protected resource with a read scope
+        ScopeRepresentation readScope = new ScopeRepresentation();
+        readScope.setName(SCOPE_READ);
+
+        ResourceRepresentation documents = new ResourceRepresentation();
+        documents.setName(RESOURCE_DOCUMENTS);
+        documents.setUris(Set.of("/documents/*"));
+        documents.setScopes(Set.of(readScope));
+
+        given()
+                .contentType(ContentType.JSON)
+                .body(documents)
+                .post("/keycloak/resource/{realmName}/{clientId}/pojo",
+                        config("test.realm"), TEST_CLIENT_ID)
+                .then()
+                .statusCode(anyOf(is(200), is(201)));
+
+        // 3. Test user
+        UserRepresentation user = new UserRepresentation();
+        user.setUsername(TEST_USER_NAME);
+        user.setEmail(TEST_USER_NAME + "@test.com");
+        user.setFirstName("Test");
+        user.setLastName("User");
+        user.setEnabled(true);
+
+        given()
+                .contentType(ContentType.JSON)
+                .body(List.of(user))
+                .post("/keycloak/user/{realmName}", config("test.realm"))
+                .then()
+                .statusCode(200);
+
+        given()
+                .queryParam("password", TEST_USER_PASSWORD)
+                .queryParam("temporary", false)
+                .post("/keycloak/user/{realmName}/{username}/reset-password",
+                        config("test.realm"), TEST_USER_NAME)
+                .then()
+                .statusCode(200);
+
+        String userId = given()
+                .get("/keycloak/user/{realmName}/{username}",
+                        config("test.realm"), TEST_USER_NAME)
+                .then()
+                .statusCode(200)
+                .extract().jsonPath().getString("id");
+
+        assertNotNull(userId, "userId must not be null after creation");
+
+        // 4. User policy
+        PolicyRepresentation userPolicy = new PolicyRepresentation();
+        userPolicy.setType("user");
+        userPolicy.setName("user-policy-" + 
UUID.randomUUID().toString().substring(0, 6));
+        userPolicy.setLogic(Logic.POSITIVE);
+        userPolicy.setConfig(Map.of("users", "[\"" + userId + "\"]"));
+
+        String policyLocation = given()
+                .contentType(ContentType.JSON)
+                .body(userPolicy)
+                .post("/keycloak/resource-policy/{realmName}/{clientId}/pojo",
+                        config("test.realm"), TEST_CLIENT_ID)
+                .then()
+                .statusCode(anyOf(is(200), is(201)))
+                .extract().header("Location");
+
+        String policyId = policyLocation != null
+                ? policyLocation.substring(policyLocation.lastIndexOf('/') + 1)
+                : fetchPolicyId(TEST_CLIENT_ID, userPolicy.getName());
+
+        // 5. Resource permission
+        ResourcePermissionRepresentation permission = new 
ResourcePermissionRepresentation();
+        permission.setName("docs-perm-" + 
UUID.randomUUID().toString().substring(0, 6));
+        permission.setResources(Set.of(RESOURCE_DOCUMENTS));
+        permission.setPolicies(Set.of(policyId));
+
+        given()
+                .contentType(ContentType.JSON)
+                .body(permission)
+                
.post("/keycloak/resource-permission/{realmName}/{clientId}/pojo",
+                        config("test.realm"), TEST_CLIENT_ID)
+                .then()
+                .statusCode(anyOf(is(200), is(201)));
+
+        // 6. Obtain access token
+        userToken = getAccessToken(TEST_USER_NAME, TEST_USER_PASSWORD, 
TEST_CLIENT_ID, TEST_CLIENT_SECRET);
+        assertNotNull(userToken, "userToken must be non-null after setup");
+    }
+
+    @Test
+    @Order(2)
+    public void testPermissionsOnly_responseContainsRequiredFields() {
+        given()
+                .queryParam("clientId", TEST_CLIENT_ID)
+                .queryParam("clientSecret", TEST_CLIENT_SECRET)
+                .queryParam("accessToken", userToken)
+                .get("/keycloak/evaluate-permission/permissions-only")
+                .then()
+                .statusCode(200)
+                .body("permissions", notNullValue())
+                .body("permissionCount", notNullValue())
+                .body("granted", notNullValue());
+    }
+
+    @Test
+    @Order(3)
+    public void testPermissionsOnly_userGrantedAccessToDocumentsRead() {
+        given()
+                .queryParam("clientId", TEST_CLIENT_ID)
+                .queryParam("clientSecret", TEST_CLIENT_SECRET)
+                .queryParam("accessToken", userToken)
+                .queryParam("resourceNames", RESOURCE_DOCUMENTS)
+                .queryParam("scopes", SCOPE_READ)
+                .get("/keycloak/evaluate-permission/permissions-only")
+                .then()
+                .statusCode(200)
+                .body("granted", is(true))
+                .body("permissionCount", greaterThan(0));
+    }
+
+    @Test
+    @Order(4)
+    public void testRptMode_responseContainsAllExpectedFields() {
+        given()
+                .queryParam("clientId", TEST_CLIENT_ID)
+                .queryParam("clientSecret", TEST_CLIENT_SECRET)
+                .queryParam("accessToken", userToken)
+                .queryParam("resourceNames", RESOURCE_DOCUMENTS)
+                .get("/keycloak/evaluate-permission/rpt")
+                .then()
+                .statusCode(200)
+                .body("token", notNullValue())
+                .body("tokenType", is("Bearer"))
+                .body("expiresIn", notNullValue())
+                .body("refreshToken", notNullValue())
+                .body("refreshExpiresIn", notNullValue())
+                .body("upgraded", notNullValue());
+    }
+
+    @Test
+    @Order(5)
+    public void testPermissionsOnly_resourceNamesWithWhitespace() {
+        given()
+                .queryParam("clientId", TEST_CLIENT_ID)
+                .queryParam("clientSecret", TEST_CLIENT_SECRET)
+                .queryParam("accessToken", userToken)
+                .queryParam("resourceNames", " " + RESOURCE_DOCUMENTS + " , 
unknown-resource ")
+                .get("/keycloak/evaluate-permission/permissions-only")
+                .then()
+                .statusCode(200)
+                .body("granted", is(true));
+    }
+
+    @Test
+    @Order(6)
+    public void testPermissionsOnly_scopeOnlyNoResource() {
+        given()
+                .queryParam("clientId", TEST_CLIENT_ID)
+                .queryParam("clientSecret", TEST_CLIENT_SECRET)
+                .queryParam("accessToken", userToken)
+                .queryParam("scopes", SCOPE_READ)
+                .get("/keycloak/evaluate-permission/permissions-only")
+                .then()
+                .statusCode(200)
+                .body("granted", is(true));
+    }
+
+    @Test
+    @Order(7)
+    public void testPermissionsOnly_withAudienceHeader() {
+        given()
+                .queryParam("clientId", TEST_CLIENT_ID)
+                .queryParam("clientSecret", TEST_CLIENT_SECRET)
+                .queryParam("accessToken", userToken)
+                .queryParam("resourceNames", RESOURCE_DOCUMENTS)
+                .queryParam("audience", TEST_CLIENT_ID)
+                .get("/keycloak/evaluate-permission/audience")
+                .then()
+                .statusCode(200)
+                .body("granted", is(true));
+    }
+
+    @Test
+    @Order(8)
+    public void testPermissionsOnly_withSubjectToken() {
+        given()
+                .queryParam("clientId", TEST_CLIENT_ID)
+                .queryParam("clientSecret", TEST_CLIENT_SECRET)
+                .queryParam("subjectToken", userToken)
+                .queryParam("resourceNames", RESOURCE_DOCUMENTS)
+                .get("/keycloak/evaluate-permission/subject-token")
+                .then()
+                .statusCode(200)
+                .body("granted", is(true));
+    }
+
+    @Test
+    @Order(9)
+    public void testEvaluatePermission_usernamePasswordAuth_granted() {
+        given()
+                .queryParam("clientId", TEST_CLIENT_ID)
+                .queryParam("clientSecret", TEST_CLIENT_SECRET)
+                .queryParam("username", TEST_USER_NAME)
+                .queryParam("password", TEST_USER_PASSWORD)
+                .queryParam("resourceNames", RESOURCE_DOCUMENTS)
+                .get("/keycloak/evaluate-permission/username-password")
+                .then()
+                .statusCode(200)
+                .body("granted", is(true));
+    }
+
+    @Test
+    @Order(10)
+    public void 
testEvaluatePermission_missingClientSecret_returns500WithValidationMessage() {
+        given()
+                .queryParam("clientId", TEST_CLIENT_ID)
+                .queryParam("clientSecret", "")
+                .queryParam("accessToken", userToken)
+                .get("/keycloak/evaluate-permission/permissions-only")
+                .then()
+                .statusCode(500)
+                .body(containsString("Client secret must be specified"));
+    }
+
+    @Test
+    @Order(11)
+    public void testEvaluatePermission_invalidToken_returns500WithAuthError() {
+        given()
+                .queryParam("clientId", TEST_CLIENT_ID)
+                .queryParam("clientSecret", TEST_CLIENT_SECRET)
+                .queryParam("accessToken", "invalid.token")
+                .get("/keycloak/evaluate-permission/permissions-only")
+                .then()
+                .statusCode(500)
+                .body(containsString("401"));
+    }
+
+    @Test
+    @Order(100)
+    public void testCleanup_DeleteRealm() {
+        KeycloakRealmLifecycle.deleteRealm(config("test.realm"));
+    }
+
+    private String fetchPolicyId(String clientId, String policyName) {
+        return given()
+                .get("/keycloak/resource-policy/{realmName}/{clientId}",
+                        config("test.realm"), clientId)
+                .then()
+                .statusCode(200)
+                .extract().jsonPath()
+                .param("name", policyName)
+                .getString("find { it.name == name }.id");
+    }
+}
diff --git 
a/extensions/keycloak/deployment/src/main/java/org/apache/camel/quarkus/component/keycloak/deployment/KeycloakProcessor.java
 
b/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakEvaluatePermissionTestIT.java
similarity index 50%
copy from 
extensions/keycloak/deployment/src/main/java/org/apache/camel/quarkus/component/keycloak/deployment/KeycloakProcessor.java
copy to 
integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakEvaluatePermissionTestIT.java
index 85530325c0..725a88a7fb 100644
--- 
a/extensions/keycloak/deployment/src/main/java/org/apache/camel/quarkus/component/keycloak/deployment/KeycloakProcessor.java
+++ 
b/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakEvaluatePermissionTestIT.java
@@ -14,26 +14,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.camel.quarkus.component.keycloak.deployment;
 
-import io.quarkus.deployment.annotations.BuildProducer;
-import io.quarkus.deployment.annotations.BuildStep;
-import io.quarkus.deployment.builditem.FeatureBuildItem;
-import 
io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem;
-import org.keycloak.common.util.BouncyIntegration;
+package org.apache.camel.quarkus.component.keycloak.it;
 
-class KeycloakProcessor {
-
-    private static final String FEATURE = "camel-keycloak";
-
-    @BuildStep
-    FeatureBuildItem feature() {
-        return new FeatureBuildItem(FEATURE);
-    }
-
-    @BuildStep
-    void 
runtimeInitializedClasses(BuildProducer<RuntimeInitializedClassBuildItem> 
runtimeInitializedClass) {
-        runtimeInitializedClass.produce(new 
RuntimeInitializedClassBuildItem(BouncyIntegration.class.getName()));
-    }
+import io.quarkus.test.junit.QuarkusIntegrationTest;
 
+@QuarkusIntegrationTest
+public class KeycloakEvaluatePermissionTestIT extends 
KeycloakEvaluatePermissionTest {
 }
diff --git 
a/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakRoleTest.java
 
b/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakRoleTest.java
index 7a0983b15b..596f0a56b4 100644
--- 
a/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakRoleTest.java
+++ 
b/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakRoleTest.java
@@ -21,8 +21,12 @@ import java.util.List;
 import io.quarkus.test.common.QuarkusTestResource;
 import io.quarkus.test.junit.QuarkusTest;
 import io.restassured.http.ContentType;
-import org.junit.jupiter.api.*;
+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.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
 
 import static io.restassured.RestAssured.given;
 import static org.hamcrest.CoreMatchers.is;
@@ -196,6 +200,87 @@ class KeycloakRoleTest extends KeycloakTestBase {
                 .body(is("Role removed from user successfully"));
     }
 
+    @Test
+    @Order(9)
+    public void testAssignRolesToUser() {
+        List<String> existingRoleNames = given()
+                .when()
+                .get("/keycloak/role/{realmName}", TEST_REALM_NAME)
+                .then()
+                .statusCode(200)
+                .contentType(ContentType.JSON)
+                .extract()
+                .body()
+                .jsonPath()
+                .getList(".", 
RoleRepresentation.class).stream().map(RoleRepresentation::getName).toList();
+
+        given()
+                .when()
+                .contentType(ContentType.JSON)
+                .body(existingRoleNames)
+                .post("/keycloak/user-role/{realmName}/user/{username}",
+                        TEST_REALM_NAME, TEST_USER_NAME)
+                .then()
+                .statusCode(200)
+                .body("total", is(existingRoleNames.size()))
+                .body("success", is(existingRoleNames.size()))
+                .body("assigned", is(existingRoleNames.size()))
+                .body("results.size()", is(existingRoleNames.size()));
+    }
+
+    @Test
+    @Order(10)
+    public void testAssignRoleToUsers() {
+        // get an role
+        RoleRepresentation role = given()
+                .when()
+                .get("/keycloak/role/{realmName}/{roleName}", TEST_REALM_NAME, 
TEST_ROLE_NAME)
+                .then()
+                .statusCode(200)
+                .contentType(ContentType.JSON)
+                .extract()
+                .as(RoleRepresentation.class);
+
+        // Create additional test user for user-role operations
+        String additionalTestUserName = TEST_USER_NAME + "-additionalTestUser";
+        given()
+                .queryParam("email", additionalTestUserName + "@test.com")
+                .queryParam("firstName", "AdditionalTest")
+                .queryParam("lastName", "AdditionalUser")
+                .when()
+                .post("/keycloak/user/{realmName}/{username}", 
TEST_REALM_NAME, additionalTestUserName)
+                .then()
+                .statusCode(201);
+
+        // get all existing users
+        List<String> allUserNames = given()
+                .when()
+                .get("/keycloak/user/{realmName}", TEST_REALM_NAME)
+                .then()
+                .statusCode(200)
+                .contentType(ContentType.JSON)
+                .extract()
+                .body()
+                .jsonPath()
+                .getList(".", 
UserRepresentation.class).stream().map(UserRepresentation::getUsername).toList();
+
+        assertThat(allUserNames.size(), is(2));
+
+        // assign one role to all users
+        given()
+                .when()
+                .contentType(ContentType.JSON)
+                .body(allUserNames)
+                .post("/keycloak/user-role/{realmName}/role/{roleName}",
+                        TEST_REALM_NAME, role.getName())
+                .then()
+                .statusCode(200)
+                .body("total", is(allUserNames.size()))
+                .body("success", is(allUserNames.size()))
+                .body("roleName", is(role.getName()))
+                .body("results.size()", is(allUserNames.size()));
+    }
+
     @Test
     @Order(100)
     public void testCleanupRoles() {
diff --git 
a/extensions/keycloak/deployment/src/main/java/org/apache/camel/quarkus/component/keycloak/deployment/KeycloakProcessor.java
 
b/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakSecurityPolicyIT.java
similarity index 50%
copy from 
extensions/keycloak/deployment/src/main/java/org/apache/camel/quarkus/component/keycloak/deployment/KeycloakProcessor.java
copy to 
integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakSecurityPolicyIT.java
index 85530325c0..77b2a2a068 100644
--- 
a/extensions/keycloak/deployment/src/main/java/org/apache/camel/quarkus/component/keycloak/deployment/KeycloakProcessor.java
+++ 
b/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakSecurityPolicyIT.java
@@ -14,26 +14,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.camel.quarkus.component.keycloak.deployment;
+package org.apache.camel.quarkus.component.keycloak.it;
 
-import io.quarkus.deployment.annotations.BuildProducer;
-import io.quarkus.deployment.annotations.BuildStep;
-import io.quarkus.deployment.builditem.FeatureBuildItem;
-import 
io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem;
-import org.keycloak.common.util.BouncyIntegration;
-
-class KeycloakProcessor {
-
-    private static final String FEATURE = "camel-keycloak";
-
-    @BuildStep
-    FeatureBuildItem feature() {
-        return new FeatureBuildItem(FEATURE);
-    }
-
-    @BuildStep
-    void 
runtimeInitializedClasses(BuildProducer<RuntimeInitializedClassBuildItem> 
runtimeInitializedClass) {
-        runtimeInitializedClass.produce(new 
RuntimeInitializedClassBuildItem(BouncyIntegration.class.getName()));
-    }
+import io.quarkus.test.junit.QuarkusIntegrationTest;
 
+@QuarkusIntegrationTest
+public class KeycloakSecurityPolicyIT extends KeycloakSecurityPolicyTest {
 }
diff --git 
a/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakSecurityPolicyTest.java
 
b/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakSecurityPolicyTest.java
new file mode 100644
index 0000000000..ad36a31bf7
--- /dev/null
+++ 
b/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakSecurityPolicyTest.java
@@ -0,0 +1,432 @@
+/*
+ * 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.quarkus.component.keycloak.it;
+
+import java.util.List;
+
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.http.ContentType;
+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.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.notNullValue;
+
+@QuarkusTest
+@QuarkusTestResource(KeycloakTestResource.class)
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class KeycloakSecurityPolicyTest extends KeycloakSecurityPolicyTestBase 
{
+
+    private static String adminToken;
+    private static String normalUserToken;
+    private static String attackerToken;
+
+    @Test
+    @Order(1)
+    public void testSetup() {
+        createRealm();
+        createClient();
+
+        createRole(ADMIN_ROLE);
+        createRole(USER_ROLE);
+
+        createUser(ADMIN_USER);
+        createUser(NORMAL_USER);
+        createUser(ATTACKER_USER);
+
+        resetPassword(ADMIN_USER, ADMIN_PASSWORD);
+        resetPassword(NORMAL_USER, NORMAL_PASSWORD);
+        resetPassword(ATTACKER_USER, ATTACKER_PASSWORD);
+
+        assignRole(ADMIN_USER, ADMIN_ROLE);
+        assignRole(NORMAL_USER, USER_ROLE);
+        assignRole(ATTACKER_USER, USER_ROLE);
+
+        adminToken = getAccessToken(ADMIN_USER, ADMIN_PASSWORD, 
config("test.client.id"), TEST_CLIENT_SECRET);
+        normalUserToken = getAccessToken(NORMAL_USER, NORMAL_PASSWORD, 
config("test.client.id"), TEST_CLIENT_SECRET);
+        attackerToken = getAccessToken(ATTACKER_USER, ATTACKER_PASSWORD, 
config("test.client.id"), TEST_CLIENT_SECRET);
+    }
+
+    @Test
+    @Order(2)
+    public void testPropertyTokenWorks() {
+        given()
+                .when()
+                .queryParam("propertyToken", normalUserToken)
+                .get("/keycloak/secure-policy/user-with-token-in-property")
+                .then()
+                .statusCode(200)
+                .body(is("Access granted - secure default"));
+    }
+
+    @Test
+    @Order(3)
+    public void testHeaderTokenWorks() {
+        given()
+                .when()
+                .queryParam("headerToken", normalUserToken)
+                .get("/keycloak/secure-policy/user-with-token-in-header")
+                .then()
+                .statusCode(200)
+                .body(is("Access granted - secure default"));
+    }
+
+    @Test
+    @Order(4)
+    public void testPropertyPreferredOverHeaderTokenWorks() {
+        given()
+                .when()
+                .queryParam("propertyToken", normalUserToken)
+                .queryParam("headerToken", attackerToken)
+                
.get("/keycloak/secure-policy/user-with-token-in-property-and-header")
+                .then()
+                .statusCode(200)
+                .body(is("Access granted - secure default"));
+    }
+
+    @Test
+    @Order(5)
+    public void testInvalidHeaderIgnoredWhenPropertyValid() {
+        given()
+                .when()
+                .queryParam("propertyToken", normalUserToken)
+                .queryParam("headerToken", "invalid.token")
+                
.get("/keycloak/secure-policy/user-with-token-in-property-and-header")
+                .then()
+                .statusCode(200)
+                .body(is("Access granted - secure default"));
+    }
+
+    @Test
+    @Order(6)
+    public void testHeaderRejectedWhenHeadersDisabled() {
+        given()
+                .when()
+                .queryParam("headerToken", normalUserToken)
+                .get("/keycloak/secure-policy/max-security")
+                .then()
+                .statusCode(500)
+                .body(containsString("Access token not found in exchange"));
+    }
+
+    @Test
+    @Order(7)
+    public void testPropertyWorksWhenHeadersDisabled() {
+        given()
+                .when()
+                .queryParam("propertyToken", normalUserToken)
+                .get("/keycloak/secure-policy/max-security")
+                .then()
+                .statusCode(200)
+                .body(is("Access granted - max security"));
+    }
+
+    @Test
+    @Order(8)
+    public void testPropertyTokenUsedNotHeader() {
+        given()
+                .when()
+                .queryParam("propertyToken", normalUserToken)
+                .queryParam("headerToken", adminToken)
+                
.get("/keycloak/secure-policy/user-with-token-in-property-and-header")
+                .then()
+                .statusCode(200)
+                .body(is("Access granted - secure default"));
+    }
+
+    @Test
+    @Order(9)
+    public void testAttackScenario_SessionHijacking() {
+        given()
+                .when()
+                .queryParam("propertyToken", normalUserToken)
+                .queryParam("headerToken", attackerToken)
+                
.get("/keycloak/secure-policy/user-with-token-in-property-and-header")
+                .then()
+                .statusCode(200)
+                .body(is("Access granted - secure default"));
+    }
+
+    @Test
+    @Order(10)
+    public void testAttackScenario_LegacyUnsafe() {
+        given()
+                .when()
+                .queryParam("propertyToken", normalUserToken)
+                .queryParam("headerToken", attackerToken)
+                .get("/keycloak/secure-policy/legacy-unsafe")
+                .then()
+                .statusCode(500)
+                .body(containsString("Token mismatch detected"));
+    }
+
+    @Test
+    @Order(11)
+    public void testAuthorizationHeaderFormat() {
+        given()
+                .when()
+                .queryParam("headerToken", normalUserToken)
+                .get("/keycloak/secure-policy/authorization-header-format")
+                .then()
+                .statusCode(200)
+                .body(is("Access granted - secure default"));
+    }
+
+    @Test
+    @Order(12)
+    public void testNoTokenRejected() {
+        given()
+                .when()
+                .get("/keycloak/secure-policy/max-security")
+                .then()
+                .statusCode(500)
+                .body(containsString("Access token not found in exchange"));
+    }
+
+    @Test
+    @Order(13)
+    public void testAdminOnly() {
+        given()
+                .when()
+                .queryParam("propertyToken", adminToken)
+                .queryParam("headerToken", normalUserToken)
+                .get("/keycloak/secure-policy/admin-only")
+                .then()
+                .statusCode(200)
+                .body(is("Admin access granted"));
+    }
+
+    @Test
+    @Order(14)
+    public void testIntrospectionEnabledWithDefaultCacheConcurrentMap() {
+        given()
+                .when()
+                .queryParam("propertyToken", adminToken)
+                .queryParam("clientId", config("test.client.id"))
+                .queryParam("clientSecret", TEST_CLIENT_SECRET)
+                
.get("/keycloak/secure-policy/introspection-cache-concurrent-map")
+                .then()
+                .statusCode(200)
+                .body(is("Access granted - concurrent map cache"));
+    }
+
+    @Test
+    @Order(15)
+    public void testIntrospectionEnabledWithNoCache() {
+        given()
+                .when()
+                .queryParam("propertyToken", adminToken)
+                .queryParam("clientId", config("test.client.id"))
+                .queryParam("clientSecret", TEST_CLIENT_SECRET)
+                .get("/keycloak/secure-policy/introspection-no-cache")
+                .then()
+                .statusCode(200)
+                .body(is("Access granted - no cache"));
+    }
+
+    @Test
+    @Order(16)
+    public void testIntrospector_concurrentMapCache_tokenIsActive() {
+        given()
+                .queryParam("accessToken", normalUserToken)
+                .queryParam("clientId", config("test.client.id"))
+                .queryParam("clientSecret", TEST_CLIENT_SECRET)
+                
.get("/keycloak/introspection-cache/introspector/concurrent-map")
+                .then()
+                .statusCode(200)
+                .body("active", is(true))
+                .body("subject", notNullValue())
+                .body("cacheSize", greaterThan(0));
+    }
+
+    @Test
+    @Order(17)
+    public void 
testIntrospector_concurrentMapCache_invalidToken_returnsInactive() {
+        given()
+                .queryParam("accessToken", "invalid.token")
+                .queryParam("clientId", config("test.client.id"))
+                .queryParam("clientSecret", TEST_CLIENT_SECRET)
+                
.get("/keycloak/introspection-cache/introspector/concurrent-map")
+                .then()
+                .statusCode(200)
+                .body("active", is(false));
+    }
+
+    @Test
+    @Order(18)
+    public void testIntrospector_caffeineCache_tokenIsActive() {
+        given()
+                .queryParam("accessToken", normalUserToken)
+                .queryParam("clientId", config("test.client.id"))
+                .queryParam("clientSecret", TEST_CLIENT_SECRET)
+                .get("/keycloak/introspection-cache/introspector/caffeine")
+                .then()
+                .statusCode(200)
+                .body("active", is(true))
+                .body("subject", notNullValue())
+                .body("cacheSize", greaterThan(0));
+    }
+
+    @Test
+    @Order(19)
+    public void testIntrospector_caffeineCache_invalidToken_returnsInactive() {
+        given()
+                .queryParam("accessToken", "invalid.token")
+                .queryParam("clientId", config("test.client.id"))
+                .queryParam("clientSecret", TEST_CLIENT_SECRET)
+                .get("/keycloak/introspection-cache/introspector/caffeine")
+                .then()
+                .statusCode(200)
+                .body("active", is(false));
+    }
+
+    @Test
+    @Order(20)
+    public void testIntrospector_noCache_tokenIsActive() {
+        given()
+                .queryParam("accessToken", normalUserToken)
+                .queryParam("clientId", config("test.client.id"))
+                .queryParam("clientSecret", TEST_CLIENT_SECRET)
+                .get("/keycloak/introspection-cache/introspector/none")
+                .then()
+                .statusCode(200)
+                .body("active", is(true))
+                .body("subject", notNullValue())
+                .body("cacheSize", is(0));
+    }
+
+    @Test
+    @Order(21)
+    public void testIntrospector_noCache_invalidToken_returnsInactive() {
+        given()
+                .queryParam("accessToken", "invalid.token")
+                .queryParam("clientId", config("test.client.id"))
+                .queryParam("clientSecret", TEST_CLIENT_SECRET)
+                .get("/keycloak/introspection-cache/introspector/none")
+                .then()
+                .statusCode(200)
+                .body("active", is(false));
+    }
+
+    @Test
+    @Order(22)
+    public void testIntrospector_caffeineStats_secondCallHitsCache() {
+        given()
+                .queryParam("accessToken", normalUserToken)
+                .queryParam("clientId", config("test.client.id"))
+                .queryParam("clientSecret", TEST_CLIENT_SECRET)
+                
.get("/keycloak/introspection-cache/introspector/caffeine-stats")
+                .then()
+                .statusCode(200)
+                .body("hitCount", is(1))
+                .body("missCount", is(1))
+                .body("hitRate", is(0.5f))
+                .body("cacheSize", greaterThan(0));
+    }
+
+    @Test
+    @Order(23)
+    public void testIntrospector_noCache_invalidToken_returns500() {
+        given()
+                .queryParam("accessToken", "invalid.token")
+                .queryParam("clientId", config("test.client.id"))
+                .queryParam("clientSecret", "wrong.secret")
+                .get("/keycloak/introspection-cache/introspector/none")
+                .then()
+                .statusCode(500);
+    }
+
+    @Test
+    @Order(100)
+    public void testCleanup_DeleteRealm() {
+        KeycloakRealmLifecycle.deleteRealm(config("test.realm"));
+    }
+
+    protected void createRealm() {
+        KeycloakRealmLifecycle.createRealmWithSmtp(config("test.realm"));
+    }
+
+    protected void createClient() {
+        ClientRepresentation client = new ClientRepresentation();
+        client.setClientId(config("test.client.id"));
+        client.setSecret(TEST_CLIENT_SECRET);
+        client.setPublicClient(false);
+        client.setDirectAccessGrantsEnabled(true);
+        client.setStandardFlowEnabled(true);
+        client.setFullScopeAllowed(true);
+
+        given()
+                .contentType(ContentType.JSON)
+                .body(client)
+                .post("/keycloak/client/{realmName}/pojo", 
config("test.realm"))
+                .then()
+                .statusCode(201);
+    }
+
+    protected void createRole(String roleName) {
+        given()
+                .queryParam("description", "Test role for integration testing")
+                .when()
+                .post("/keycloak/role/{realmName}/{roleName}", 
config("test.realm"), roleName)
+                .then()
+                .statusCode(200);
+    }
+
+    protected void createUser(String username) {
+        UserRepresentation user = new UserRepresentation();
+        user.setUsername(username);
+        user.setEmail(username + "@test.com");
+        user.setFirstName(username);
+        user.setLastName("User");
+        user.setEnabled(true);
+
+        given()
+                .contentType(ContentType.JSON)
+                .body(List.of(user))
+                .when()
+                .post("/keycloak/user/{realmName}", config("test.realm"))
+                .then()
+                .statusCode(200);
+    }
+
+    private void resetPassword(String username, String password) {
+        given()
+                .queryParam("password", password)
+                .queryParam("temporary", false)
+                .when()
+                .post("/keycloak/user/{realmName}/{username}/reset-password", 
config("test.realm"), username)
+                .then()
+                .statusCode(200);
+    }
+
+    protected void assignRole(String username, String role) {
+        given()
+                .when()
+                .post("/keycloak/user-role/{realmName}/{username}/{roleName}",
+                        config("test.realm"), username, role)
+                .then()
+                .statusCode(200);
+    }
+}
diff --git 
a/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakSecurityPolicyTestBase.java
 
b/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakSecurityPolicyTestBase.java
new file mode 100644
index 0000000000..c8f682bd2c
--- /dev/null
+++ 
b/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakSecurityPolicyTestBase.java
@@ -0,0 +1,36 @@
+/*
+ * 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.quarkus.component.keycloak.it;
+
+import java.util.UUID;
+
+import io.quarkus.test.common.QuarkusTestResource;
+
+@QuarkusTestResource(KeycloakTestResource.class)
+public class KeycloakSecurityPolicyTestBase extends KeycloakTestBase {
+    // Test users
+    protected static final String ADMIN_USER = "admin-" + 
UUID.randomUUID().toString().substring(0, 8);
+    protected static final String ADMIN_PASSWORD = "admin123";
+    protected static final String NORMAL_USER = "user-" + 
UUID.randomUUID().toString().substring(0, 8);
+    protected static final String NORMAL_PASSWORD = "user123";
+    protected static final String ATTACKER_USER = "attacker-" + 
UUID.randomUUID().toString().substring(0, 8);
+    protected static final String ATTACKER_PASSWORD = "attacker123";
+
+    // Test roles
+    protected static final String ADMIN_ROLE = "admin";
+    protected static final String USER_ROLE = "user";
+}
diff --git 
a/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakTestBase.java
 
b/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakTestBase.java
index b3e228d5b8..9d8660b7ac 100644
--- 
a/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakTestBase.java
+++ 
b/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakTestBase.java
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.quarkus.component.keycloak.it;
 
+import java.util.Map;
 import java.util.UUID;
 
 import com.fasterxml.jackson.databind.DeserializationFeature;
@@ -24,6 +25,13 @@ import io.quarkus.test.common.QuarkusTestResource;
 import io.restassured.RestAssured;
 import io.restassured.config.ObjectMapperConfig;
 import io.restassured.config.RestAssuredConfig;
+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.eclipse.microprofile.config.ConfigProvider;
 import org.junit.jupiter.api.BeforeAll;
 
 /**
@@ -36,9 +44,11 @@ public abstract class KeycloakTestBase {
     // Test data - use unique names to avoid conflicts
     protected static final String TEST_REALM_NAME = "test-realm-" + 
UUID.randomUUID().toString().substring(0, 8);
     protected static final String TEST_USER_NAME = "test-user-" + 
UUID.randomUUID().toString().substring(0, 8);
+    protected static final String TEST_USER_PASSWORD = "Test@password123";
     protected static final String TEST_ROLE_NAME = "test-role-" + 
UUID.randomUUID().toString().substring(0, 8);
     protected static final String TEST_GROUP_NAME = "test-group-" + 
UUID.randomUUID().toString().substring(0, 8);
     protected static final String TEST_CLIENT_ID = "test-client-" + 
UUID.randomUUID().toString().substring(0, 8);
+    protected static final String TEST_CLIENT_SECRET = "test-client-secret";
     protected static final String TEST_CLIENT_ROLE_NAME = "test-client-role-"
             + UUID.randomUUID().toString().substring(0, 8);
     protected static final String TEST_CLIENT_SCOPE_NAME = "test-scope-" + 
UUID.randomUUID().toString().substring(0, 8);
@@ -62,4 +72,36 @@ public abstract class KeycloakTestBase {
                             return mapper;
                         }));
     }
+
+    protected String config(String name) {
+        return ConfigProvider.getConfig().getValue(name, String.class);
+    }
+
+    protected String getAccessToken(String username, String password,
+            String clientId, String clientSecret) {
+        try (Client client = ClientBuilder.newClient()) {
+            String tokenUrl = 
String.format("%s/realms/%s/protocol/openid-connect/token",
+                    config("keycloak.url"), config("test.realm"));
+
+            Form form = new Form()
+                    .param("grant_type", "password")
+                    .param("client_id", clientId)
+                    .param("client_secret", clientSecret)
+                    .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> body = response.readEntity(Map.class);
+                    return (String) body.get("access_token");
+                }
+                throw new RuntimeException("Failed to get token for " + 
username
+                        + " [" + response.getStatus() + "]: " + 
response.readEntity(String.class));
+            }
+        }
+    }
 }
diff --git 
a/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakTestResource.java
 
b/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakTestResource.java
index e043482905..d65de24760 100644
--- 
a/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakTestResource.java
+++ 
b/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakTestResource.java
@@ -19,6 +19,7 @@ package org.apache.camel.quarkus.component.keycloak.it;
 import java.time.Duration;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.UUID;
 
 import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
 import io.quarkus.test.keycloak.server.KeycloakContainer;
@@ -70,6 +71,9 @@ public class KeycloakTestResource implements 
QuarkusTestResourceLifecycleManager
         properties.put("keycloak.username", "admin");
         properties.put("keycloak.password", "admin");
         properties.put("keycloak.realm", "master");
+        properties.put("test.client.secret", "test-client-secret");
+        properties.put("test.client.id", "token-binding-client-" + 
UUID.randomUUID().toString().substring(0, 8));
+        properties.put("test.realm", "token-binding-realm-" + 
UUID.randomUUID().toString().substring(0, 8));
 
         // GreenMail SMTP configuration (accessible from host)
         properties.put("mail.smtp.host", greenMail.getHost());
diff --git 
a/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakUserTest.java
 
b/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakUserTest.java
index 92fd49e6c9..5c1e1e7bae 100644
--- 
a/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakUserTest.java
+++ 
b/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakUserTest.java
@@ -16,22 +16,29 @@
  */
 package org.apache.camel.quarkus.component.keycloak.it;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 import io.quarkus.test.common.QuarkusTestResource;
 import io.quarkus.test.junit.QuarkusTest;
 import io.restassured.common.mapper.TypeRef;
 import io.restassured.http.ContentType;
-import org.junit.jupiter.api.*;
+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.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
 
 import static io.restassured.RestAssured.given;
+import static org.hamcrest.CoreMatchers.hasItem;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.notNullValue;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.hasItems;
 
 @QuarkusTest
 @QuarkusTestResource(KeycloakTestResource.class)
@@ -391,6 +398,161 @@ class KeycloakUserTest extends KeycloakTestBase {
                 .body(is("Actions email sent successfully"));
     }
 
+    @Test
+    @Order(21)
+    public void testBulkCreateUsers() {
+        String bulkUserNameOne = TEST_USER_NAME + "-bulkOne";
+
+        UserRepresentation userOne = new UserRepresentation();
+        userOne.setUsername(bulkUserNameOne);
+        userOne.setEmail(bulkUserNameOne + "@test.com");
+        userOne.setFirstName("Test One");
+        userOne.setLastName("User Bulk One");
+        userOne.setEnabled(true);
+
+        String bulkUserNameTwo = TEST_USER_NAME + "-bulkTwo";
+
+        UserRepresentation userTwo = new UserRepresentation();
+        userTwo.setUsername(bulkUserNameTwo);
+        userTwo.setEmail(bulkUserNameTwo + "@test.com");
+        userTwo.setFirstName("Test Two");
+        userTwo.setLastName("User Bulk Two");
+        userTwo.setEnabled(true);
+
+        List<UserRepresentation> users = new ArrayList<>();
+        users.add(userOne);
+        users.add(userTwo);
+
+        given()
+                .contentType(ContentType.JSON)
+                .body(users)
+                .when()
+                .post("/keycloak/user/{realmName}", TEST_REALM_NAME)
+                .then()
+                .statusCode(200)
+                .body("total", is(2))
+                .body("success", is(2))
+                .body("results.username", hasItems(bulkUserNameOne, 
bulkUserNameTwo))
+                .body("results.status", hasItem("success"));
+    }
+
+    @Test
+    @Order(22)
+    public void testBulkUpdateUsers() {
+        //First get list of users
+        List<UserRepresentation> batchCreatedUsers = given()
+                .when()
+                .get("/keycloak/user/{realmName}", TEST_REALM_NAME)
+                .then()
+                .statusCode(200)
+                .contentType(ContentType.JSON)
+                .extract()
+                .body()
+                .jsonPath()
+                .getList(".", UserRepresentation.class).stream().filter(user 
-> user.getUsername().contains("bulk")).toList();
+
+        //update firstname and lastname of each user
+        batchCreatedUsers.forEach(user -> {
+            user.setFirstName("updatedFirstNameForBulkCreatedUsers");
+            user.setLastName("updatedLastNameForBulkCreatedUsers");
+        });
+
+        //bulk update users
+        given()
+                .contentType(ContentType.JSON)
+                .body(batchCreatedUsers)
+                .when()
+                .put("/keycloak/user/{realmName}", TEST_REALM_NAME)
+                .then()
+                .statusCode(200)
+                .body("total", is(batchCreatedUsers.size()))
+                .body("success", is(batchCreatedUsers.size()));
+
+        //verify updated result
+        List<UserRepresentation> updatedUsers = given()
+                .when()
+                .get("/keycloak/user/{realmName}", TEST_REALM_NAME)
+                .then()
+                .statusCode(200)
+                .contentType(ContentType.JSON)
+                .extract()
+                .body()
+                .jsonPath()
+                .getList(".", UserRepresentation.class).stream().filter(user 
-> user.getUsername().contains("bulk")).toList();
+
+        Map<String, UserRepresentation> updatedUserMap = updatedUsers.stream()
+                .collect(Collectors.toMap(UserRepresentation::getUsername, u 
-> u));
+
+        for (UserRepresentation user : batchCreatedUsers) {
+            UserRepresentation updatedUser = 
updatedUserMap.get(user.getUsername());
+            assertThat(updatedUser, notNullValue());
+            assertThat(updatedUser.getFirstName(), 
is("updatedFirstNameForBulkCreatedUsers"));
+            assertThat(updatedUser.getLastName(), 
is("updatedLastNameForBulkCreatedUsers"));
+        }
+    }
+
+    @Test
+    @Order(23)
+    public void testBulkUpdateUsersWithContinueOnError() {
+        //First get list of users
+        List<UserRepresentation> batchCreatedUsers = new ArrayList<>(given()
+                .when()
+                .get("/keycloak/user/{realmName}", TEST_REALM_NAME)
+                .then()
+                .statusCode(200)
+                .contentType(ContentType.JSON)
+                .extract()
+                .body()
+                .jsonPath()
+                .getList(".", UserRepresentation.class).stream().filter(user 
-> user.getUsername().contains("bulk")).toList());
+
+        //add a wrong user at the beginning, even failed to handle but still 
keep processing
+        batchCreatedUsers.add(0, new UserRepresentation());
+
+        //bulk update users
+        given()
+                .contentType(ContentType.JSON)
+                .body(batchCreatedUsers)
+                .header("continueOnError", true)
+                .when()
+                .put("/keycloak/user/{realmName}", TEST_REALM_NAME)
+                .then()
+                .statusCode(200)
+                .body("total", is(batchCreatedUsers.size()))
+                .body("success", is(2))
+                .body("failed", is(1));
+    }
+
+    @Test
+    @Order(24)
+    public void testBulkDeleteUsers() {
+        // First get list of users
+        List<String> batchCreatedUsers = given()
+                .when()
+                .get("/keycloak/user/{realmName}", TEST_REALM_NAME)
+                .then()
+                .statusCode(200)
+                .contentType(ContentType.JSON)
+                .extract()
+                .body()
+                .jsonPath()
+                .getList(".", 
UserRepresentation.class).stream().map(UserRepresentation::getUsername)
+                .filter(username -> username.contains("bulk")).toList();
+
+        // Bulk delete users
+        given()
+                .when()
+                .contentType(ContentType.JSON)
+                .body(batchCreatedUsers)
+                .delete("/keycloak/user/{realmName}", TEST_REALM_NAME)
+                .then()
+                .statusCode(200)
+                .body("total", is(batchCreatedUsers.size()))
+                .body("success", is(batchCreatedUsers.size()))
+                .body("results.size()", is(batchCreatedUsers.size()));
+
+    }
+
     @Test
     @Order(100)
     public void testCleanupUsers() {

Reply via email to