oscerd commented on code in PR #26747:
URL: https://github.com/apache/camel/pull/26747#discussion_r4122493821


##########
components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/cache/ConcurrentMapTokenCacheTest.java:
##########
@@ -172,4 +172,35 @@ void testConcurrentAccess() throws InterruptedException {
 
         assertEquals(threadCount, cache.size());
     }
+
+    @Test
+    void testExpiredResultNotServed() {
+        // A result whose token has already expired must not be served, even 
while the configured TTL has not elapsed.
+        Map<String, Object> claims = new HashMap<>();
+        claims.put("active", true);
+        claims.put("sub", "test-user");
+        claims.put("exp", System.currentTimeMillis() / 1000 - 60); // expired 
60 seconds ago
+        KeycloakTokenIntrospector.IntrospectionResult expired
+                = new KeycloakTokenIntrospector.IntrospectionResult(claims);
+
+        cache.put("expired-token", expired);
+
+        assertNull(cache.get("expired-token"));

Review Comment:
   Addressed in e9580c7 and f7cff3f. 
`testResultExpiringBeforeTtlNotServedAfterExp` (in both 
`ConcurrentMapTokenCacheTest` and `CaffeineTokenCacheTest`) puts a token whose 
`exp` is about 2 s away against a 300 s TTL, then waits with Awaitility until 
it is no longer served. That exercises the shortened TTL rather than `put()`'s 
early return. The already-expired test's comment now says it covers the early 
return and points to that test.
   
   _Claude Code on behalf of @oscerd_
   



##########
components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessorTest.java:
##########
@@ -518,4 +518,25 @@ public KeycloakTokenIntrospector getTokenIntrospector() {
         assertFalse(routeReached.get(),
                 "Route body must not be reached when the token has the 
required permission but the wrong authorized party");
     }
+
+    @Test
+    void testActiveButExpiredIntrospectionResultRejected() throws Exception {

Review Comment:
   Addressed in e9580c7. 
`testActiveButExpiredIntrospectionResultRejectedOnRolesPath` and 
`testActiveButExpiredIntrospectionResultRejectedOnPermissionsPath` cover 
`validateRoles()` and `validatePermissions()`. They assert that the expiry 
rejection fires before the role or permission check.
   
   _Claude Code on behalf of @oscerd_
   



##########
components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/cache/ConcurrentMapTokenCache.java:
##########
@@ -64,11 +64,30 @@ public KeycloakTokenIntrospector.IntrospectionResult 
get(String token) {
 
     @Override
     public void put(String token, 
KeycloakTokenIntrospector.IntrospectionResult result) {
-        cache.put(token, new CachedEntry(result, ttlMillis));
+        if (result.isExpired()) {
+            // Never cache a result whose token has already expired: it must 
not be served on a later hit.
+            LOG.trace("Token already expired; skipping cache put");
+            return;
+        }
+        cache.put(token, new CachedEntry(result, effectiveTtlMillis(result)));
         LOG.trace("Token introspection result cached");
         cleanupExpiredEntries();
     }
 
+    /**
+     * Computes the effective time-to-live for a result, bounding the 
configured TTL by the token's own remaining
+     * validity so a cached result is never returned after the token's {@code 
exp}. Results without an {@code exp} claim
+     * keep the configured TTL.
+     */
+    private long 
effectiveTtlMillis(KeycloakTokenIntrospector.IntrospectionResult result) {
+        Long expSeconds = result.getExpiration();
+        if (expSeconds == null) {
+            return ttlMillis;
+        }
+        long remainingMillis = expSeconds * 1000L - System.currentTimeMillis();
+        return Math.min(ttlMillis, remainingMillis);

Review Comment:
   Addressed in e9580c7. Both cache tests now have 
`testResultExpiringBeforeTtlNotServedAfterExp` with `exp` inside the TTL window 
(about 2 s against 300 s), so replacing the `min` with `return ttlMillis;` (or 
`return maxTtlNanos;` in the Caffeine cache) now fails. It waits with 
`await().atMost(10, SECONDS)` rather than a sleep.
   
   _Claude Code on behalf of @oscerd_
   



##########
components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/cache/CaffeineTokenCache.java:
##########
@@ -126,4 +127,56 @@ public CacheStats getStats() {
     public Cache<String, KeycloakTokenIntrospector.IntrospectionResult> 
getCaffeineCache() {
         return cache;
     }
+
+    /**
+     * Caffeine expiry policy that bounds each entry's lifetime by the smaller 
of the configured TTL and the token's own
+     * remaining validity ({@code exp}), so a cached introspection result is 
never returned after the token has expired.
+     * Reads do not extend an entry's lifetime.
+     */
+    private static final class IntrospectionExpiry
+            implements Expiry<String, 
KeycloakTokenIntrospector.IntrospectionResult> {
+
+        private final long maxTtlNanos;
+
+        IntrospectionExpiry(long maxTtlNanos) {
+            this.maxTtlNanos = maxTtlNanos;
+        }
+
+        @Override
+        public long expireAfterCreate(
+                String key, KeycloakTokenIntrospector.IntrospectionResult 
value, long currentTime) {
+            return expiryNanos(value);
+        }
+
+        @Override
+        public long expireAfterUpdate(
+                String key, KeycloakTokenIntrospector.IntrospectionResult 
value, long currentTime, long currentDuration) {
+            return expiryNanos(value);
+        }
+
+        @Override
+        public long expireAfterRead(
+                String key, KeycloakTokenIntrospector.IntrospectionResult 
value, long currentTime, long currentDuration) {
+            // Reads must not extend the cached lifetime beyond the token's 
expiry.
+            return currentDuration;
+        }
+
+        private long expiryNanos(KeycloakTokenIntrospector.IntrospectionResult 
value) {
+            Long expSeconds = value.getExpiration();
+            if (expSeconds == null) {
+                return maxTtlNanos;
+            }
+            long remainingMillis = expSeconds * 1000L - 
System.currentTimeMillis();
+            if (remainingMillis <= 0) {
+                // Already expired: expire immediately so the entry is not 
served.
+                return 0L;
+            }
+            long remainingNanos = 
TimeUnit.MILLISECONDS.toNanos(remainingMillis);
+            if (remainingNanos < 0) {

Review Comment:
   Addressed in e9580c7. The unreachable guard is gone. The comment on the 
`remainingMillis <= 0` branch now names the `expSeconds * 1000L` wrap as the 
overflow that actually reaches it, which fails closed, and notes that 
`toNanos()` saturates, so `min()` still yields the TTL for a far-future `exp`.
   
   _Claude Code on behalf of @oscerd_
   



##########
components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/cache/ConcurrentMapTokenCacheTest.java:
##########
@@ -172,4 +174,54 @@ void testConcurrentAccess() throws InterruptedException {
 
         assertEquals(threadCount, cache.size());
     }
+
+    @Test
+    void testExpiredResultNotServed() {
+        // A result whose token has already expired must not be served, even 
while the configured TTL has not elapsed.
+        Map<String, Object> claims = new HashMap<>();

Review Comment:
   Addressed in f7cff3f. The comment now says the mechanism is `put()`'s 
`isExpired()` early return, so no entry is ever inserted, and points to 
`testResultExpiringBeforeTtlNotServedAfterExp` for the TTL bounding. The 
Caffeine test's comment was corrected the same way: `expiryNanos` returns 0 
there.
   
   _Claude Code on behalf of @oscerd_
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to