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

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


The following commit(s) were added to refs/heads/main by this push:
     new ce5adc2bff23 CAMEL-22582 - Camel-Keycloak: Keycloak Introspector 
should provide di… (#19662)
ce5adc2bff23 is described below

commit ce5adc2bff2383a19630f942532c04f86c6e70da
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed Oct 22 11:08:46 2025 +0200

    CAMEL-22582 - Camel-Keycloak: Keycloak Introspector should provide di… 
(#19662)
    
    * CAMEL-22582 - Camel-Keycloak: Keycloak Introspector should provide 
different pluggable cache support
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * CAMEL-22582 - Camel-Keycloak: Keycloak Introspector should provide 
different pluggable cache support
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    ---------
    
    Signed-off-by: Andrea Cosentino <[email protected]>
---
 components/camel-keycloak/pom.xml                  |   4 +
 .../src/main/docs/keycloak-component.adoc          | 217 +++++++++++++++++++++
 .../security/KeycloakTokenIntrospector.java        | 131 ++++++++-----
 .../security/cache/CaffeineTokenCache.java         | 129 ++++++++++++
 .../security/cache/ConcurrentMapTokenCache.java    | 133 +++++++++++++
 .../keycloak/security/cache/TokenCache.java        | 117 +++++++++++
 .../keycloak/security/cache/TokenCacheFactory.java | 126 ++++++++++++
 .../keycloak/security/cache/TokenCacheType.java    |  41 ++++
 .../security/KeycloakTokenIntrospectionIT.java     | 208 ++++++++++++++++++++
 .../security/cache/CaffeineTokenCacheTest.java     | 202 +++++++++++++++++++
 .../cache/ConcurrentMapTokenCacheTest.java         | 175 +++++++++++++++++
 .../security/cache/TokenCacheFactoryTest.java      |  98 ++++++++++
 12 files changed, 1530 insertions(+), 51 deletions(-)

diff --git a/components/camel-keycloak/pom.xml 
b/components/camel-keycloak/pom.xml
index a8f52cc64316..611ff46f7519 100644
--- a/components/camel-keycloak/pom.xml
+++ b/components/camel-keycloak/pom.xml
@@ -64,6 +64,10 @@
             <artifactId>httpclient5</artifactId>
             <version>${httpclient-version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-caffeine</artifactId>
+        </dependency>
 
         <!-- test dependencies -->
         <dependency>
diff --git a/components/camel-keycloak/src/main/docs/keycloak-component.adoc 
b/components/camel-keycloak/src/main/docs/keycloak-component.adoc
index 3a82e07540fc..6de8ae48707a 100644
--- a/components/camel-keycloak/src/main/docs/keycloak-component.adoc
+++ b/components/camel-keycloak/src/main/docs/keycloak-component.adoc
@@ -3021,6 +3021,223 @@ policy.setIntrospectionCacheTtl(30); // 30 seconds
 
 NOTE: When a token is introspected and cached, subsequent requests with the 
same token will use the cached result until the TTL expires. Balance security 
requirements with performance needs.
 
+==== Pluggable Cache Implementation
+
+The token introspection feature supports pluggable cache implementations for 
flexible caching strategies. This allows you to choose the best caching 
solution for your performance and scalability requirements.
+
+===== Available Cache Types
+
+The component provides three cache implementations:
+
+1. **ConcurrentMap Cache** (default) - Simple in-memory cache using 
`ConcurrentHashMap`
+   - Time-based expiration (TTL)
+   - No external dependencies
+   - Suitable for basic use cases and backward compatibility
+
+2. **Caffeine Cache** (recommended for production) - High-performance cache 
with advanced features
+   - Time-based expiration
+   - Size-based eviction
+   - Detailed statistics (hit rate, miss rate, evictions)
+   - Optimized for high throughput
+
+3. **No Cache** - Disables caching completely
+   - Every token is introspected on each request
+   - Useful for testing or strict security requirements
+
+===== Using Caffeine Cache
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+import org.apache.camel.component.keycloak.security.cache.TokenCacheType;
+
+// Create introspector with Caffeine cache for high performance
+KeycloakTokenIntrospector introspector = new KeycloakTokenIntrospector(
+    serverUrl,
+    realm,
+    clientId,
+    clientSecret,
+    TokenCacheType.CAFFEINE,
+    300,         // TTL in seconds (5 minutes)
+    10000,       // max cache size (0 for unlimited)
+    true         // record statistics
+);
+
+// Check cache statistics
+TokenCache.CacheStats stats = introspector.getCacheStats();
+if (stats != null) {
+    System.out.println("Cache hit rate: " + stats.getHitRate());
+    System.out.println("Total hits: " + stats.getHitCount());
+    System.out.println("Total misses: " + stats.getMissCount());
+    System.out.println("Evictions: " + stats.getEvictionCount());
+}
+
+// Get current cache size
+long size = introspector.getCacheSize();
+System.out.println("Cached tokens: " + size);
+----
+====
+
+===== Custom Cache Configuration
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+import org.apache.camel.component.keycloak.security.cache.TokenCache;
+import org.apache.camel.component.keycloak.security.cache.CaffeineTokenCache;
+
+// Create a custom cache with specific settings
+TokenCache customCache = new CaffeineTokenCache(
+    600,         // 10 minutes TTL
+    50000,       // max 50k entries
+    true         // enable stats
+);
+
+// Use the custom cache instance
+KeycloakTokenIntrospector introspector = new KeycloakTokenIntrospector(
+    serverUrl,
+    realm,
+    clientId,
+    clientSecret,
+    customCache
+);
+----
+====
+
+===== Cache Configuration in Security Policy
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+// High-performance policy with Caffeine cache
+KeycloakSecurityPolicy highPerfPolicy = new KeycloakSecurityPolicy();
+highPerfPolicy.setServerUrl("http://localhost:8080";);
+highPerfPolicy.setRealm("production-realm");
+highPerfPolicy.setClientId("api-service");
+highPerfPolicy.setClientSecret("api-secret");
+highPerfPolicy.setRequiredRoles("user");
+
+// Enable introspection with Caffeine cache
+highPerfPolicy.setUseTokenIntrospection(true);
+highPerfPolicy.setIntrospectionCacheType(TokenCacheType.CAFFEINE);
+highPerfPolicy.setIntrospectionCacheTtl(120);  // 2 minutes
+highPerfPolicy.setIntrospectionCacheMaxSize(5000);  // max 5k tokens
+highPerfPolicy.setIntrospectionCacheStats(true);  // enable statistics
+
+from("rest:get:/api/data")
+    .policy(highPerfPolicy)
+    .to("bean:dataService?method=getData");
+----
+
+YAML::
++
+[source,yaml]
+----
+# Bean definition with Caffeine cache
+beans:
+  - name: highPerfPolicy
+    type: org.apache.camel.component.keycloak.security.KeycloakSecurityPolicy
+    properties:
+      serverUrl: "http://localhost:8080";
+      realm: "production-realm"
+      clientId: "api-service"
+      clientSecret: "api-secret"
+      requiredRoles: "user"
+      useTokenIntrospection: true
+      introspectionCacheType: "CAFFEINE"
+      introspectionCacheTtl: 120
+      introspectionCacheMaxSize: 5000
+      introspectionCacheStats: true
+----
+====
+
+===== Cache Performance Monitoring
+
+For production systems, monitor cache performance to optimize TTL and size 
settings:
+
+[source,java]
+----
+// Periodically check cache performance
+TokenCache.CacheStats stats = introspector.getCacheStats();
+
+if (stats != null) {
+    logger.info("Token cache performance: " + stats.toString());
+    // Output: CacheStats{hits=1000, misses=100, hitRate=90.91%, evictions=50}
+
+    // Alert if hit rate is too low
+    if (stats.getHitRate() < 0.7) {
+        logger.warn("Low cache hit rate: " + stats.getHitRate());
+        logger.warn("Consider increasing cache TTL or size");
+    }
+
+    // Monitor evictions
+    if (stats.getEvictionCount() > threshold) {
+        logger.info("High eviction count: " + stats.getEvictionCount());
+        logger.info("Consider increasing cache max size");
+    }
+}
+----
+
+===== Cache Selection Guidelines
+
+[width="100%",cols="30%,35%,35%",options="header"]
+|===
+| Use Case | Recommended Cache | Configuration
+
+| **Development/Testing** | ConcurrentMap | TTL: 60s, Default settings
+| **Production (Low-Medium Traffic)** | ConcurrentMap | TTL: 120s, Monitor size
+| **Production (High Traffic)** | Caffeine | TTL: 300s, maxSize: 10000+, stats 
enabled
+| **Very High Traffic** | Caffeine | TTL: 300s, maxSize: 50000+, stats enabled
+| **Strict Security** | NONE or low TTL | TTL: 30s or disable caching
+| **Testing/Debugging** | NONE | Disable caching for fresh validation
+|===
+
+===== Best Practices
+
+1. **TTL Configuration**: Set TTL slightly shorter than your token expiration 
time to balance security and performance
+
+2. **Cache Size**: For Caffeine, set `maxSize` based on expected concurrent 
users:
+   - Small deployments (< 1000 users): 1000-5000 entries
+   - Medium deployments (1000-10000 users): 10000-25000 entries
+   - Large deployments (> 10000 users): 50000+ entries
+
+3. **Statistics**: Enable statistics in production environments for monitoring 
and tuning
+
+4. **Cleanup**: Always call `introspector.close()` when shutting down to 
release resources properly
+
+5. **Different Policies for Different Endpoints**: Use Caffeine cache for 
high-traffic endpoints and stricter settings for security-critical endpoints
+
+[source,java]
+----
+// High-traffic endpoint - use Caffeine with longer TTL
+KeycloakSecurityPolicy highTrafficPolicy = new KeycloakSecurityPolicy();
+highTrafficPolicy.setUseTokenIntrospection(true);
+highTrafficPolicy.setIntrospectionCacheType(TokenCacheType.CAFFEINE);
+highTrafficPolicy.setIntrospectionCacheTtl(300); // 5 minutes
+
+// Security-critical endpoint - shorter TTL or no cache
+KeycloakSecurityPolicy criticalPolicy = new KeycloakSecurityPolicy();
+criticalPolicy.setUseTokenIntrospection(true);
+criticalPolicy.setIntrospectionCacheType(TokenCacheType.NONE); // No caching
+
+from("rest:get:/api/users")
+    .policy(highTrafficPolicy)  // Cached validation
+    .to("bean:userService");
+
+from("rest:post:/payments")
+    .policy(criticalPolicy)  // Real-time validation
+    .to("bean:paymentService");
+----
+
 ==== Token Revocation Workflow
 
 When using token introspection, you can implement a complete token revocation 
workflow:
diff --git 
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakTokenIntrospector.java
 
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakTokenIntrospector.java
index da1696fd1346..25f473c1fac2 100644
--- 
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakTokenIntrospector.java
+++ 
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakTokenIntrospector.java
@@ -19,9 +19,11 @@ package org.apache.camel.component.keycloak.security;
 import java.io.IOException;
 import java.util.Base64;
 import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.camel.component.keycloak.security.cache.TokenCache;
+import org.apache.camel.component.keycloak.security.cache.TokenCacheFactory;
+import org.apache.camel.component.keycloak.security.cache.TokenCacheType;
 import org.apache.camel.util.ObjectHelper;
 import org.apache.hc.client5.http.classic.methods.HttpPost;
 import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
@@ -37,7 +39,7 @@ import org.slf4j.LoggerFactory;
 /**
  * Token introspection client for Keycloak OAuth 2.0 Token Introspection (RFC 
7662). This class handles communication
  * with Keycloak's introspection endpoint to validate tokens in real-time, 
including detecting revoked tokens before
- * their expiration.
+ * their expiration. Supports pluggable cache implementations for flexible 
caching strategies.
  */
 public class KeycloakTokenIntrospector {
     private static final Logger LOG = 
LoggerFactory.getLogger(KeycloakTokenIntrospector.class);
@@ -48,21 +50,62 @@ public class KeycloakTokenIntrospector {
     private final String clientId;
     private final String clientSecret;
     private final CloseableHttpClient httpClient;
-    private final Map<String, CachedIntrospectionResult> cache;
-    private final boolean cacheEnabled;
-    private final long cacheTtlMillis;
+    private final TokenCache cache;
 
+    /**
+     * Creates a new token introspector with the specified cache configuration.
+     *
+     * @param serverUrl       the Keycloak server URL
+     * @param realm           the realm name
+     * @param clientId        the client ID
+     * @param clientSecret    the client secret
+     * @param cacheEnabled    whether to enable caching
+     * @param cacheTtlSeconds cache time-to-live in seconds
+     */
     public KeycloakTokenIntrospector(
                                      String serverUrl, String realm, String 
clientId, String clientSecret,
                                      boolean cacheEnabled, long 
cacheTtlSeconds) {
+        this(serverUrl, realm, clientId, clientSecret,
+             cacheEnabled ? TokenCacheFactory.createCache(cacheTtlSeconds) : 
null);
+    }
+
+    /**
+     * Creates a new token introspector with advanced cache configuration.
+     *
+     * @param serverUrl       the Keycloak server URL
+     * @param realm           the realm name
+     * @param clientId        the client ID
+     * @param clientSecret    the client secret
+     * @param cacheType       the type of cache to use
+     * @param cacheTtlSeconds cache time-to-live in seconds
+     * @param maxCacheSize    maximum cache size (only for CAFFEINE type, 0 
for unlimited)
+     * @param recordStats     whether to record cache statistics (only for 
CAFFEINE type)
+     */
+    public KeycloakTokenIntrospector(
+                                     String serverUrl, String realm, String 
clientId, String clientSecret,
+                                     TokenCacheType cacheType, long 
cacheTtlSeconds, long maxCacheSize, boolean recordStats) {
+        this(serverUrl, realm, clientId, clientSecret,
+             TokenCacheFactory.createCache(cacheType, cacheTtlSeconds, 
maxCacheSize, recordStats));
+    }
+
+    /**
+     * Creates a new token introspector with a custom cache implementation.
+     *
+     * @param serverUrl    the Keycloak server URL
+     * @param realm        the realm name
+     * @param clientId     the client ID
+     * @param clientSecret the client secret
+     * @param cache        the cache implementation to use (null to disable 
caching)
+     */
+    public KeycloakTokenIntrospector(
+                                     String serverUrl, String realm, String 
clientId, String clientSecret,
+                                     TokenCache cache) {
         this.serverUrl = serverUrl;
         this.realm = realm;
         this.clientId = clientId;
         this.clientSecret = clientSecret;
         this.httpClient = HttpClients.createDefault();
-        this.cacheEnabled = cacheEnabled;
-        this.cacheTtlMillis = cacheTtlSeconds * 1000;
-        this.cache = cacheEnabled ? new ConcurrentHashMap<>() : null;
+        this.cache = cache;
     }
 
     /**
@@ -78,13 +121,11 @@ public class KeycloakTokenIntrospector {
         }
 
         // Check cache first
-        if (cacheEnabled) {
-            CachedIntrospectionResult cached = cache.get(token);
-            if (cached != null && !cached.isExpired()) {
+        if (cache != null) {
+            IntrospectionResult cached = cache.get(token);
+            if (cached != null) {
                 LOG.debug("Returning cached introspection result for token");
-                return cached.result;
-            } else if (cached != null) {
-                cache.remove(token);
+                return cached;
             }
         }
 
@@ -113,9 +154,8 @@ public class KeycloakTokenIntrospector {
             });
 
             // Cache the result
-            if (cacheEnabled && result != null) {
-                cache.put(token, new CachedIntrospectionResult(result, 
cacheTtlMillis));
-                cleanupExpiredCacheEntries();
+            if (cache != null && result != null) {
+                cache.put(token, result);
             }
 
             return result;
@@ -148,33 +188,36 @@ public class KeycloakTokenIntrospector {
         }
     }
 
-    private void cleanupExpiredCacheEntries() {
-        if (!cacheEnabled || cache.isEmpty()) {
-            return;
-        }
-
-        // Cleanup expired entries (max 100 at a time to avoid performance 
issues)
-        cache.entrySet().removeIf(entry -> {
-            if (entry.getValue().isExpired()) {
-                LOG.trace("Removing expired cache entry");
-                return true;
-            }
-            return false;
-        });
-    }
-
     /**
      * Clears the introspection cache.
      */
     public void clearCache() {
-        if (cacheEnabled) {
+        if (cache != null) {
             cache.clear();
             LOG.debug("Introspection cache cleared");
         }
     }
 
     /**
-     * Closes the HTTP client.
+     * Returns cache statistics if available.
+     *
+     * @return cache statistics, or null if not available
+     */
+    public TokenCache.CacheStats getCacheStats() {
+        return cache != null ? cache.getStats() : null;
+    }
+
+    /**
+     * Returns the current cache size.
+     *
+     * @return the number of entries in the cache, or 0 if caching is disabled
+     */
+    public long getCacheSize() {
+        return cache != null ? cache.size() : 0;
+    }
+
+    /**
+     * Closes the HTTP client and cache.
      */
     public void close() {
         try {
@@ -184,6 +227,9 @@ public class KeycloakTokenIntrospector {
         } catch (IOException e) {
             LOG.warn("Error closing HTTP client", e);
         }
+        if (cache != null) {
+            cache.close();
+        }
     }
 
     /**
@@ -296,21 +342,4 @@ public class KeycloakTokenIntrospector {
             return claims.get(claimName);
         }
     }
-
-    /**
-     * Internal class to hold cached introspection results with expiration.
-     */
-    private static class CachedIntrospectionResult {
-        private final IntrospectionResult result;
-        private final long expirationTime;
-
-        public CachedIntrospectionResult(IntrospectionResult result, long 
ttlMillis) {
-            this.result = result;
-            this.expirationTime = System.currentTimeMillis() + ttlMillis;
-        }
-
-        public boolean isExpired() {
-            return System.currentTimeMillis() >= expirationTime;
-        }
-    }
 }
diff --git 
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/cache/CaffeineTokenCache.java
 
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/cache/CaffeineTokenCache.java
new file mode 100644
index 000000000000..b735fdc92ca8
--- /dev/null
+++ 
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/cache/CaffeineTokenCache.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.keycloak.security.cache;
+
+import java.util.concurrent.TimeUnit;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import org.apache.camel.component.keycloak.security.KeycloakTokenIntrospector;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * High-performance token cache implementation using Caffeine. This cache 
provides advanced features including automatic
+ * expiration, size-based eviction, and detailed statistics. This 
implementation is recommended for production use with
+ * high throughput requirements.
+ */
+public class CaffeineTokenCache implements TokenCache {
+    private static final Logger LOG = 
LoggerFactory.getLogger(CaffeineTokenCache.class);
+
+    private final Cache<String, KeycloakTokenIntrospector.IntrospectionResult> 
cache;
+
+    /**
+     * Creates a new Caffeine-based token cache with the specified 
configuration.
+     *
+     * @param ttlSeconds  time-to-live for cache entries in seconds
+     * @param maxSize     maximum number of entries to cache (0 or negative 
for unlimited)
+     * @param recordStats whether to record cache statistics
+     */
+    public CaffeineTokenCache(long ttlSeconds, long maxSize, boolean 
recordStats) {
+        Caffeine<Object, Object> builder = Caffeine.newBuilder()
+                .expireAfterWrite(ttlSeconds, TimeUnit.SECONDS);
+
+        if (maxSize > 0) {
+            builder.maximumSize(maxSize);
+        }
+
+        if (recordStats) {
+            builder.recordStats();
+        }
+
+        this.cache = builder.build();
+        LOG.debug("Initialized Caffeine token cache with TTL={}s, maxSize={}, 
stats={}",
+                ttlSeconds, maxSize > 0 ? maxSize : "unlimited", recordStats);
+    }
+
+    /**
+     * Creates a new Caffeine-based token cache with default settings (no size 
limit, stats enabled).
+     *
+     * @param ttlSeconds time-to-live for cache entries in seconds
+     */
+    public CaffeineTokenCache(long ttlSeconds) {
+        this(ttlSeconds, 0, true);
+    }
+
+    @Override
+    public KeycloakTokenIntrospector.IntrospectionResult get(String token) {
+        KeycloakTokenIntrospector.IntrospectionResult result = 
cache.getIfPresent(token);
+        if (result != null) {
+            LOG.trace("Cache hit for token");
+        } else {
+            LOG.trace("Cache miss for token");
+        }
+        return result;
+    }
+
+    @Override
+    public void put(String token, 
KeycloakTokenIntrospector.IntrospectionResult result) {
+        cache.put(token, result);
+        LOG.trace("Token introspection result cached");
+    }
+
+    @Override
+    public void remove(String token) {
+        cache.invalidate(token);
+        LOG.trace("Token removed from cache");
+    }
+
+    @Override
+    public void clear() {
+        cache.invalidateAll();
+        LOG.debug("Cache cleared");
+    }
+
+    @Override
+    public long size() {
+        return cache.estimatedSize();
+    }
+
+    @Override
+    public void close() {
+        cache.invalidateAll();
+        cache.cleanUp();
+        LOG.debug("Cache closed and cleaned up");
+    }
+
+    @Override
+    public CacheStats getStats() {
+        com.github.benmanes.caffeine.cache.stats.CacheStats caffeineStats = 
cache.stats();
+
+        return new CacheStats(
+                caffeineStats.hitCount(),
+                caffeineStats.missCount(),
+                caffeineStats.evictionCount());
+    }
+
+    /**
+     * Returns the underlying Caffeine cache for advanced usage.
+     *
+     * @return the Caffeine cache instance
+     */
+    public Cache<String, KeycloakTokenIntrospector.IntrospectionResult> 
getCaffeineCache() {
+        return cache;
+    }
+}
diff --git 
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/cache/ConcurrentMapTokenCache.java
 
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/cache/ConcurrentMapTokenCache.java
new file mode 100644
index 000000000000..aa2ebb06ec4b
--- /dev/null
+++ 
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/cache/ConcurrentMapTokenCache.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.keycloak.security.cache;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.camel.component.keycloak.security.KeycloakTokenIntrospector;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Simple in-memory token cache implementation using ConcurrentHashMap with 
time-based expiration. This is the default
+ * cache implementation that provides basic caching without external 
dependencies.
+ */
+public class ConcurrentMapTokenCache implements TokenCache {
+    private static final Logger LOG = 
LoggerFactory.getLogger(ConcurrentMapTokenCache.class);
+
+    private final Map<String, CachedEntry> cache;
+    private final long ttlMillis;
+    private final AtomicLong hitCount = new AtomicLong(0);
+    private final AtomicLong missCount = new AtomicLong(0);
+    private final AtomicLong evictionCount = new AtomicLong(0);
+
+    public ConcurrentMapTokenCache(long ttlSeconds) {
+        this.cache = new ConcurrentHashMap<>();
+        this.ttlMillis = ttlSeconds * 1000;
+    }
+
+    @Override
+    public KeycloakTokenIntrospector.IntrospectionResult get(String token) {
+        CachedEntry entry = cache.get(token);
+        if (entry != null) {
+            if (!entry.isExpired()) {
+                hitCount.incrementAndGet();
+                LOG.trace("Cache hit for token");
+                return entry.result;
+            } else {
+                // Remove expired entry
+                cache.remove(token);
+                evictionCount.incrementAndGet();
+                LOG.trace("Cache entry expired and removed");
+            }
+        }
+        missCount.incrementAndGet();
+        LOG.trace("Cache miss for token");
+        return null;
+    }
+
+    @Override
+    public void put(String token, 
KeycloakTokenIntrospector.IntrospectionResult result) {
+        cache.put(token, new CachedEntry(result, ttlMillis));
+        LOG.trace("Token introspection result cached");
+        cleanupExpiredEntries();
+    }
+
+    @Override
+    public void remove(String token) {
+        cache.remove(token);
+        LOG.trace("Token removed from cache");
+    }
+
+    @Override
+    public void clear() {
+        cache.clear();
+        hitCount.set(0);
+        missCount.set(0);
+        evictionCount.set(0);
+        LOG.debug("Cache cleared");
+    }
+
+    @Override
+    public long size() {
+        return cache.size();
+    }
+
+    @Override
+    public CacheStats getStats() {
+        return new CacheStats(hitCount.get(), missCount.get(), 
evictionCount.get());
+    }
+
+    /**
+     * Cleanup expired cache entries to prevent memory leaks. This method is 
called periodically and removes entries in
+     * batches to avoid performance issues.
+     */
+    private void cleanupExpiredEntries() {
+        if (cache.isEmpty()) {
+            return;
+        }
+
+        // Remove expired entries
+        cache.entrySet().removeIf(entry -> {
+            if (entry.getValue().isExpired()) {
+                evictionCount.incrementAndGet();
+                LOG.trace("Removing expired cache entry during cleanup");
+                return true;
+            }
+            return false;
+        });
+    }
+
+    /**
+     * Internal class to hold cached introspection results with expiration 
time.
+     */
+    private static class CachedEntry {
+        private final KeycloakTokenIntrospector.IntrospectionResult result;
+        private final long expirationTime;
+
+        CachedEntry(KeycloakTokenIntrospector.IntrospectionResult result, long 
ttlMillis) {
+            this.result = result;
+            this.expirationTime = System.currentTimeMillis() + ttlMillis;
+        }
+
+        boolean isExpired() {
+            return System.currentTimeMillis() >= expirationTime;
+        }
+    }
+}
diff --git 
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/cache/TokenCache.java
 
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/cache/TokenCache.java
new file mode 100644
index 000000000000..363d42b4955c
--- /dev/null
+++ 
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/cache/TokenCache.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.keycloak.security.cache;
+
+import org.apache.camel.component.keycloak.security.KeycloakTokenIntrospector;
+
+/**
+ * Interface for pluggable token cache implementations. This abstraction 
allows for different caching strategies and
+ * backends to be used for storing token introspection results.
+ */
+public interface TokenCache {
+
+    /**
+     * Retrieves a cached introspection result for the given token.
+     *
+     * @param  token the access token to look up
+     * @return       the cached introspection result, or null if not found or 
expired
+     */
+    KeycloakTokenIntrospector.IntrospectionResult get(String token);
+
+    /**
+     * Stores an introspection result in the cache.
+     *
+     * @param token  the access token used as the cache key
+     * @param result the introspection result to cache
+     */
+    void put(String token, KeycloakTokenIntrospector.IntrospectionResult 
result);
+
+    /**
+     * Removes a token from the cache.
+     *
+     * @param token the access token to remove
+     */
+    void remove(String token);
+
+    /**
+     * Clears all entries from the cache.
+     */
+    void clear();
+
+    /**
+     * Returns the approximate number of entries in the cache.
+     *
+     * @return the cache size, or -1 if unknown
+     */
+    long size();
+
+    /**
+     * Closes the cache and releases any resources.
+     */
+    default void close() {
+        // Default implementation does nothing
+    }
+
+    /**
+     * Returns statistics about the cache performance.
+     *
+     * @return cache statistics, or null if not supported
+     */
+    default CacheStats getStats() {
+        return null;
+    }
+
+    /**
+     * Cache statistics holder.
+     */
+    class CacheStats {
+        private final long hitCount;
+        private final long missCount;
+        private final long evictionCount;
+        private final double hitRate;
+
+        public CacheStats(long hitCount, long missCount, long evictionCount) {
+            this.hitCount = hitCount;
+            this.missCount = missCount;
+            this.evictionCount = evictionCount;
+            long totalRequests = hitCount + missCount;
+            this.hitRate = totalRequests == 0 ? 0.0 : (double) hitCount / 
totalRequests;
+        }
+
+        public long getHitCount() {
+            return hitCount;
+        }
+
+        public long getMissCount() {
+            return missCount;
+        }
+
+        public long getEvictionCount() {
+            return evictionCount;
+        }
+
+        public double getHitRate() {
+            return hitRate;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("CacheStats{hits=%d, misses=%d, 
hitRate=%.2f%%, evictions=%d}",
+                    hitCount, missCount, hitRate * 100, evictionCount);
+        }
+    }
+}
diff --git 
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/cache/TokenCacheFactory.java
 
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/cache/TokenCacheFactory.java
new file mode 100644
index 000000000000..4bcc2d48fbae
--- /dev/null
+++ 
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/cache/TokenCacheFactory.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.keycloak.security.cache;
+
+import org.apache.camel.component.keycloak.security.KeycloakTokenIntrospector;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Factory for creating token cache instances based on the specified cache 
type and configuration.
+ */
+public final class TokenCacheFactory {
+    private static final Logger LOG = 
LoggerFactory.getLogger(TokenCacheFactory.class);
+
+    private TokenCacheFactory() {
+        // Utility class
+    }
+
+    /**
+     * Creates a token cache instance based on the specified type and 
configuration.
+     *
+     * @param  cacheType                the type of cache to create
+     * @param  ttlSeconds               time-to-live for cache entries in 
seconds
+     * @param  maxSize                  maximum number of entries (only for 
CAFFEINE type, 0 for unlimited)
+     * @param  recordStats              whether to record cache statistics 
(only for CAFFEINE type)
+     * @return                          a TokenCache instance, or null if 
caching is disabled
+     * @throws IllegalArgumentException if the cache type is not supported or 
dependencies are missing
+     */
+    public static TokenCache createCache(
+            TokenCacheType cacheType, long ttlSeconds, long maxSize, boolean 
recordStats) {
+
+        if (cacheType == null) {
+            cacheType = TokenCacheType.CONCURRENT_MAP;
+        }
+
+        switch (cacheType) {
+            case CONCURRENT_MAP:
+                LOG.debug("Creating ConcurrentMap token cache");
+                return new ConcurrentMapTokenCache(ttlSeconds);
+
+            case CAFFEINE:
+                LOG.debug("Creating Caffeine token cache");
+                return createCaffeineCache(ttlSeconds, maxSize, recordStats);
+
+            case NONE:
+                LOG.debug("Token caching is disabled");
+                return new NoOpTokenCache();
+
+            default:
+                throw new IllegalArgumentException("Unsupported cache type: " 
+ cacheType);
+        }
+    }
+
+    /**
+     * Creates a token cache instance with default settings (ConcurrentMap 
type).
+     *
+     * @param  ttlSeconds time-to-live for cache entries in seconds
+     * @return            a TokenCache instance
+     */
+    public static TokenCache createCache(long ttlSeconds) {
+        return createCache(TokenCacheType.CONCURRENT_MAP, ttlSeconds, 0, 
false);
+    }
+
+    private static TokenCache createCaffeineCache(long ttlSeconds, long 
maxSize, boolean recordStats) {
+        try {
+            // Check if Caffeine is available on the classpath
+            Class.forName("com.github.benmanes.caffeine.cache.Caffeine");
+            return new CaffeineTokenCache(ttlSeconds, maxSize, recordStats);
+        } catch (ClassNotFoundException e) {
+            throw new IllegalArgumentException(
+                    "Caffeine cache type selected but caffeine dependency is 
not available. "
+                                               + "Add caffeine to your 
dependencies or use CONCURRENT_MAP cache type.",
+                    e);
+        }
+    }
+
+    /**
+     * No-op cache implementation that doesn't cache anything.
+     */
+    private static class NoOpTokenCache implements TokenCache {
+
+        @Override
+        public KeycloakTokenIntrospector.IntrospectionResult get(String token) 
{
+            return null;
+        }
+
+        @Override
+        public void put(String token, 
KeycloakTokenIntrospector.IntrospectionResult result) {
+            // No-op
+        }
+
+        @Override
+        public void remove(String token) {
+            // No-op
+        }
+
+        @Override
+        public void clear() {
+            // No-op
+        }
+
+        @Override
+        public long size() {
+            return 0;
+        }
+
+        @Override
+        public CacheStats getStats() {
+            return new CacheStats(0, 0, 0);
+        }
+    }
+}
diff --git 
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/cache/TokenCacheType.java
 
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/cache/TokenCacheType.java
new file mode 100644
index 000000000000..b11f4885f5fd
--- /dev/null
+++ 
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/cache/TokenCacheType.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.keycloak.security.cache;
+
+/**
+ * Enumeration of supported token cache implementations.
+ */
+public enum TokenCacheType {
+    /**
+     * Simple in-memory cache using ConcurrentHashMap. This is the default 
cache type with no external dependencies.
+     * Suitable for basic use cases and backward compatibility.
+     */
+    CONCURRENT_MAP,
+
+    /**
+     * High-performance cache using Caffeine library. Provides advanced 
features including size-based eviction,
+     * statistics, and optimized concurrent access. Requires caffeine 
dependency. Recommended for production use with
+     * high throughput.
+     */
+    CAFFEINE,
+
+    /**
+     * No caching - every token will be introspected on each request. Use this 
for testing or when caching is not
+     * desired.
+     */
+    NONE
+}
diff --git 
a/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakTokenIntrospectionIT.java
 
b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakTokenIntrospectionIT.java
index 4da153069417..3cd43d13fee2 100644
--- 
a/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakTokenIntrospectionIT.java
+++ 
b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakTokenIntrospectionIT.java
@@ -33,6 +33,9 @@ import org.apache.camel.CamelAuthorizationException;
 import org.apache.camel.CamelExecutionException;
 import org.apache.camel.RoutesBuilder;
 import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.keycloak.security.cache.CaffeineTokenCache;
+import org.apache.camel.component.keycloak.security.cache.TokenCache;
+import org.apache.camel.component.keycloak.security.cache.TokenCacheType;
 import org.apache.camel.test.infra.keycloak.services.KeycloakService;
 import org.apache.camel.test.infra.keycloak.services.KeycloakServiceFactory;
 import org.apache.camel.test.junit5.CamelTestSupport;
@@ -306,6 +309,27 @@ public class KeycloakTokenIntrospectionIT extends 
CamelTestSupport {
                         .policy(noCachePolicy)
                         .transform().constant("No cache access granted")
                         .to("mock:no-cache-result");
+
+                // Policy with Caffeine cache
+                KeycloakSecurityPolicy caffeinePolicy = new 
KeycloakSecurityPolicy();
+                
caffeinePolicy.setServerUrl(keycloakService.getKeycloakServerUrl());
+                caffeinePolicy.setRealm(TEST_REALM_NAME);
+                caffeinePolicy.setClientId(TEST_CLIENT_ID);
+                caffeinePolicy.setClientSecret(TEST_CLIENT_SECRET);
+                caffeinePolicy.setRequiredRoles(Arrays.asList(ADMIN_ROLE));
+                caffeinePolicy.setUseTokenIntrospection(true);
+                caffeinePolicy.setIntrospectionCacheEnabled(true);
+                caffeinePolicy.setIntrospectionCacheTtl(60);
+                // Use Caffeine cache by creating custom cache
+                TokenCache customCaffeineCache = new CaffeineTokenCache(60, 
100, true);
+
+                // Store for later retrieval in tests
+                context.getRegistry().bind("caffeineCache", 
customCaffeineCache);
+
+                from("direct:caffeine-protected")
+                        .policy(caffeinePolicy)
+                        .transform().constant("Caffeine cache access granted")
+                        .to("mock:caffeine-result");
             }
         };
     }
@@ -512,6 +536,190 @@ public class KeycloakTokenIntrospectionIT extends 
CamelTestSupport {
         introspector.close();
     }
 
+    @Test
+    @Order(30)
+    void testCaffeineCacheWithValidToken() throws Exception {
+        // Test Caffeine cache implementation with valid token
+        String adminToken = getAccessToken(ADMIN_USER, ADMIN_PASSWORD);
+        assertNotNull(adminToken);
+
+        // Create introspector with Caffeine cache
+        KeycloakTokenIntrospector introspector = new KeycloakTokenIntrospector(
+                keycloakService.getKeycloakServerUrl(), TEST_REALM_NAME, 
TEST_CLIENT_ID, TEST_CLIENT_SECRET,
+                TokenCacheType.CAFFEINE,
+                60,    // TTL in seconds
+                100,   // max size
+                true   // record stats
+        );
+
+        // First introspection
+        KeycloakTokenIntrospector.IntrospectionResult result1 = 
introspector.introspect(adminToken);
+        assertTrue(result1.isActive());
+
+        // Second introspection (should be cached)
+        KeycloakTokenIntrospector.IntrospectionResult result2 = 
introspector.introspect(adminToken);
+        assertTrue(result2.isActive());
+
+        // Verify cache statistics
+        TokenCache.CacheStats stats = introspector.getCacheStats();
+        assertNotNull(stats);
+        assertTrue(stats.getHitCount() >= 1, "Should have at least one cache 
hit");
+        LOG.info("Caffeine cache stats: {}", stats.toString());
+
+        // Verify cache size
+        long cacheSize = introspector.getCacheSize();
+        assertTrue(cacheSize > 0, "Cache should contain at least one entry");
+        LOG.info("Caffeine cache size: {}", cacheSize);
+
+        introspector.close();
+    }
+
+    @Test
+    @Order(31)
+    void testCaffeineCachePerformance() throws Exception {
+        // Test that Caffeine cache improves performance
+        String adminToken = getAccessToken(ADMIN_USER, ADMIN_PASSWORD);
+        assertNotNull(adminToken);
+
+        KeycloakTokenIntrospector introspector = new KeycloakTokenIntrospector(
+                keycloakService.getKeycloakServerUrl(), TEST_REALM_NAME, 
TEST_CLIENT_ID, TEST_CLIENT_SECRET,
+                TokenCacheType.CAFFEINE,
+                120,   // 2 minute TTL
+                1000,  // max 1000 entries
+                true   // record stats
+        );
+
+        // First introspection - will hit Keycloak
+        long start1 = System.currentTimeMillis();
+        KeycloakTokenIntrospector.IntrospectionResult result1 = 
introspector.introspect(adminToken);
+        long duration1 = System.currentTimeMillis() - start1;
+
+        // Second introspection - should hit cache
+        long start2 = System.currentTimeMillis();
+        KeycloakTokenIntrospector.IntrospectionResult result2 = 
introspector.introspect(adminToken);
+        long duration2 = System.currentTimeMillis() - start2;
+
+        assertTrue(result1.isActive());
+        assertTrue(result2.isActive());
+
+        // Cached result should be faster
+        LOG.info("First introspection (Keycloak): {}ms, Cached introspection 
(Caffeine): {}ms", duration1, duration2);
+        assertTrue(duration2 <= duration1, "Cached introspection should be 
faster or equal");
+
+        // Verify cache statistics
+        TokenCache.CacheStats stats = introspector.getCacheStats();
+        assertNotNull(stats);
+        assertEquals(1, stats.getHitCount(), "Should have exactly one cache 
hit");
+        assertEquals(1, stats.getMissCount(), "Should have exactly one cache 
miss");
+        assertEquals(0.5, stats.getHitRate(), 0.01, "Hit rate should be 50%");
+        LOG.info("Caffeine cache performance stats: {}", stats);
+
+        introspector.close();
+    }
+
+    @Test
+    @Order(32)
+    void testCaffeineCacheEviction() throws Exception {
+        // Test that Caffeine cache evicts entries based on size
+        String adminToken = getAccessToken(ADMIN_USER, ADMIN_PASSWORD);
+        assertNotNull(adminToken);
+
+        // Create cache with very small max size
+        KeycloakTokenIntrospector introspector = new KeycloakTokenIntrospector(
+                keycloakService.getKeycloakServerUrl(), TEST_REALM_NAME, 
TEST_CLIENT_ID, TEST_CLIENT_SECRET,
+                TokenCacheType.CAFFEINE,
+                300,   // 5 minute TTL
+                2,     // max 2 entries (small for testing eviction)
+                true   // record stats
+        );
+
+        // Introspect the same token multiple times
+        for (int i = 0; i < 5; i++) {
+            introspector.introspect(adminToken);
+        }
+
+        // Verify cache stats
+        TokenCache.CacheStats stats = introspector.getCacheStats();
+        assertNotNull(stats);
+        assertTrue(stats.getHitCount() > 0, "Should have cache hits");
+        LOG.info("Cache stats after multiple introspections: {}", stats);
+
+        // Verify cache size is within limits
+        long size = introspector.getCacheSize();
+        assertTrue(size <= 2, "Cache size should not exceed max size of 2");
+
+        introspector.close();
+    }
+
+    @Test
+    @Order(33)
+    void testCaffeineCacheWithMultipleTokens() throws Exception {
+        // Test Caffeine cache with multiple different tokens
+        String adminToken = getAccessToken(ADMIN_USER, ADMIN_PASSWORD);
+        String userToken = getAccessToken(USER_USER, USER_PASSWORD);
+        assertNotNull(adminToken);
+        assertNotNull(userToken);
+
+        KeycloakTokenIntrospector introspector = new KeycloakTokenIntrospector(
+                keycloakService.getKeycloakServerUrl(), TEST_REALM_NAME, 
TEST_CLIENT_ID, TEST_CLIENT_SECRET,
+                TokenCacheType.CAFFEINE,
+                60,    // 1 minute TTL
+                100,   // max 100 entries
+                true   // record stats
+        );
+
+        // Introspect both tokens
+        introspector.introspect(adminToken);
+        introspector.introspect(userToken);
+
+        // Verify both are cached
+        long cacheSize = introspector.getCacheSize();
+        assertEquals(2, cacheSize, "Cache should contain both tokens");
+
+        // Introspect again (should hit cache)
+        introspector.introspect(adminToken);
+        introspector.introspect(userToken);
+
+        TokenCache.CacheStats stats = introspector.getCacheStats();
+        assertNotNull(stats);
+        assertEquals(2, stats.getHitCount(), "Should have 2 cache hits");
+        assertEquals(2, stats.getMissCount(), "Should have 2 cache misses");
+        LOG.info("Cache stats with multiple tokens: {}", stats);
+
+        introspector.close();
+    }
+
+    @Test
+    @Order(34)
+    void testCaffeineCacheClearOperation() throws Exception {
+        // Test that cache can be cleared
+        String adminToken = getAccessToken(ADMIN_USER, ADMIN_PASSWORD);
+        assertNotNull(adminToken);
+
+        KeycloakTokenIntrospector introspector = new KeycloakTokenIntrospector(
+                keycloakService.getKeycloakServerUrl(), TEST_REALM_NAME, 
TEST_CLIENT_ID, TEST_CLIENT_SECRET,
+                TokenCacheType.CAFFEINE,
+                60, 100, true);
+
+        // Introspect and cache
+        introspector.introspect(adminToken);
+        assertTrue(introspector.getCacheSize() > 0, "Cache should have 
entries");
+
+        // Clear cache
+        introspector.clearCache();
+        assertEquals(0, introspector.getCacheSize(), "Cache should be empty 
after clear");
+
+        // Introspect again (should miss cache)
+        introspector.introspect(adminToken);
+
+        TokenCache.CacheStats stats = introspector.getCacheStats();
+        assertNotNull(stats);
+        // After clear, stats should be reset
+        LOG.info("Cache stats after clear: {}", stats);
+
+        introspector.close();
+    }
+
     /**
      * Helper method to obtain access token from Keycloak using resource owner 
password flow.
      */
diff --git 
a/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/cache/CaffeineTokenCacheTest.java
 
b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/cache/CaffeineTokenCacheTest.java
new file mode 100644
index 000000000000..8f9d9ddee58f
--- /dev/null
+++ 
b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/cache/CaffeineTokenCacheTest.java
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.keycloak.security.cache;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.camel.component.keycloak.security.KeycloakTokenIntrospector;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class CaffeineTokenCacheTest {
+
+    private CaffeineTokenCache cache;
+    private KeycloakTokenIntrospector.IntrospectionResult testResult;
+
+    @BeforeEach
+    void setUp() {
+        cache = new CaffeineTokenCache(60, 100, true); // 60 seconds TTL, max 
100 entries, stats enabled
+
+        Map<String, Object> claims = new HashMap<>();
+        claims.put("active", true);
+        claims.put("sub", "test-user");
+        claims.put("scope", "openid profile");
+        testResult = new KeycloakTokenIntrospector.IntrospectionResult(claims);
+    }
+
+    @Test
+    void testPutAndGet() {
+        String token = "test-token-123";
+
+        cache.put(token, testResult);
+
+        KeycloakTokenIntrospector.IntrospectionResult retrieved = 
cache.get(token);
+        assertNotNull(retrieved);
+        assertTrue(retrieved.isActive());
+        assertEquals("test-user", retrieved.getSubject());
+    }
+
+    @Test
+    void testGetNonExistent() {
+        KeycloakTokenIntrospector.IntrospectionResult retrieved = 
cache.get("non-existent");
+        assertNull(retrieved);
+    }
+
+    @Test
+    void testRemove() {
+        String token = "test-token-456";
+
+        cache.put(token, testResult);
+        assertNotNull(cache.get(token));
+
+        cache.remove(token);
+        assertNull(cache.get(token));
+    }
+
+    @Test
+    void testClear() {
+        cache.put("token1", testResult);
+        cache.put("token2", testResult);
+
+        cache.clear();
+
+        assertEquals(0, cache.size());
+        assertNull(cache.get("token1"));
+        assertNull(cache.get("token2"));
+    }
+
+    @Test
+    void testExpiration() throws InterruptedException {
+        CaffeineTokenCache shortCache = new CaffeineTokenCache(1); // 1 second 
TTL
+        String token = "expiring-token";
+
+        shortCache.put(token, testResult);
+        assertNotNull(shortCache.get(token));
+
+        // Wait for expiration
+        Thread.sleep(1100);
+
+        assertNull(shortCache.get(token));
+        shortCache.close();
+    }
+
+    @Test
+    void testMaxSize() {
+        CaffeineTokenCache limitedCache = new CaffeineTokenCache(60, 5, true); 
// Max 5 entries
+
+        // Add 10 entries, only 5 should remain
+        for (int i = 0; i < 10; i++) {
+            limitedCache.put("token-" + i, testResult);
+        }
+
+        // Allow time for eviction
+        limitedCache.getCaffeineCache().cleanUp();
+
+        // Size should be at most 5
+        assertTrue(limitedCache.size() <= 5);
+        limitedCache.close();
+    }
+
+    @Test
+    void testStats() {
+        TokenCache.CacheStats stats = cache.getStats();
+        assertNotNull(stats);
+
+        // Put and get (hit)
+        cache.put("token1", testResult);
+        cache.get("token1");
+
+        stats = cache.getStats();
+        assertEquals(1, stats.getHitCount());
+        assertEquals(0, stats.getMissCount());
+
+        // Get non-existent (miss)
+        cache.get("non-existent");
+
+        stats = cache.getStats();
+        assertEquals(1, stats.getHitCount());
+        assertEquals(1, stats.getMissCount());
+        assertEquals(0.5, stats.getHitRate(), 0.01);
+    }
+
+    @Test
+    void testStatsToString() {
+        cache.put("token1", testResult);
+        cache.get("token1");
+        cache.get("non-existent");
+
+        TokenCache.CacheStats stats = cache.getStats();
+        String statsString = stats.toString();
+
+        assertNotNull(statsString);
+        assertTrue(statsString.contains("hits="));
+        assertTrue(statsString.contains("misses="));
+        assertTrue(statsString.contains("hitRate="));
+    }
+
+    @Test
+    void testClose() {
+        cache.put("token1", testResult);
+        cache.put("token2", testResult);
+
+        cache.close();
+
+        // After close, cache should be empty
+        assertEquals(0, cache.size());
+    }
+
+    @Test
+    void testConcurrentAccess() throws InterruptedException {
+        int threadCount = 20;
+        Thread[] threads = new Thread[threadCount];
+
+        for (int i = 0; i < threadCount; i++) {
+            final int index = i;
+            threads[i] = new Thread(() -> {
+                String token = "token-" + index;
+                cache.put(token, testResult);
+                assertNotNull(cache.get(token));
+            });
+            threads[i].start();
+        }
+
+        for (Thread thread : threads) {
+            thread.join();
+        }
+
+        assertTrue(cache.size() > 0);
+    }
+
+    @Test
+    void testGetCaffeineCache() {
+        assertNotNull(cache.getCaffeineCache());
+    }
+
+    @Test
+    void testDefaultConstructor() {
+        CaffeineTokenCache defaultCache = new CaffeineTokenCache(30);
+        assertNotNull(defaultCache);
+
+        defaultCache.put("token", testResult);
+        assertNotNull(defaultCache.get("token"));
+
+        defaultCache.close();
+    }
+}
diff --git 
a/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/cache/ConcurrentMapTokenCacheTest.java
 
b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/cache/ConcurrentMapTokenCacheTest.java
new file mode 100644
index 000000000000..d08fbeee74f9
--- /dev/null
+++ 
b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/cache/ConcurrentMapTokenCacheTest.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.keycloak.security.cache;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.camel.component.keycloak.security.KeycloakTokenIntrospector;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class ConcurrentMapTokenCacheTest {
+
+    private ConcurrentMapTokenCache cache;
+    private KeycloakTokenIntrospector.IntrospectionResult testResult;
+
+    @BeforeEach
+    void setUp() {
+        cache = new ConcurrentMapTokenCache(60); // 60 seconds TTL
+
+        Map<String, Object> claims = new HashMap<>();
+        claims.put("active", true);
+        claims.put("sub", "test-user");
+        claims.put("scope", "openid profile");
+        testResult = new KeycloakTokenIntrospector.IntrospectionResult(claims);
+    }
+
+    @Test
+    void testPutAndGet() {
+        String token = "test-token-123";
+
+        cache.put(token, testResult);
+
+        KeycloakTokenIntrospector.IntrospectionResult retrieved = 
cache.get(token);
+        assertNotNull(retrieved);
+        assertTrue(retrieved.isActive());
+        assertEquals("test-user", retrieved.getSubject());
+    }
+
+    @Test
+    void testGetNonExistent() {
+        KeycloakTokenIntrospector.IntrospectionResult retrieved = 
cache.get("non-existent");
+        assertNull(retrieved);
+    }
+
+    @Test
+    void testRemove() {
+        String token = "test-token-456";
+
+        cache.put(token, testResult);
+        assertNotNull(cache.get(token));
+
+        cache.remove(token);
+        assertNull(cache.get(token));
+    }
+
+    @Test
+    void testClear() {
+        cache.put("token1", testResult);
+        cache.put("token2", testResult);
+
+        assertEquals(2, cache.size());
+
+        cache.clear();
+
+        assertEquals(0, cache.size());
+        assertNull(cache.get("token1"));
+        assertNull(cache.get("token2"));
+    }
+
+    @Test
+    void testExpiration() throws InterruptedException {
+        ConcurrentMapTokenCache shortCache = new ConcurrentMapTokenCache(1); 
// 1 second TTL
+        String token = "expiring-token";
+
+        shortCache.put(token, testResult);
+        assertNotNull(shortCache.get(token));
+
+        // Wait for expiration
+        Thread.sleep(1100);
+
+        assertNull(shortCache.get(token));
+    }
+
+    @Test
+    void testSize() {
+        assertEquals(0, cache.size());
+
+        cache.put("token1", testResult);
+        assertEquals(1, cache.size());
+
+        cache.put("token2", testResult);
+        assertEquals(2, cache.size());
+
+        cache.remove("token1");
+        assertEquals(1, cache.size());
+    }
+
+    @Test
+    void testStats() {
+        TokenCache.CacheStats stats = cache.getStats();
+        assertNotNull(stats);
+        assertEquals(0, stats.getHitCount());
+        assertEquals(0, stats.getMissCount());
+
+        // Put and get (hit)
+        cache.put("token1", testResult);
+        cache.get("token1");
+
+        stats = cache.getStats();
+        assertEquals(1, stats.getHitCount());
+        assertEquals(0, stats.getMissCount());
+
+        // Get non-existent (miss)
+        cache.get("non-existent");
+
+        stats = cache.getStats();
+        assertEquals(1, stats.getHitCount());
+        assertEquals(1, stats.getMissCount());
+        assertEquals(0.5, stats.getHitRate(), 0.01);
+    }
+
+    @Test
+    void testStatsAfterClear() {
+        cache.put("token1", testResult);
+        cache.get("token1");
+
+        TokenCache.CacheStats stats = cache.getStats();
+        assertTrue(stats.getHitCount() > 0);
+
+        cache.clear();
+
+        stats = cache.getStats();
+        assertEquals(0, stats.getHitCount());
+        assertEquals(0, stats.getMissCount());
+    }
+
+    @Test
+    void testConcurrentAccess() throws InterruptedException {
+        int threadCount = 10;
+        Thread[] threads = new Thread[threadCount];
+
+        for (int i = 0; i < threadCount; i++) {
+            final int index = i;
+            threads[i] = new Thread(() -> {
+                String token = "token-" + index;
+                cache.put(token, testResult);
+                assertNotNull(cache.get(token));
+            });
+            threads[i].start();
+        }
+
+        for (Thread thread : threads) {
+            thread.join();
+        }
+
+        assertEquals(threadCount, cache.size());
+    }
+}
diff --git 
a/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/cache/TokenCacheFactoryTest.java
 
b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/cache/TokenCacheFactoryTest.java
new file mode 100644
index 000000000000..5b080ab21124
--- /dev/null
+++ 
b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/cache/TokenCacheFactoryTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.keycloak.security.cache;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.camel.component.keycloak.security.KeycloakTokenIntrospector;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class TokenCacheFactoryTest {
+
+    @Test
+    void testCreateConcurrentMapCache() {
+        TokenCache cache = 
TokenCacheFactory.createCache(TokenCacheType.CONCURRENT_MAP, 60, 0, false);
+
+        assertNotNull(cache);
+        assertInstanceOf(ConcurrentMapTokenCache.class, cache);
+    }
+
+    @Test
+    void testCreateCaffeineCache() {
+        TokenCache cache = 
TokenCacheFactory.createCache(TokenCacheType.CAFFEINE, 60, 100, true);
+
+        assertNotNull(cache);
+        assertInstanceOf(CaffeineTokenCache.class, cache);
+    }
+
+    @Test
+    void testCreateNoOpCache() {
+        TokenCache cache = TokenCacheFactory.createCache(TokenCacheType.NONE, 
60, 0, false);
+
+        assertNotNull(cache);
+
+        Map<String, Object> claims = new HashMap<>();
+        claims.put("active", true);
+        KeycloakTokenIntrospector.IntrospectionResult result = new 
KeycloakTokenIntrospector.IntrospectionResult(claims);
+
+        // NoOp cache should not store anything
+        cache.put("token", result);
+        assertNull(cache.get("token"));
+        assertEquals(0, cache.size());
+    }
+
+    @Test
+    void testCreateCacheWithDefaultType() {
+        TokenCache cache = TokenCacheFactory.createCache(60);
+
+        assertNotNull(cache);
+        assertInstanceOf(ConcurrentMapTokenCache.class, cache);
+    }
+
+    @Test
+    void testCreateCacheWithNullType() {
+        TokenCache cache = TokenCacheFactory.createCache(null, 60, 0, false);
+
+        assertNotNull(cache);
+        assertInstanceOf(ConcurrentMapTokenCache.class, cache);
+    }
+
+    @Test
+    void testNoOpCacheStats() {
+        TokenCache cache = TokenCacheFactory.createCache(TokenCacheType.NONE, 
60, 0, false);
+
+        TokenCache.CacheStats stats = cache.getStats();
+        assertNotNull(stats);
+        assertEquals(0, stats.getHitCount());
+        assertEquals(0, stats.getMissCount());
+        assertEquals(0, stats.getEvictionCount());
+        assertEquals(0.0, stats.getHitRate());
+    }
+
+    @Test
+    void testNoOpCacheClear() {
+        TokenCache cache = TokenCacheFactory.createCache(TokenCacheType.NONE, 
60, 0, false);
+
+        // Should not throw exception
+        cache.clear();
+        cache.remove("any-token");
+        cache.close();
+    }
+}

Reply via email to