This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch CXF-7594 in repository https://gitbox.apache.org/repos/asf/cxf.git
commit 543b8b0d25cf9a9636671bbe7b599d0288811dbf Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Fri Apr 10 12:29:57 2020 +0100 Ported SAML SSO TokenReplayCache --- rt/rs/security/sso/saml/pom.xml | 8 +- .../AbstractRequestAssertionConsumerHandler.java | 4 +- .../security/saml/sso/EHCacheTokenReplayCache.java | 167 +++++++++------------ .../saml/sso/SAMLSSOResponseValidator.java | 7 +- .../cxf/rs/security/saml/sso/TokenReplayCache.java | 5 +- .../saml/src/main/resources/cxf-samlp-ehcache.xml | 23 ++- .../cxf/rs/security/saml/sso/EHCacheUtilTest.java | 59 -------- .../rs/security/saml/sso/TokenReplayCacheTest.java | 108 +++++++++++++ .../saml/src/test/resources/cxf-test-ehcache.xml | 5 - 9 files changed, 216 insertions(+), 170 deletions(-) diff --git a/rt/rs/security/sso/saml/pom.xml b/rt/rs/security/sso/saml/pom.xml index b932556..014d1be 100644 --- a/rt/rs/security/sso/saml/pom.xml +++ b/rt/rs/security/sso/saml/pom.xml @@ -34,7 +34,7 @@ <cxf.module.name>org.apache.cxf.rs.security.sso.saml</cxf.module.name> <cxf.osgi.import> javax.servlet*;version="${cxf.osgi.javax.servlet.version}", - net.sf.ehcache*;resolution:=optional;version="[2.5, 3.0.0)", + org.ehcache*;resolution:=optional;version="[3.0.0, 4.0.0)", org.opensaml*;version="${cxf.opensaml.osgi.version.range}", javax.xml.bind*;version="${cxf.osgi.javax.bind.version}", javax.annotation*;version="${cxf.osgi.javax.annotation.version}" @@ -71,5 +71,11 @@ <version>${cxf.ehcache.version}</version> <scope>compile</scope> </dependency> + <dependency> + <groupId>org.ehcache</groupId> + <artifactId>ehcache</artifactId> + <version>${cxf.ehcache3.version}</version> + <scope>compile</scope> + </dependency> </dependencies> </project> 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 8bf89e2..82c7492 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 @@ -92,7 +92,7 @@ public abstract class AbstractRequestAssertionConsumerHandler extends AbstractSS this.replayCache = replayCache; } - public TokenReplayCache<String> getReplayCache() { + public TokenReplayCache<String> getReplayCache() throws Exception { if (replayCache == null) { Bus bus = (Bus)messageContext.getContextualProperty(Bus.class.getName()); replayCache = new EHCacheTokenReplayCache(bus); @@ -359,7 +359,7 @@ public abstract class AbstractRequestAssertionConsumerHandler extends AbstractSS } return ssoResponseValidator.validateSamlResponse(samlResponse, postBinding); - } catch (WSSecurityException ex) { + } catch (Exception ex) { reportError("INVALID_SAML_RESPONSE"); throw ExceptionUtils.toBadRequestException(ex, null); } 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 61cc5dd..4cb88d7 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,155 +22,138 @@ package org.apache.cxf.rs.security.saml.sso; import java.io.File; import java.io.IOException; import java.net.URL; +import java.time.Instant; +import java.util.Random; -import net.sf.ehcache.Cache; -import net.sf.ehcache.CacheManager; -import net.sf.ehcache.Ehcache; -import net.sf.ehcache.Element; -import net.sf.ehcache.config.CacheConfiguration; -import net.sf.ehcache.config.Configuration; -import net.sf.ehcache.config.ConfigurationFactory; -import net.sf.ehcache.config.DiskStoreConfiguration; import org.apache.cxf.Bus; import org.apache.cxf.BusFactory; import org.apache.cxf.jaxrs.utils.ResourceUtils; +import org.apache.wss4j.common.cache.EHCacheReplayCache; +import org.apache.wss4j.common.cache.EHCacheValue; +import org.apache.wss4j.common.util.Loader; +import org.ehcache.Cache; +import org.ehcache.CacheManager; +import org.ehcache.Status; +import org.ehcache.config.builders.CacheConfigurationBuilder; +import org.ehcache.config.builders.CacheManagerBuilder; +import org.ehcache.xml.XmlConfiguration; /** * An in-memory EHCache implementation of the TokenReplayCache interface. * The default TTL is 60 minutes and the max TTL is 12 hours. */ + public class EHCacheTokenReplayCache implements TokenReplayCache<String> { - public static final long DEFAULT_TTL = 3600L; - public static final long MAX_TTL = DEFAULT_TTL * 12L; public static final String CACHE_KEY = "cxf.samlp.replay.cache"; + + private static final org.slf4j.Logger LOG = + org.slf4j.LoggerFactory.getLogger(EHCacheReplayCache.class); private static final String DEFAULT_CONFIG_URL = "/cxf-samlp-ehcache.xml"; - private Ehcache cache; - private CacheManager cacheManager; - private long ttl = DEFAULT_TTL; + private final Cache<String, EHCacheValue> cache; + private final CacheManager cacheManager; - public EHCacheTokenReplayCache() { + public EHCacheTokenReplayCache() + throws IllegalAccessException, InstantiationException, ClassNotFoundException { this(DEFAULT_CONFIG_URL, null); } - public EHCacheTokenReplayCache(Bus bus) { + public EHCacheTokenReplayCache(Bus bus) + throws IllegalAccessException, InstantiationException, ClassNotFoundException { this(DEFAULT_CONFIG_URL, bus); } - public EHCacheTokenReplayCache(String configFileURL) { - this(configFileURL, null); + public EHCacheTokenReplayCache(String configFile) + throws IllegalAccessException, InstantiationException, ClassNotFoundException { + this(configFile, null); } - public EHCacheTokenReplayCache(String configFileURL, Bus bus) { - createCache(configFileURL, bus); - } - - private void createCache(String configFile, Bus bus) { + public EHCacheTokenReplayCache(String configFile, Bus bus) + throws IllegalAccessException, ClassNotFoundException, InstantiationException { if (bus == null) { bus = BusFactory.getThreadDefaultBus(true); } URL configFileURL = null; try { configFileURL = - ResourceUtils.getClasspathResourceURL(configFile, EHCacheTokenReplayCache.class, bus); + ResourceUtils.getClasspathResourceURL(configFile, EHCacheTokenReplayCache.class, bus); } catch (Exception ex) { // ignore } - if (configFileURL == null) { - cacheManager = EHCacheUtil.createCacheManager(); - } else { - Configuration conf = ConfigurationFactory.parseConfiguration(configFileURL); - - if (bus != null) { - conf.setName(bus.getId()); - DiskStoreConfiguration dsc = conf.getDiskStoreConfiguration(); - if (dsc != null && "java.io.tmpdir".equals(dsc.getOriginalPath())) { - String path = conf.getDiskStoreConfiguration().getPath() + File.separator - + bus.getId(); - conf.getDiskStoreConfiguration().setPath(path); - } - } - - cacheManager = EHCacheUtil.createCacheManager(conf); - } - CacheConfiguration cc = EHCacheUtil.getCacheConfiguration(CACHE_KEY, cacheManager); + XmlConfiguration xmlConfig = new XmlConfiguration(getConfigFileURL(configFileURL)); + CacheConfigurationBuilder<String, EHCacheValue> configurationBuilder = + xmlConfig.newCacheConfigurationBuilderFromTemplate(CACHE_KEY, + String.class, EHCacheValue.class); + // Note, we don't require strong random values here + String diskKey = CACHE_KEY + "-" + Math.abs(new Random().nextInt()); + cacheManager = CacheManagerBuilder.newCacheManagerBuilder().withCache(CACHE_KEY, configurationBuilder) + .with(CacheManagerBuilder.persistence(new File(System.getProperty("java.io.tmpdir"), diskKey))).build(); - Ehcache newCache = new Cache(cc); - cache = cacheManager.addCacheIfAbsent(newCache); - } + cacheManager.init(); + cache = cacheManager.getCache(CACHE_KEY, String.class, EHCacheValue.class); - /** - * Set a new (default) TTL value in seconds - * @param newTtl a new (default) TTL value in seconds - */ - public void setTTL(long newTtl) { - ttl = newTtl; } - /** - * Get the (default) TTL value in seconds - * @return the (default) TTL value in seconds - */ - public long getTTL() { - return ttl; + private URL getConfigFileURL(URL suppliedConfigFileURL) { + if (suppliedConfigFileURL == null) { + //using the default + String defaultConfigFile = "/cxf-samlp-ehcache.xml"; + URL configFileURL = null; + try { + configFileURL = Loader.getResource(defaultConfigFile); + if (configFileURL == null) { + configFileURL = new URL(defaultConfigFile); + } + return configFileURL; + } catch (IOException e) { + // Do nothing + LOG.debug(e.getMessage()); + } + } + return suppliedConfigFileURL; } /** * Add the given identifier to the cache. It will be cached for a default amount of time. - * @param id The identifier to be added + * @param identifier The identifier to be added */ - public void putId(String id) { - putId(id, ttl); + public void putId(String identifier) { + putId(identifier, null); } /** - * Add the given identifier to the cache. - * @param id The identifier to be added - * @param timeToLive The length of time to cache the Identifier in seconds + * Add the given identifier to the cache to be cached for the given time + * @param identifier The identifier to be added + * @param expiry A custom expiry time for the identifier. Can be null in which case, the default expiry is used. */ - public void putId(String id, long timeToLive) { - if (id == null || "".equals(id)) { + public void putId(String identifier, Instant expiry) { + if (identifier == null || "".equals(identifier)) { return; } - int parsedTTL = (int)timeToLive; - if (timeToLive != parsedTTL || parsedTTL < 0 || parsedTTL > MAX_TTL) { - // Default to configured value - parsedTTL = (int)ttl; - if (ttl != parsedTTL) { - // Fall back to 60 minutes if the default TTL is set incorrectly - parsedTTL = 3600; - } - } - Element element = new Element(id, id, parsedTTL, parsedTTL); - element.resetAccessStatistics(); - cache.put(element); + cache.put(identifier, new EHCacheValue(identifier, expiry)); } /** - * Return the given identifier if it is contained in the cache, otherwise null. - * @param id The identifier to check + * Return true if the given identifier is contained in the cache + * @param identifier The identifier to check */ - public String getId(String id) { - Element element = cache.get(id); - if (element != null) { - if (cache.isExpired(element)) { - cache.remove(id); - return null; - } - return (String)element.getObjectValue(); + public boolean contains(String identifier) { + if (cache == null) { + return false; } - return null; + EHCacheValue element = cache.get(identifier); + return element != null; } - public void close() throws IOException { - if (cacheManager != null) { - cacheManager.shutdown(); - cacheManager = null; - cache = null; + public void close() { + if (cacheManager.getStatus() == Status.AVAILABLE) { + cacheManager.removeCache(CACHE_KEY); + cacheManager.close(); } } + } 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 6772436..3f6de43 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 @@ -18,7 +18,6 @@ */ package org.apache.cxf.rs.security.saml.sso; -import java.time.Duration; import java.time.Instant; import java.util.List; import java.util.logging.Logger; @@ -240,11 +239,9 @@ public class SAMLSSOResponseValidator { // Need to keep bearer assertion IDs based on NotOnOrAfter to detect replay attacks if (postBinding && replayCache != null) { - if (replayCache.getId(id) == null) { + if (!replayCache.contains(id)) { Instant expires = Instant.ofEpochMilli(subjectConfData.getNotOnOrAfter().toDate().getTime()); - Instant currentTime = Instant.now(); - long ttl = Duration.between(currentTime, expires).getSeconds(); - replayCache.putId(id, ttl); + replayCache.putId(id, expires); } else { LOG.warning("Replay attack with token id: " + id); throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); diff --git a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/TokenReplayCache.java b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/TokenReplayCache.java index 828aaa1..6175e6d 100644 --- a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/TokenReplayCache.java +++ b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/TokenReplayCache.java @@ -21,14 +21,15 @@ package org.apache.cxf.rs.security.saml.sso; import java.io.Closeable; import java.io.IOException; +import java.time.Instant; public interface TokenReplayCache<T> extends Closeable { - T getId(T id); + boolean contains(T id); void putId(T id); - void putId(T id, long timeToLive); + void putId(T id, Instant expiry); void close() throws IOException; } \ No newline at end of file diff --git a/rt/rs/security/sso/saml/src/main/resources/cxf-samlp-ehcache.xml b/rt/rs/security/sso/saml/src/main/resources/cxf-samlp-ehcache.xml index 1a3f43d..8100cce 100644 --- a/rt/rs/security/sso/saml/src/main/resources/cxf-samlp-ehcache.xml +++ b/rt/rs/security/sso/saml/src/main/resources/cxf-samlp-ehcache.xml @@ -1,5 +1,20 @@ <?xml version="1.0"?> -<ehcache xsi:noNamespaceSchemaLocation="ehcache.xsd" updateCheck="false" monitoring="autodetect" dynamicConfig="true"> - <diskStore path="java.io.tmpdir"/> - <defaultCache maxEntriesLocalHeap="5000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="3600" overflowToDisk="true" maxElementsOnDisk="10000000" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"/> -</ehcache> +<config + xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' + xmlns='http://www.ehcache.org/v3' + xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core.xsd"> + + <cache-template name="cxf.samlp.replay.cache"> + <key-type>java.lang.String</key-type> + <value-type>org.apache.wss4j.common.cache.EHCacheValue</value-type> + <expiry> + <class>org.apache.wss4j.common.cache.EHCacheExpiry</class> + </expiry> + <resources> + <heap unit="entries">5000</heap> + <disk unit="MB" persistent="false">10</disk> + </resources> + </cache-template> + +</config> + diff --git a/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/EHCacheUtilTest.java b/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/EHCacheUtilTest.java deleted file mode 100644 index d61320d..0000000 --- a/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/EHCacheUtilTest.java +++ /dev/null @@ -1,59 +0,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. - */ - -package org.apache.cxf.rs.security.saml.sso; - -import net.sf.ehcache.CacheManager; -import net.sf.ehcache.Status; -import net.sf.ehcache.config.Configuration; -import net.sf.ehcache.config.ConfigurationFactory; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -/** - * - */ -public class EHCacheUtilTest { - - @Test - public void testCreateCacheManager() { - Configuration conf = - ConfigurationFactory.parseConfiguration(EHCacheUtil.class.getResource("/cxf-test-ehcache.xml")); - - assertNotNull(conf); - conf.setName("testCache"); - - CacheManager manager1 = EHCacheUtil.createCacheManager(conf); - assertNotNull(manager1); - CacheManager manager2 = EHCacheUtil.createCacheManager(); - assertNotNull(manager2); - - manager1.shutdown(); - assertEquals(Status.STATUS_SHUTDOWN, manager1.getStatus()); - - assertEquals(Status.STATUS_ALIVE, manager2.getStatus()); - - manager2.shutdown(); - assertEquals(Status.STATUS_SHUTDOWN, manager2.getStatus()); - - } -} \ No newline at end of file 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 new file mode 100644 index 0000000..0283d48 --- /dev/null +++ b/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/TokenReplayCacheTest.java @@ -0,0 +1,108 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.cxf.rs.security.saml.sso; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.UUID; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Some unit tests for the TokenReplayCache implementations + */ +public class TokenReplayCacheTest { + + @Test + public void testEhCacheTokenReplayCache() throws Exception { + + TokenReplayCache<String> replayCache = new EHCacheTokenReplayCache(); + + testTokenReplayCacheInstance(replayCache); + + replayCache.close(); + } + + @Test + public void testEhCacheCloseCacheTwice() throws Exception { + TokenReplayCache replayCache = new EHCacheTokenReplayCache(); + replayCache.close(); + replayCache.close(); + } + + // No expiry specified so it falls back to the default + @Test + public void testEhCacheTokenReplayCacheNoExpirySpecified() throws Exception { + TokenReplayCache replayCache = new EHCacheTokenReplayCache(); + + String id = UUID.randomUUID().toString(); + replayCache.putId(id); + assertTrue(replayCache.contains(id)); + + replayCache.close(); + } + + // The negative expiry is rejected and it falls back to the default + @Test + public void testEhCacheTokenReplayCacheNegativeExpiry() throws Exception { + TokenReplayCache replayCache = new EHCacheTokenReplayCache(); + + String id = UUID.randomUUID().toString(); + replayCache.putId(id, Instant.now().minusSeconds(100L)); + assertTrue(replayCache.contains(id)); + + replayCache.close(); + } + + // The huge expiry is rejected and it falls back to the default + @Test + public void testEhCacheTokenReplayCacheHugeExpiry() throws Exception { + TokenReplayCache replayCache = new EHCacheTokenReplayCache(); + + String id = UUID.randomUUID().toString(); + replayCache.putId(id, Instant.now().plus(14, ChronoUnit.HOURS)); + assertTrue(replayCache.contains(id)); + + replayCache.close(); + } + + private void testTokenReplayCacheInstance(TokenReplayCache<String> replayCache) throws InterruptedException { + + // Test default TTL caches OK + String id = UUID.randomUUID().toString(); + replayCache.putId(id); + assertTrue(replayCache.contains(id)); + + // Test specifying TTL caches OK + id = UUID.randomUUID().toString(); + replayCache.putId(id, Instant.now().plusSeconds(100L)); + assertTrue(replayCache.contains(id)); + + // Test expiration + id = UUID.randomUUID().toString(); + replayCache.putId(id, Instant.now().plusSeconds(1L)); + Thread.sleep(1250L); + assertFalse(replayCache.contains(id)); + + } +} \ No newline at end of file diff --git a/rt/rs/security/sso/saml/src/test/resources/cxf-test-ehcache.xml b/rt/rs/security/sso/saml/src/test/resources/cxf-test-ehcache.xml deleted file mode 100644 index 1a3f43d..0000000 --- a/rt/rs/security/sso/saml/src/test/resources/cxf-test-ehcache.xml +++ /dev/null @@ -1,5 +0,0 @@ -<?xml version="1.0"?> -<ehcache xsi:noNamespaceSchemaLocation="ehcache.xsd" updateCheck="false" monitoring="autodetect" dynamicConfig="true"> - <diskStore path="java.io.tmpdir"/> - <defaultCache maxEntriesLocalHeap="5000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="3600" overflowToDisk="true" maxElementsOnDisk="10000000" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"/> -</ehcache>
