This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch camel-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.18.x by this push:
new 6be02c19c025 [camel-4.18.x] CAMEL-24902: camel-keycloak - honor token
expiry (exp) when caching introspection results (#27183)
6be02c19c025 is described below
commit 6be02c19c0259a983db5910e28deb4b3a00c9d22
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Oct 1 17:39:38 2026 +0200
[camel-4.18.x] CAMEL-24902: camel-keycloak - honor token expiry (exp) when
caching introspection results (#27183)
Co-authored-by: Claude Opus 4.8 <[email protected]>
---
.../src/main/docs/keycloak-component.adoc | 4 +-
.../security/KeycloakSecurityProcessor.java | 19 +++++++
.../security/KeycloakTokenIntrospector.java | 11 ++++
.../security/cache/CaffeineTokenCache.java | 54 ++++++++++++++++++-
.../security/cache/ConcurrentMapTokenCache.java | 21 +++++++-
.../security/KeycloakSecurityProcessorTest.java | 63 ++++++++++++++++++++++
.../security/cache/CaffeineTokenCacheTest.java | 62 +++++++++++++++++++++
.../cache/ConcurrentMapTokenCacheTest.java | 54 +++++++++++++++++++
8 files changed, 283 insertions(+), 5 deletions(-)
diff --git a/components/camel-keycloak/src/main/docs/keycloak-component.adoc
b/components/camel-keycloak/src/main/docs/keycloak-component.adoc
index 8b27b38bb155..5450e8bce4b5 100644
--- a/components/camel-keycloak/src/main/docs/keycloak-component.adoc
+++ b/components/camel-keycloak/src/main/docs/keycloak-component.adoc
@@ -3651,7 +3651,7 @@ beans:
| `useTokenIntrospection` | false | Enable OAuth 2.0 token introspection. When
enabled, tokens are validated via Keycloak's introspection endpoint instead of
local JWT parsing.
| `introspectionCacheEnabled` | true | Enable caching of introspection results
to reduce API calls to Keycloak. Highly recommended for production use.
-| `introspectionCacheTtl` | 60 | Time-to-live for cached introspection results
in seconds. Balance between security (lower TTL) and performance (higher TTL).
+| `introspectionCacheTtl` | 60 | Time-to-live for cached introspection results
in seconds. A cached result is additionally bounded by the token's own expiry
(`exp`), so it is never returned after the token has expired even if the TTL
has not elapsed. Balance between security (lower TTL) and performance (higher
TTL).
|===
NOTE: Token introspection requires a confidential client with client
credentials (client ID and client secret).
@@ -3920,7 +3920,7 @@ policy.setIntrospectionCacheTtl(30); // 30 seconds
| Internal Services | 300-600 seconds | Trusted environment, prioritize
performance
|===
-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.
+NOTE: When a token is introspected and cached, subsequent requests with the
same token will use the cached result until the TTL expires or the token's own
expiry (`exp`) is reached, whichever comes first. A cached result is never
returned after the token has expired. Balance security requirements with
performance needs.
==== Pluggable Cache Implementation
diff --git
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessor.java
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessor.java
index 38da7d5d2ce4..ae061ee8afd4 100644
---
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessor.java
+++
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessor.java
@@ -107,6 +107,8 @@ public class KeycloakSecurityProcessor extends
DelegateProcessor {
throw new CamelAuthorizationException("Token is not active
(may be revoked or expired)", exchange);
}
+ ensureTokenNotExpired(introspectionResult, exchange);
+
if (policy.isValidateIssuer()) {
validateIssuerFromIntrospection(introspectionResult, exchange);
}
@@ -127,6 +129,19 @@ public class KeycloakSecurityProcessor extends
DelegateProcessor {
}
}
+ /**
+ * Enforces token expiry on the introspection path. The introspection
endpoint reports an expired token as inactive,
+ * but a cached result can outlive the token's own {@code exp}; this check
ensures an expired token is rejected on a
+ * cache hit, consistent with the expiry enforcement performed on the
local JWT verification path.
+ */
+ private void ensureTokenNotExpired(
+ KeycloakTokenIntrospector.IntrospectionResult introspectionResult,
Exchange exchange)
+ throws CamelAuthorizationException {
+ if (introspectionResult.isExpired()) {
+ throw new CamelAuthorizationException("Token has expired",
exchange);
+ }
+ }
+
private String getAccessToken(Exchange exchange) throws Exception {
// Get token from exchange property (application-controlled, TRUSTED)
String propertyToken =
exchange.getProperty(KeycloakSecurityConstants.ACCESS_TOKEN_PROPERTY,
String.class);
@@ -281,6 +296,8 @@ public class KeycloakSecurityProcessor extends
DelegateProcessor {
throw new CamelAuthorizationException("Token is not active
(may be revoked or expired)", exchange);
}
+ ensureTokenNotExpired(introspectionResult, exchange);
+
// Validate issuer from introspection result if enabled
if (policy.isValidateIssuer()) {
validateIssuerFromIntrospection(introspectionResult,
exchange);
@@ -506,6 +523,8 @@ public class KeycloakSecurityProcessor extends
DelegateProcessor {
throw new CamelAuthorizationException("Token is not active
(may be revoked or expired)", exchange);
}
+ ensureTokenNotExpired(introspectionResult, exchange);
+
// Validate issuer from introspection result if enabled
if (policy.isValidateIssuer()) {
validateIssuerFromIntrospection(introspectionResult,
exchange);
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 61f6f1d62b4b..d4f5df43e8d2 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
@@ -252,6 +252,17 @@ public class KeycloakTokenIntrospector {
return active instanceof Boolean b && b;
}
+ /**
+ * Returns whether the token has passed its expiry ({@code exp}) time.
A result that carries no {@code exp}
+ * claim is treated as not expired, leaving expiry enforcement to the
introspection endpoint.
+ *
+ * @return true if the {@code exp} claim is present and lies in the
past, false otherwise
+ */
+ public boolean isExpired() {
+ Long exp = getExpiration();
+ return exp != null && exp * 1000L <= System.currentTimeMillis();
+ }
+
/**
* Returns the subject (user ID) of the token.
*
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
index b735fdc92ca8..30d43cdd4c81 100644
---
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
@@ -20,6 +20,7 @@ import java.util.concurrent.TimeUnit;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
+import com.github.benmanes.caffeine.cache.Expiry;
import org.apache.camel.component.keycloak.security.KeycloakTokenIntrospector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -42,8 +43,8 @@ public class CaffeineTokenCache implements TokenCache {
* @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);
+ Caffeine<String, KeycloakTokenIntrospector.IntrospectionResult>
builder = Caffeine.newBuilder()
+ .expireAfter(new
IntrospectionExpiry(TimeUnit.SECONDS.toNanos(ttlSeconds)));
if (maxSize > 0) {
builder.maximumSize(maxSize);
@@ -126,4 +127,53 @@ public class CaffeineTokenCache implements TokenCache {
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, or an out-of-range exp whose millisecond
conversion overflowed to a
+ // negative value: expire immediately so the entry is not
served.
+ return 0L;
+ }
+ // toNanos() saturates to Long.MAX_VALUE for a far-future exp, so
min() still yields the TTL.
+ return Math.min(maxTtlNanos,
TimeUnit.MILLISECONDS.toNanos(remainingMillis));
+ }
+ }
}
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
index aa2ebb06ec4b..17dae7e3d84b 100644
---
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
@@ -64,11 +64,30 @@ public class ConcurrentMapTokenCache implements TokenCache {
@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);
+ }
+
@Override
public void remove(String token) {
cache.remove(token);
diff --git
a/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessorTest.java
b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessorTest.java
index 837ddd22c4d9..f657e7216126 100644
---
a/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessorTest.java
+++
b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessorTest.java
@@ -518,4 +518,67 @@ class KeycloakSecurityProcessorTest {
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 {
+ // Simulates a cached introspection result that was active when stored
but whose token exp has since passed:
+ // the introspection path must reject it, consistent with expiry
enforcement on the local JWT path, instead of
+ // admitting the request until the cache TTL elapses.
+ long expiredSecondsAgo = System.currentTimeMillis() / 1000 - 60;
+ KeycloakTokenIntrospector introspector
+ = introspectorReturning(Map.of("active", true, "exp",
expiredSecondsAgo));
+
+ KeycloakSecurityPolicy policy = introspectionPolicy(introspector);
+ policy.setValidateIssuer(false);
+
+ AtomicBoolean routeReached = new AtomicBoolean(false);
+ KeycloakSecurityProcessor processor = new KeycloakSecurityProcessor(e
-> routeReached.set(true), policy);
+
+ CamelAuthorizationException e
+ = assertThrows(CamelAuthorizationException.class, () ->
processor.process(bearer("x")));
+ assertTrue(e.getMessage().contains("expired"), "unexpected message: "
+ e.getMessage());
+ assertFalse(routeReached.get(), "Route body must not be reached for an
expired token");
+ }
+
+ @Test
+ void testActiveButExpiredIntrospectionResultRejectedOnRolesPath() throws
Exception {
+ // With required roles configured, authentication runs through
validateRoles(); the expiry check there
+ // must reject an active-but-expired result before the role check, so
removing it from that path is caught.
+ long expiredSecondsAgo = System.currentTimeMillis() / 1000 - 60;
+ KeycloakTokenIntrospector introspector
+ = introspectorReturning(Map.of("active", true, "exp",
expiredSecondsAgo));
+
+ KeycloakSecurityPolicy policy = introspectionPolicy(introspector);
+ policy.setValidateIssuer(false);
+ policy.setRequiredRoles("admin");
+
+ AtomicBoolean routeReached = new AtomicBoolean(false);
+ KeycloakSecurityProcessor processor = new KeycloakSecurityProcessor(e
-> routeReached.set(true), policy);
+
+ CamelAuthorizationException e
+ = assertThrows(CamelAuthorizationException.class, () ->
processor.process(bearer("x")));
+ assertTrue(e.getMessage().contains("expired"), "unexpected message: "
+ e.getMessage());
+ assertFalse(routeReached.get(), "Route body must not be reached for an
expired token on the roles path");
+ }
+
+ @Test
+ void testActiveButExpiredIntrospectionResultRejectedOnPermissionsPath()
throws Exception {
+ // With required permissions configured, authentication runs through
validatePermissions(); the expiry
+ // check there must reject an active-but-expired result before the
permission check.
+ long expiredSecondsAgo = System.currentTimeMillis() / 1000 - 60;
+ KeycloakTokenIntrospector introspector
+ = introspectorReturning(Map.of("active", true, "exp",
expiredSecondsAgo));
+
+ KeycloakSecurityPolicy policy = introspectionPolicy(introspector);
+ policy.setValidateIssuer(false);
+ policy.setRequiredPermissions("read");
+
+ AtomicBoolean routeReached = new AtomicBoolean(false);
+ KeycloakSecurityProcessor processor = new KeycloakSecurityProcessor(e
-> routeReached.set(true), policy);
+
+ CamelAuthorizationException e
+ = assertThrows(CamelAuthorizationException.class, () ->
processor.process(bearer("x")));
+ assertTrue(e.getMessage().contains("expired"), "unexpected message: "
+ e.getMessage());
+ assertFalse(routeReached.get(), "Route body must not be reached for an
expired token on the permissions path");
+ }
}
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
index 8f9d9ddee58f..88d45dfe6668 100644
---
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
@@ -18,11 +18,13 @@ package org.apache.camel.component.keycloak.security.cache;
import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
import org.apache.camel.component.keycloak.security.KeycloakTokenIntrospector;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.*;
class CaffeineTokenCacheTest {
@@ -199,4 +201,64 @@ class CaffeineTokenCacheTest {
defaultCache.close();
}
+
+ @Test
+ void testExpiredResultNotServed() {
+ // A result whose token has already expired is given a 0 lifetime by
IntrospectionExpiry
+ // (expiryNanos returns 0), so it is evicted immediately rather than
kept for the TTL. The TTL
+ // bounding for a not-yet-expired token is covered by
testResultExpiringBeforeTtlNotServedAfterExp.
+ 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);
+ cache.getCaffeineCache().cleanUp();
+
+ assertNull(cache.get("expired-token"));
+ }
+
+ @Test
+ void testResultWithFutureExpirationServed() {
+ Map<String, Object> claims = new HashMap<>();
+ claims.put("active", true);
+ claims.put("sub", "test-user");
+ claims.put("exp", System.currentTimeMillis() / 1000 + 300); // valid
for 5 more minutes
+ KeycloakTokenIntrospector.IntrospectionResult valid
+ = new KeycloakTokenIntrospector.IntrospectionResult(claims);
+
+ cache.put("valid-token", valid);
+
+ KeycloakTokenIntrospector.IntrospectionResult retrieved =
cache.get("valid-token");
+ assertNotNull(retrieved);
+ assertTrue(retrieved.isActive());
+ }
+
+ @Test
+ void testResultExpiringBeforeTtlNotServedAfterExp() {
+ // The token's exp lands inside the TTL window (~2s vs a 300s TTL), so
the entry must expire at exp,
+ // not at the configured TTL. This exercises
IntrospectionExpiry.expiryNanos()'s min(ttl, remaining):
+ // returning maxTtlNanos unconditionally would keep the entry served
for 300s and fail this test.
+ CaffeineTokenCache longTtlCache = new CaffeineTokenCache(300, 100,
true);
+ Map<String, Object> claims = new HashMap<>();
+ claims.put("active", true);
+ claims.put("sub", "test-user");
+ claims.put("exp", System.currentTimeMillis() / 1000 + 2); // expires
in ~2 seconds
+ KeycloakTokenIntrospector.IntrospectionResult shortLived
+ = new KeycloakTokenIntrospector.IntrospectionResult(claims);
+
+ try {
+ longTtlCache.put("short-lived-token", shortLived);
+ assertNotNull(longTtlCache.get("short-lived-token"));
+
+ await().atMost(10, TimeUnit.SECONDS).until(() -> {
+ longTtlCache.getCaffeineCache().cleanUp();
+ return longTtlCache.get("short-lived-token") == null;
+ });
+ } finally {
+ longTtlCache.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
index d08fbeee74f9..6656995856c8 100644
---
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
@@ -18,11 +18,13 @@ package org.apache.camel.component.keycloak.security.cache;
import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
import org.apache.camel.component.keycloak.security.KeycloakTokenIntrospector;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.*;
class ConcurrentMapTokenCacheTest {
@@ -172,4 +174,56 @@ class ConcurrentMapTokenCacheTest {
assertEquals(threadCount, cache.size());
}
+
+ @Test
+ void testExpiredResultNotServed() {
+ // A result whose token has already expired is not cached at all:
put() rejects it up front via the
+ // isExpired() early-return, so get() returns null because no entry
was ever inserted. The TTL bounding
+ // for a not-yet-expired token is covered by
testResultExpiringBeforeTtlNotServedAfterExp.
+ 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"));
+ }
+
+ @Test
+ void testResultWithFutureExpirationServed() {
+ Map<String, Object> claims = new HashMap<>();
+ claims.put("active", true);
+ claims.put("sub", "test-user");
+ claims.put("exp", System.currentTimeMillis() / 1000 + 300); // valid
for 5 more minutes
+ KeycloakTokenIntrospector.IntrospectionResult valid
+ = new KeycloakTokenIntrospector.IntrospectionResult(claims);
+
+ cache.put("valid-token", valid);
+
+ KeycloakTokenIntrospector.IntrospectionResult retrieved =
cache.get("valid-token");
+ assertNotNull(retrieved);
+ assertTrue(retrieved.isActive());
+ }
+
+ @Test
+ void testResultExpiringBeforeTtlNotServedAfterExp() {
+ // The token's exp lands inside the TTL window (~2s vs a 300s TTL), so
the entry must expire at exp,
+ // not at the configured TTL. This exercises effectiveTtlMillis()'s
min(ttl, remaining): replacing that
+ // with a plain ttlMillis would keep the entry served for 300s and
fail this test.
+ ConcurrentMapTokenCache longTtlCache = new
ConcurrentMapTokenCache(300);
+ Map<String, Object> claims = new HashMap<>();
+ claims.put("active", true);
+ claims.put("sub", "test-user");
+ claims.put("exp", System.currentTimeMillis() / 1000 + 2); // expires
in ~2 seconds
+ KeycloakTokenIntrospector.IntrospectionResult shortLived
+ = new KeycloakTokenIntrospector.IntrospectionResult(claims);
+
+ longTtlCache.put("short-lived-token", shortLived);
+ assertNotNull(longTtlCache.get("short-lived-token"));
+
+ await().atMost(10, TimeUnit.SECONDS).until(() ->
longTtlCache.get("short-lived-token") == null);
+ }
}