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

coheigea pushed a commit to branch coheigea/TokenReplayCache
in repository https://gitbox.apache.org/repos/asf/cxf.git

commit bb7521f7991c335ad4dfc8ce219eab91f5ca53ea
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Mon Nov 9 07:47:32 2026 +0000

    Reject SAML assertions that outlive the TokenReplayCache
---
 .../security/oauth2/saml/SamlOAuthValidator.java   | 18 +++++
 .../oauth2/saml/SamlOAuthValidatorTest.java        | 81 +++++++++++++++++++++-
 .../AbstractRequestAssertionConsumerHandler.java   | 11 +++
 .../security/saml/sso/EHCacheTokenReplayCache.java | 14 ++++
 .../saml/sso/SAMLSSOResponseValidator.java         | 19 +++++
 .../saml/sso/SAMLSSOResponseValidatorTest.java     | 54 +++++++++++++++
 .../rs/security/saml/sso/TokenReplayCacheTest.java | 13 ++++
 .../cxf/rs/security/common/TokenReplayCache.java   | 11 +++
 .../common/jcache/JCacheTokenReplayCache.java      | 50 +++++++++----
 .../rs/security/common/TokenReplayCacheTest.java   | 44 ++++++++++++
 .../src/test/resources/custom-replay-jcache.xml    | 43 ++++++++++++
 11 files changed, 344 insertions(+), 14 deletions(-)

diff --git 
a/rt/rs/security/oauth-parent/oauth2-saml/src/main/java/org/apache/cxf/rs/security/oauth2/saml/SamlOAuthValidator.java
 
b/rt/rs/security/oauth-parent/oauth2-saml/src/main/java/org/apache/cxf/rs/security/oauth2/saml/SamlOAuthValidator.java
index 95f326cafa7..58512238eab 100644
--- 
a/rt/rs/security/oauth-parent/oauth2-saml/src/main/java/org/apache/cxf/rs/security/oauth2/saml/SamlOAuthValidator.java
+++ 
b/rt/rs/security/oauth-parent/oauth2-saml/src/main/java/org/apache/cxf/rs/security/oauth2/saml/SamlOAuthValidator.java
@@ -19,6 +19,7 @@
 
 package org.apache.cxf.rs.security.oauth2.saml;
 
+import java.time.Duration;
 import java.time.Instant;
 import java.util.List;
 
