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>