@@ -43,6 +44,7 @@ public class SamlOAuthValidator {
     private String clientAddress;
     private boolean subjectConfirmationDataRequired;
     private TokenReplayCache<String> replayCache;
+    private boolean allowAssertionsBeyondReplayCacheTTL;
     public SamlOAuthValidator() {
     }
 
@@ -76,6 +78,15 @@ public class SamlOAuthValidator {
         return replayCache;
     }
 
+    /**
+     * Whether to accept an assertion that is valid for longer than the replay 
cache can hold its ID (see
+     * TokenReplayCache#getMaxTTL()). Such an assertion can be replayed once 
its ID has expired from the
+     * cache, so the default is false.
+     */
+    public void setAllowAssertionsBeyondReplayCacheTTL(boolean 
allowAssertionsBeyondReplayCacheTTL) {
+        this.allowAssertionsBeyondReplayCacheTTL = 
allowAssertionsBeyondReplayCacheTTL;
+    }
+
     public void validate(Message message, SamlAssertionWrapper wrapper) {
         validateSAMLVersion(wrapper);
 
@@ -107,6 +118,13 @@ public class SamlOAuthValidator {
             return;
         }
         Instant expiry = getReplayCacheExpiry(cs, 
wrapper.getSaml2().getSubject());
+        // The cache only holds the ID for so long. An assertion that is valid 
for longer could be
+        // replayed once its ID has expired from the cache, so reject it
+        Duration maxTTL = replayCache.getMaxTTL();
+        if (!allowAssertionsBeyondReplayCacheTTL && maxTTL != null && expiry 
!= null
+            && expiry.isAfter(Instant.now().plus(maxTTL))) {
+            throw ExceptionUtils.toNotAuthorizedException(null, null);
+        }
         if (!replayCache.putIdIfAbsent(wrapper.getId(), expiry)) {
             throw ExceptionUtils.toNotAuthorizedException(null, null);
         }
diff --git 
a/rt/rs/security/oauth-parent/oauth2-saml/src/test/java/org/apache/cxf/rs/security/oauth2/saml/SamlOAuthValidatorTest.java
 
b/rt/rs/security/oauth-parent/oauth2-saml/src/test/java/org/apache/cxf/rs/security/oauth2/saml/SamlOAuthValidatorTest.java
index 46187b27935..ab8ef3fe15f 100644
--- 
a/rt/rs/security/oauth-parent/oauth2-saml/src/test/java/org/apache/cxf/rs/security/oauth2/saml/SamlOAuthValidatorTest.java
+++ 
b/rt/rs/security/oauth-parent/oauth2-saml/src/test/java/org/apache/cxf/rs/security/oauth2/saml/SamlOAuthValidatorTest.java
@@ -136,15 +136,78 @@ public class SamlOAuthValidatorTest {
         validator.validate(null, wrapper);
     }
 
+    // An assertion that is valid for longer than the replay cache can hold 
its ID could be replayed once
+    // the ID has expired from the cache, so it is rejected
+    @Test
+    public void testAssertionValidBeyondReplayCacheTTLIsRejected() throws 
Exception {
+        String tokenServiceAddress = "https://service.example.org/token";;
+
+        SamlOAuthValidator validator = new SamlOAuthValidator();
+        validator.setAccessTokenServiceAddress(tokenServiceAddress);
+        TestTokenReplayCache replayCache = new 
TestTokenReplayCache(Duration.ofHours(12));
+        validator.setReplayCache(replayCache);
+
+        SamlAssertionWrapper wrapper = createAssertion(tokenServiceAddress, 
null, Duration.ofHours(24));
+        try {
+            validator.validate(null, wrapper);
+            fail("Expected failure as the assertion is valid for longer than 
the replay cache holds it");
+        } catch (NotAuthorizedException ex) {
+            // expected
+        }
+        assertFalse(replayCache.contains(wrapper.getId()));
+
+        // An assertion that the cache can hold is accepted
+        validator.validate(null, createAssertion(tokenServiceAddress, null));
+    }
+
+    @Test
+    public void testAssertionValidBeyondReplayCacheTTLIsAllowed() throws 
Exception {
+        String tokenServiceAddress = "https://service.example.org/token";;
+
+        SamlOAuthValidator validator = new SamlOAuthValidator();
+        validator.setAccessTokenServiceAddress(tokenServiceAddress);
+        validator.setReplayCache(new 
TestTokenReplayCache(Duration.ofHours(12)));
+        validator.setAllowAssertionsBeyondReplayCacheTTL(true);
+
+        SamlAssertionWrapper wrapper = createAssertion(tokenServiceAddress, 
null, Duration.ofHours(24));
+        validator.validate(null, wrapper);
+
+        // A replay is still detected
+        try {
+            validator.validate(null, wrapper);
+            fail("Expected failure on replayed assertion");
+        } catch (NotAuthorizedException ex) {
+            // expected
+        }
+    }
+
+    // A replay cache that does not say how long it holds an ID for is not 
checked
+    @Test
+    public void testAssertionValidBeyondUnknownReplayCacheTTL() throws 
Exception {
+        String tokenServiceAddress = "https://service.example.org/token";;
+
+        SamlOAuthValidator validator = new SamlOAuthValidator();
+        validator.setAccessTokenServiceAddress(tokenServiceAddress);
+        validator.setReplayCache(new TestTokenReplayCache());
+
+        validator.validate(null, createAssertion(tokenServiceAddress, null, 
Duration.ofHours(24)));
+    }
+
     private SamlAssertionWrapper createAssertion(String recipient, String 
clientAddress) throws Exception {
+        return createAssertion(recipient, clientAddress, 
Duration.ofMinutes(5));
+    }
+
+    private SamlAssertionWrapper createAssertion(
+        String recipient, String clientAddress, Duration validity
+    ) throws Exception {
         SubjectConfirmationDataBean confirmationData = new 
SubjectConfirmationDataBean();
         confirmationData.setRecipient(recipient);
         confirmationData.setAddress(clientAddress);
-        
confirmationData.setNotAfter(Instant.now().plus(Duration.ofMinutes(5)));
+        confirmationData.setNotAfter(Instant.now().plus(validity));
 
         ConditionsBean conditions = new ConditionsBean();
         conditions.setNotBefore(Instant.now().minusSeconds(30));
-        conditions.setNotAfter(Instant.now().plus(Duration.ofMinutes(5)));
+        conditions.setNotAfter(Instant.now().plus(validity));
         AudienceRestrictionBean audienceRestriction = new 
AudienceRestrictionBean();
         
audienceRestriction.setAudienceURIs(Collections.singletonList(recipient));
         
conditions.setAudienceRestrictions(Collections.singletonList(audienceRestriction));
@@ -156,6 +219,20 @@ public class SamlOAuthValidatorTest {
 
     private static final class TestTokenReplayCache implements 
TokenReplayCache<String> {
         private final Set<String> ids = ConcurrentHashMap.newKeySet();
+        private final Duration maxTTL;
+
+        TestTokenReplayCache() {
+            this(null);
+        }
+
+        TestTokenReplayCache(Duration maxTTL) {
+            this.maxTTL = maxTTL;
+        }
+
+        @Override
+        public Duration getMaxTTL() {
+            return maxTTL;
+        }
 
         @Override
         public boolean contains(String id) {
diff --git 
a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/AbstractRequestAssertionConsumerHandler.java
 
b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/AbstractRequestAssertionConsumerHandler.java
index aa0fde871e7..643aac9795c 100644
--- 
a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/AbstractRequestAssertionConsumerHandler.java
+++ 
b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/AbstractRequestAssertionConsumerHandler.java
@@ -70,6 +70,7 @@ public abstract class AbstractRequestAssertionConsumerHandler 
extends AbstractSS
     private boolean keyInfoMustBeAvailable = true;
     private boolean checkClientAddress = true;
     private boolean enforceResponseSigned;
+    private boolean allowAssertionsBeyondReplayCacheTTL;
     private TokenReplayCache<String> replayCache;
 
     private MessageContext messageContext;
@@ -116,6 +117,15 @@ public abstract class 
AbstractRequestAssertionConsumerHandler extends AbstractSS
         this.enforceKnownIssuer = enforceKnownIssuer;
     }
 
+    /**
+     * Whether to accept an Assertion that is valid for longer than the replay 
cache can hold its
+     * identifier (see TokenReplayCache#getMaxTTL()). Such an Assertion can be 
replayed once its identifier
+     * has expired from the cache, so the default is false.
+     */
+    public void setAllowAssertionsBeyondReplayCacheTTL(boolean 
allowAssertionsBeyondReplayCacheTTL) {
+        this.allowAssertionsBeyondReplayCacheTTL = 
allowAssertionsBeyondReplayCacheTTL;
+    }
+
     public void setSupportBase64Encoding(boolean supportBase64Encoding) {
         this.supportBase64Encoding = supportBase64Encoding;
     }
@@ -357,6 +367,7 @@ public abstract class 
AbstractRequestAssertionConsumerHandler extends AbstractSS
             
ssoResponseValidator.setEnforceResponseSigned(enforceResponseSigned);
             ssoResponseValidator.setEnforceKnownIssuer(enforceKnownIssuer);
             ssoResponseValidator.setReplayCache(getReplayCache());
+            
ssoResponseValidator.setAllowAssertionsBeyondReplayCacheTTL(allowAssertionsBeyondReplayCacheTTL);
 
             return ssoResponseValidator.validateSamlResponse(samlResponse, 
postBinding);
         } catch (Exception ex) {
diff --git 
a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/EHCacheTokenReplayCache.java
 
b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/EHCacheTokenReplayCache.java
index 2a814646382..925d36f644c 100644
--- 
a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/EHCacheTokenReplayCache.java
+++ 
b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/EHCacheTokenReplayCache.java
@@ -22,12 +22,14 @@ package org.apache.cxf.rs.security.saml.sso;
 import java.io.File;
 import java.io.IOException;
 import java.net.URL;
+import java.time.Duration;
 import java.time.Instant;
 import java.util.Random;
 
 import org.apache.cxf.Bus;
 import org.apache.cxf.BusFactory;
 import org.apache.cxf.jaxrs.utils.ResourceUtils;
+import org.apache.wss4j.common.cache.EHCacheExpiry;
 import org.apache.wss4j.common.cache.EHCacheReplayCache;
 import org.apache.wss4j.common.cache.EHCacheValue;
 import org.apache.wss4j.common.util.Loader;
@@ -167,6 +169,18 @@ public class EHCacheTokenReplayCache implements 
TokenReplayCache<String> {
         return element != null;
     }
 
+    /**
+     * Return the maximum time for which this cache holds an identifier: the 
max TTL of EHCacheExpiry if
+     * the cache uses it (as the default configuration does), or null (not 
known) otherwise
+     */
+    @Override
+    public Duration getMaxTTL() {
+        if (cache.getRuntimeConfiguration().getExpiryPolicy() instanceof 
EHCacheExpiry) {
+            return Duration.ofSeconds(EHCacheExpiry.MAX_TTL);
+        }
+        return null;
+    }
+
     public synchronized void close() {
         if (cacheManager.getStatus() == Status.AVAILABLE) {
             cacheManager.removeCache(CACHE_KEY);
diff --git 
a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidator.java
 
b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidator.java
index 889539e1aa0..1830abf97d0 100644
--- 
a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidator.java
+++ 
b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidator.java
@@ -19,6 +19,7 @@
 package org.apache.cxf.rs.security.saml.sso;
 
 import java.net.URI;
+import java.time.Duration;
 import java.time.Instant;
 import java.util.List;
 import java.util.logging.Logger;
@@ -50,6 +51,7 @@ public class SAMLSSOResponseValidator {
     private boolean enforceKnownIssuer = true;
     private boolean enforceStrictIssuerMatch;
     private TokenReplayCache<String> replayCache;
+    private boolean allowAssertionsBeyondReplayCacheTTL;
 
     /**
      * Enforce that Assertions contained in the Response must be signed (if 
the Response itself is not
@@ -303,6 +305,14 @@ public class SAMLSSOResponseValidator {
         // applies to every binding, not just POST, as a Response received via 
GET can be replayed too
         if (replayCache != null) {
             Instant expires = subjectConfData.getNotOnOrAfter();
+            // The cache only holds the identifier for so long. An Assertion 
that is valid for longer
+            // could be replayed once its identifier has expired from the 
cache, so reject it
+            Duration maxTTL = replayCache.getMaxTTL();
+            if (!allowAssertionsBeyondReplayCacheTTL && maxTTL != null
+                && expires.isAfter(Instant.now().plus(maxTTL))) {
+                LOG.warning("The Assertion " + id + " is valid for longer than 
the replay cache can hold it");
+                throw new 
WSSecurityException(WSSecurityException.ErrorCode.FAILURE, 
"invalidSAMLsecurity");
+            }
             if (!replayCache.putIdIfAbsent(id, expires)) {
                 LOG.warning("Replay attack with token id: " + id);
                 throw new 
WSSecurityException(WSSecurityException.ErrorCode.FAILURE, 
"invalidSAMLsecurity");
@@ -419,6 +429,15 @@ public class SAMLSSOResponseValidator {
         this.replayCache = replayCache;
     }
 
+    /**
+     * Whether to accept an Assertion that is valid for longer than the replay 
cache can hold its
+     * identifier (see TokenReplayCache#getMaxTTL()). Such an Assertion can be 
replayed once its identifier
+     * has expired from the cache, so the default is false.
+     */
+    public void setAllowAssertionsBeyondReplayCacheTTL(boolean 
allowAssertionsBeyondReplayCacheTTL) {
+        this.allowAssertionsBeyondReplayCacheTTL = 
allowAssertionsBeyondReplayCacheTTL;
+    }
+
     public boolean isEnforceResponseSigned() {
         return enforceResponseSigned;
     }
diff --git 
a/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidatorTest.java
 
b/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidatorTest.java
index 5a033a1e8f3..398f5fa3c9b 100644
--- 
a/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidatorTest.java
+++ 
b/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidatorTest.java
@@ -781,6 +781,60 @@ public class SAMLSSOResponseValidatorTest {
         }
     }
 
+    // An assertion that is valid for longer than the replay cache can hold 
its id could be replayed once
+    // the id has expired from the cache, so it is rejected
+    @org.junit.Test
+    public void testAssertionValidBeyondReplayCacheTTLIsRejected() throws 
Exception {
+        Response response = 
createResponse(createLongLivedSubjectConfirmationData());
+
+        TokenReplayCache<String> replayCache = new EHCacheTokenReplayCache();
+        try {
+            SAMLSSOResponseValidator validator = 
createReplayValidator(replayCache);
+            try {
+                validator.validateSamlResponse(response, true);
+                fail("Expected failure as the assertion is valid for longer 
than the replay cache holds it");
+            } catch (WSSecurityException ex) {
+                // expected
+            }
+        } finally {
+            replayCache.close();
+        }
+    }
+
+    @org.junit.Test
+    public void testAssertionValidBeyondReplayCacheTTLIsAllowed() throws 
Exception {
+        Response response = 
createResponse(createLongLivedSubjectConfirmationData());
+
+        TokenReplayCache<String> replayCache = new EHCacheTokenReplayCache();
+        try {
+            SAMLSSOResponseValidator validator = 
createReplayValidator(replayCache);
+            validator.setAllowAssertionsBeyondReplayCacheTTL(true);
+
+            SSOValidatorResponse validateSamlResponse = 
validator.validateSamlResponse(response, true);
+            assertEquals(response.getID(), 
validateSamlResponse.getResponseId());
+
+            // A replay is still detected
+            try {
+                validator.validateSamlResponse(copyResponse(response), true);
+                fail("Expected failure on replayed response");
+            } catch (WSSecurityException ex) {
+                // expected
+            }
+        } finally {
+            replayCache.close();
+        }
+    }
+
+    // Valid for 24 hours, longer than the max TTL of 12 hours of the default 
replay cache
+    private static SubjectConfirmationDataBean 
createLongLivedSubjectConfirmationData() {
+        SubjectConfirmationDataBean subjectConfirmationData = new 
SubjectConfirmationDataBean();
+        subjectConfirmationData.setAddress("http://apache.org";);
+        subjectConfirmationData.setInResponseTo("12345");
+        
subjectConfirmationData.setNotAfter(Instant.now().plus(Duration.ofHours(24)));
+        subjectConfirmationData.setRecipient("http://recipient.apache.org";);
+        return subjectConfirmationData;
+    }
+
     private SAMLSSOResponseValidator 
createReplayValidator(TokenReplayCache<String> replayCache) {
         SAMLSSOResponseValidator validator = new SAMLSSOResponseValidator();
         validator.setEnforceAssertionsSigned(false);
diff --git 
a/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/TokenReplayCacheTest.java
 
b/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/TokenReplayCacheTest.java
index 8850fe5a803..b98fdc10408 100644
--- 
a/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/TokenReplayCacheTest.java
+++ 
b/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/TokenReplayCacheTest.java
@@ -19,6 +19,7 @@
 
 package org.apache.cxf.rs.security.saml.sso;
 
+import java.time.Duration;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
@@ -34,6 +35,7 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicIntegerArray;
 
 import org.apache.cxf.rs.security.saml.sso.jcache.JCacheTokenReplayCache;
+import org.apache.wss4j.common.cache.EHCacheExpiry;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -179,6 +181,17 @@ public class TokenReplayCacheTest {
         }
     }
 
+    // The default configuration holds an identifier for at most the max TTL 
of EHCacheExpiry
+    @Test
+    public void testMaxTTL() throws Exception {
+        TokenReplayCache<String> replayCache = createCache();
+        try {
+            assertEquals(Duration.ofSeconds(EHCacheExpiry.MAX_TTL), 
replayCache.getMaxTTL());
+        } finally {
+            replayCache.close();
+        }
+    }
+
     private void testTokenReplayCacheInstance(TokenReplayCache<String> 
replayCache) throws InterruptedException {
 
         // Test default TTL caches OK
diff --git 
a/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/common/TokenReplayCache.java
 
b/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/common/TokenReplayCache.java
index 94c9cb80648..8a694b29011 100644
--- 
a/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/common/TokenReplayCache.java
+++ 
b/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/common/TokenReplayCache.java
@@ -21,6 +21,7 @@ package org.apache.cxf.rs.security.common;
 
 import java.io.Closeable;
 import java.io.IOException;
+import java.time.Duration;
 import java.time.Instant;
 
 /**
@@ -59,5 +60,15 @@ public interface TokenReplayCache<T> extends Closeable {
         }
     }
 
+    /**
+     * Return the maximum time for which this cache holds an identifier. An 
identifier whose token is
+     * valid for longer than this can be replayed once it has expired from the 
cache, so a replay check
+     * should reject such a token.
+     * @return the maximum time for which this cache holds an identifier, or 
null if this is not known
+     */
+    default Duration getMaxTTL() {
+        return null;
+    }
+
     void close() throws IOException;
 }
diff --git 
a/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/common/jcache/JCacheTokenReplayCache.java
 
b/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/common/jcache/JCacheTokenReplayCache.java
index 12502eccd4c..7a6037c664f 100644
--- 
a/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/common/jcache/JCacheTokenReplayCache.java
+++ 
b/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/common/jcache/JCacheTokenReplayCache.java
@@ -23,7 +23,9 @@ import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.time.Duration;
 import java.time.Instant;
+import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -31,6 +33,7 @@ import javax.cache.Cache;
 import javax.cache.CacheManager;
 import javax.cache.Caching;
 import javax.cache.configuration.MutableConfiguration;
+import javax.cache.expiry.CreatedExpiryPolicy;
 import javax.cache.spi.CachingProvider;
 
 import org.apache.cxf.Bus;
@@ -38,12 +41,15 @@ import org.apache.cxf.BusFactory;
 import org.apache.cxf.common.logging.LogUtils;
 import org.apache.cxf.jaxrs.utils.ResourceUtils;
 import org.apache.cxf.rs.security.common.TokenReplayCache;
+import org.apache.wss4j.common.cache.EHCacheExpiry;
 import org.apache.wss4j.common.cache.EHCacheValue;
 import org.apache.wss4j.common.util.Loader;
 
 /**
  * A JCache based implementation of the TokenReplayCache interface. The 
default configuration uses
- * Ehcache as the JCache provider, with a default TTL of 60 minutes and a max 
TTL of 12 hours.
+ * Ehcache as the JCache provider, with a default TTL of 60 minutes and a max 
TTL of 12 hours. If the
+ * JCache provider does not define the cache (for example, a provider other 
than Ehcache, which cannot
+ * read the default configuration), it is created with a fixed TTL of 12 hours.
  */
 public class JCacheTokenReplayCache implements TokenReplayCache<String> {
 
@@ -55,6 +61,7 @@ public class JCacheTokenReplayCache implements 
TokenReplayCache<String> {
     private final String cacheKey;
     private final Cache<String, EHCacheValue> cache;
     private final CacheManager cacheManager;
+    private final Duration maxTTL;
 
     public JCacheTokenReplayCache() throws URISyntaxException {
         this(DEFAULT_CONFIG_URL, null);
@@ -99,19 +106,29 @@ public class JCacheTokenReplayCache implements 
TokenReplayCache<String> {
             ? resolvedConfigFileURL.toURI() : cachingProvider.getDefaultURI();
         cacheManager = cachingProvider.getCacheManager(configURI, 
getClass().getClassLoader());
 
-        cache = getOrCreate(cacheManager, cacheKey, String.class, 
EHCacheValue.class);
-    }
-
-    private static <K, V> Cache<K, V> getOrCreate(CacheManager cacheManager, 
String name,
-            Class<K> kclass, Class<V> vclass) {
+        Cache<String, EHCacheValue> configuredCache =
+            cacheManager.getCache(cacheKey, String.class, EHCacheValue.class);
+        cache = configuredCache != null ? configuredCache : 
createCache(cacheManager, cacheKey);
 
-        final Cache<K, V> cache = cacheManager.getCache(name, kclass, vclass);
-        if (cache != null) {
-            return cache;
-        }
+        // With the default configuration, the cache uses EHCacheExpiry, which 
holds an entry for at most
+        // its max TTL. Without a configuration that defines the cache, it 
gets the same max TTL from
+        // createCache. How long a cache defined by some other configuration 
holds an entry for is not
+        // known.
+        boolean defaultConfig = configFileURL == null || 
defaultConfigFile.equals(configFile);
+        maxTTL = defaultConfig ? Duration.ofSeconds(EHCacheExpiry.MAX_TTL) : 
null;
+    }
 
-        final MutableConfiguration<K, V> cacheConfiguration = new 
MutableConfiguration<>();
-        cacheConfiguration.setTypes(kclass, vclass);
+    /**
+     * Create the cache, if the JCache provider does not define it. A 
configuration (such as the default
+     * one) can still supply its settings, including its expiry, through a 
template for the cache name.
+     * Without one, every entry is held for the max TTL (JCache has no 
per-entry expiry), rather than
+     * forever.
+     */
+    private static Cache<String, EHCacheValue> createCache(CacheManager 
cacheManager, String name) {
+        final MutableConfiguration<String, EHCacheValue> cacheConfiguration = 
new MutableConfiguration<>();
+        cacheConfiguration.setTypes(String.class, EHCacheValue.class);
+        
cacheConfiguration.setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(
+            new javax.cache.expiry.Duration(TimeUnit.SECONDS, 
EHCacheExpiry.MAX_TTL)));
 
         return cacheManager.createCache(name, cacheConfiguration);
     }
@@ -183,6 +200,15 @@ public class JCacheTokenReplayCache implements 
TokenReplayCache<String> {
         return element != null;
     }
 
+    /**
+     * Return the maximum time for which this cache holds an identifier: the 
max TTL of 12 hours with the
+     * default configuration, or null (not known) with some other configuration
+     */
+    @Override
+    public Duration getMaxTTL() {
+        return maxTTL;
+    }
+
     public synchronized void close() {
         if (!cacheManager.isClosed()) {
             cacheManager.destroyCache(cacheKey);
diff --git 
a/rt/rs/security/xml/src/test/java/org/apache/cxf/rs/security/common/TokenReplayCacheTest.java
 
b/rt/rs/security/xml/src/test/java/org/apache/cxf/rs/security/common/TokenReplayCacheTest.java
index f761ded111e..a5a1d60e953 100644
--- 
a/rt/rs/security/xml/src/test/java/org/apache/cxf/rs/security/common/TokenReplayCacheTest.java
+++ 
b/rt/rs/security/xml/src/test/java/org/apache/cxf/rs/security/common/TokenReplayCacheTest.java
@@ -19,6 +19,7 @@
 
 package org.apache.cxf.rs.security.common;
 
+import java.time.Duration;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
@@ -34,6 +35,7 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicIntegerArray;
 
 import org.apache.cxf.rs.security.common.jcache.JCacheTokenReplayCache;
+import org.apache.wss4j.common.cache.EHCacheExpiry;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -41,6 +43,7 @@ import org.junit.runners.Parameterized;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 /**
@@ -163,6 +166,47 @@ public class TokenReplayCacheTest {
         }
     }
 
+    // The default configuration holds an identifier for at most the max TTL 
of EHCacheExpiry
+    @Test
+    public void testMaxTTL() throws Exception {
+        TokenReplayCache<String> replayCache = createCache();
+        try {
+            assertEquals(Duration.ofSeconds(EHCacheExpiry.MAX_TTL), 
replayCache.getMaxTTL());
+        } finally {
+            replayCache.close();
+        }
+    }
+
+    // A cache that the configuration does not define is created with a fixed 
TTL of the max TTL
+    @Test
+    public void testMaxTTLOfCreatedCache() throws Exception {
+        TokenReplayCache<String> replayCache = new JCacheTokenReplayCache(
+            "cxf.rs.security.replay.cache.test-" + UUID.randomUUID(),
+            "/cxf-rs-security-replay-jcache.xml", 
"/cxf-rs-security-replay-jcache.xml", null) { };
+        try {
+            assertEquals(Duration.ofSeconds(EHCacheExpiry.MAX_TTL), 
replayCache.getMaxTTL());
+
+            String id = UUID.randomUUID().toString();
+            assertTrue(replayCache.putIdIfAbsent(id, null));
+            assertTrue(replayCache.contains(id));
+            assertFalse(replayCache.putIdIfAbsent(id, null));
+        } finally {
+            replayCache.close();
+        }
+    }
+
+    // How long a cache defined by a configuration other than the default one 
holds an entry for is not
+    // known
+    @Test
+    public void testMaxTTLOfCustomConfiguration() throws Exception {
+        TokenReplayCache<String> replayCache = new 
JCacheTokenReplayCache("/custom-replay-jcache.xml");
+        try {
+            assertNull(replayCache.getMaxTTL());
+        } finally {
+            replayCache.close();
+        }
+    }
+
     private TokenReplayCache<String> createCache() throws Exception {
         return tokenReplayCacheClass.getDeclaredConstructor().newInstance();
     }
diff --git a/rt/rs/security/xml/src/test/resources/custom-replay-jcache.xml 
b/rt/rs/security/xml/src/test/resources/custom-replay-jcache.xml
new file mode 100644
index 00000000000..c80469a8728
--- /dev/null
+++ b/rt/rs/security/xml/src/test/resources/custom-replay-jcache.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0"?>
+<!--
+  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.
+-->
+<config
+        xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
+        xmlns='http://www.ehcache.org/v3'
+        xmlns:jsr107='http://www.ehcache.org/v3/jsr107'
+        xsi:schemaLocation="http://www.ehcache.org/v3 
http://www.ehcache.org/schema/ehcache-core-3.8.xsd
+            http://www.ehcache.org/v3/jsr107 
http://www.ehcache.org/schema/ehcache-107-ext-3.0.xsd";>
+    <service>
+        <jsr107:defaults>
+            <jsr107:cache name="cxf.rs.security.replay.cache" 
template="cxf.rs.security.replay.cache.template"/>
+        </jsr107:defaults>
+    </service>
+    <persistence 
directory="${java.io.tmpdir}/ehcache-cxf-rs-security-replay-custom/" />
+    <cache-template name="cxf.rs.security.replay.cache.template">
+        <key-type>java.lang.String</key-type>
+        <value-type>org.apache.wss4j.common.cache.EHCacheValue</value-type>
+        <expiry>
+            <ttl unit="seconds">300</ttl>
+        </expiry>
+        <resources>
+            <heap unit="entries">5000</heap>
+            <disk unit="MB" persistent="false">10</disk>
+        </resources>
+    </cache-template>
+</config>

Reply via email to