This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch 4.1.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/4.1.x-fixes by this push:
new 1fc3a31e8e8 Support detecting the replay of SAML2 bearer assertions at
the OAuth token endpoint (#3637)
1fc3a31e8e8 is described below
commit 1fc3a31e8e8a25fe90a4a9a5d580069f5a236243
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Wed Oct 28 11:14:54 2026 +0000
Support detecting the replay of SAML2 bearer assertions at the OAuth token
endpoint (#3637)
(cherry picked from commit 8f0f8cdfd099b1fb5196a532b1a83139b77ce5b4)
---
.../oauth2/auth/saml/Saml2BearerAuthHandler.java | 6 +
.../grants/saml/Saml2BearerGrantHandler.java | 5 +
.../security/oauth2/saml/SamlOAuthValidator.java | 53 ++++++-
.../oauth2/saml/SamlOAuthValidatorTest.java | 85 +++++++++++
.../cxf/rs/security/saml/sso/TokenReplayCache.java | 43 +-----
.../saml/sso/jcache/JCacheTokenReplayCache.java | 134 +---------------
rt/rs/security/xml/pom.xml | 20 +++
.../cxf/rs/security/common}/TokenReplayCache.java | 13 +-
.../common}/jcache/JCacheTokenReplayCache.java | 71 +++++----
.../resources/cxf-rs-security-replay-jcache.xml | 43 ++++++
.../rs/security/common/TokenReplayCacheTest.java | 169 +++++++++++++++++++++
.../security/oauth2/grants/JAXRSOAuth2Test.java | 80 ++++++++++
.../jaxrs/security/oauth2/grants/server-jcache.xml | 42 +++++
13 files changed, 564 insertions(+), 200 deletions(-)
diff --git
a/rt/rs/security/oauth-parent/oauth2-saml/src/main/java/org/apache/cxf/rs/security/oauth2/auth/saml/Saml2BearerAuthHandler.java
b/rt/rs/security/oauth-parent/oauth2-saml/src/main/java/org/apache/cxf/rs/security/oauth2/auth/saml/Saml2BearerAuthHandler.java
index 3f9b36bfbe8..7897ced0695 100644
---
a/rt/rs/security/oauth-parent/oauth2-saml/src/main/java/org/apache/cxf/rs/security/oauth2/auth/saml/Saml2BearerAuthHandler.java
+++
b/rt/rs/security/oauth-parent/oauth2-saml/src/main/java/org/apache/cxf/rs/security/oauth2/auth/saml/Saml2BearerAuthHandler.java
@@ -50,6 +50,12 @@ public class Saml2BearerAuthHandler extends
AbstractSamlInHandler {
public Saml2BearerAuthHandler() {
}
+ /**
+ * Set the SamlOAuthValidator. The replay of client authentication
assertions is only detected if a
+ * replay cache is set on the validator, as clients may legitimately reuse
an assertion until it
+ * expires.
+ * @param validator the SamlOAuthValidator
+ */
public void setSamlOAuthValidator(SamlOAuthValidator validator) {
samlOAuthValidator = validator;
}
diff --git
a/rt/rs/security/oauth-parent/oauth2-saml/src/main/java/org/apache/cxf/rs/security/oauth2/grants/saml/Saml2BearerGrantHandler.java
b/rt/rs/security/oauth-parent/oauth2-saml/src/main/java/org/apache/cxf/rs/security/oauth2/grants/saml/Saml2BearerGrantHandler.java
index f7a18c94287..7dfbd9d1fde 100644
---
a/rt/rs/security/oauth-parent/oauth2-saml/src/main/java/org/apache/cxf/rs/security/oauth2/grants/saml/Saml2BearerGrantHandler.java
+++
b/rt/rs/security/oauth-parent/oauth2-saml/src/main/java/org/apache/cxf/rs/security/oauth2/grants/saml/Saml2BearerGrantHandler.java
@@ -99,6 +99,11 @@ public class Saml2BearerGrantHandler extends
AbstractGrantHandler {
samlValidator = validator;
}
+ /**
+ * Set the SamlOAuthValidator. The replay of SAML2 bearer grant assertions
is only detected if a
+ * replay cache is set on the validator.
+ * @param validator the SamlOAuthValidator
+ */
public void setSamlOAuthValidator(SamlOAuthValidator validator) {
samlOAuthValidator = validator;
}
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 94a15e2a564..95f326cafa7 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
@@ -26,6 +26,7 @@ import jakarta.ws.rs.core.UriBuilder;
import org.apache.cxf.jaxrs.impl.UriInfoImpl;
import org.apache.cxf.jaxrs.utils.ExceptionUtils;
import org.apache.cxf.message.Message;
+import org.apache.cxf.rs.security.common.TokenReplayCache;
import org.apache.cxf.rs.security.oauth2.utils.OAuthConstants;
import org.apache.wss4j.common.saml.SamlAssertionWrapper;
import org.apache.wss4j.common.saml.builder.SAML2Constants;
@@ -41,6 +42,7 @@ public class SamlOAuthValidator {
private String issuer;
private String clientAddress;
private boolean subjectConfirmationDataRequired;
+ private TokenReplayCache<String> replayCache;
public SamlOAuthValidator() {
}
@@ -61,6 +63,19 @@ public class SamlOAuthValidator {
clientAddress = value;
}
+ /**
+ * Set the cache used to detect the replay of bearer assertions. If it is
set, an assertion is
+ * rejected if its ID has already been seen, until the assertion expires.
It is not set by default.
+ * @param replayCache the replay cache
+ */
+ public void setReplayCache(TokenReplayCache<String> replayCache) {
+ this.replayCache = replayCache;
+ }
+
+ public TokenReplayCache<String> getReplayCache() {
+ return replayCache;
+ }
+
public void validate(Message message, SamlAssertionWrapper wrapper) {
validateSAMLVersion(wrapper);
@@ -78,6 +93,42 @@ public class SamlOAuthValidator {
if (!validateAuthenticationSubject(message, cs,
wrapper.getSaml2().getSubject())) {
throw ExceptionUtils.toNotAuthorizedException(null, null);
}
+
+ // This must be the last check, so that only an otherwise valid
assertion is cached
+ checkReplay(wrapper, cs);
+ }
+
+ /**
+ * Bearer assertion IDs are cached until the assertion expires, so that
the same assertion cannot be
+ * used more than once. This is done once per assertion, rather than per
Subject Confirmation.
+ */
+ private void checkReplay(SamlAssertionWrapper wrapper, Conditions cs) {
+ if (replayCache == null) {
+ return;
+ }
+ Instant expiry = getReplayCacheExpiry(cs,
wrapper.getSaml2().getSubject());
+ if (!replayCache.putIdIfAbsent(wrapper.getId(), expiry)) {
+ throw ExceptionUtils.toNotAuthorizedException(null, null);
+ }
+ }
+
+ /**
+ * The assertion ID must be cached for as long as the assertion can be
accepted, so the latest of the
+ * NotOnOrAfter times of the Conditions and of the Bearer Subject
Confirmations is used.
+ */
+ private static Instant getReplayCacheExpiry(Conditions cs,
org.opensaml.saml.saml2.core.Subject subject) {
+ Instant expiry = cs != null ? cs.getNotOnOrAfter() : null;
+ if (subject != null && subject.getSubjectConfirmations() != null) {
+ for (SubjectConfirmation subjectConf :
subject.getSubjectConfirmations()) {
+ SubjectConfirmationData subjectConfData =
subjectConf.getSubjectConfirmationData();
+ if (SAML2Constants.CONF_BEARER.equals(subjectConf.getMethod())
&& subjectConfData != null
+ && subjectConfData.getNotOnOrAfter() != null
+ && (expiry == null ||
subjectConfData.getNotOnOrAfter().isAfter(expiry))) {
+ expiry = subjectConfData.getNotOnOrAfter();
+ }
+ }
+ }
+ return expiry;
}
private void validateSAMLVersion(SamlAssertionWrapper assertionW) {
@@ -163,8 +214,6 @@ public class SamlOAuthValidator {
throw ExceptionUtils.toNotAuthorizedException(null, null);
}
- //TODO: replay cache, same as with SAML SSO case
-
// Check address
if (subjectConfData.getAddress() != null
&& (clientAddress == null ||
!subjectConfData.getAddress().equals(clientAddress))) {
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 a127ae9bfb2..46187b27935 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
@@ -22,12 +22,15 @@ import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import jakarta.ws.rs.NotAuthorizedException;
+import org.apache.cxf.rs.security.common.TokenReplayCache;
import org.apache.wss4j.common.saml.SAMLCallback;
import org.apache.wss4j.common.saml.SAMLUtil;
import org.apache.wss4j.common.saml.SamlAssertionWrapper;
@@ -41,6 +44,9 @@ import org.apache.wss4j.dom.engine.WSSConfig;
import org.junit.Test;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
+
public class SamlOAuthValidatorTest {
static {
@@ -75,6 +81,61 @@ public class SamlOAuthValidatorTest {
validator.validate(null, wrapper);
}
+ @Test
+ public void testReplayedAssertionIsRejected() throws Exception {
+ String tokenServiceAddress = "https://service.example.org/token";
+
+ SamlOAuthValidator validator = new SamlOAuthValidator();
+ validator.setAccessTokenServiceAddress(tokenServiceAddress);
+ validator.setReplayCache(new TestTokenReplayCache());
+
+ SamlAssertionWrapper wrapper = createAssertion(tokenServiceAddress,
null);
+ validator.validate(null, wrapper);
+
+ try {
+ validator.validate(null, wrapper);
+ fail("Expected failure on replayed assertion");
+ } catch (NotAuthorizedException ex) {
+ // expected
+ }
+
+ // A different assertion is still accepted
+ validator.validate(null, createAssertion(tokenServiceAddress, null));
+ }
+
+ // An assertion that fails validation is not cached, so it doesn't block
the same ID later
+ @Test
+ public void testInvalidAssertionIsNotCached() throws Exception {
+ String tokenServiceAddress = "https://service.example.org/token";
+ TestTokenReplayCache replayCache = new TestTokenReplayCache();
+
+ SamlOAuthValidator validator = new SamlOAuthValidator();
+ validator.setAccessTokenServiceAddress(tokenServiceAddress);
+ validator.setClientAddress("192.0.2.10");
+ validator.setReplayCache(replayCache);
+
+ SamlAssertionWrapper wrapper = createAssertion(tokenServiceAddress,
"192.0.2.20");
+ try {
+ validator.validate(null, wrapper);
+ fail("Expected failure on mismatching client address");
+ } catch (NotAuthorizedException ex) {
+ // expected
+ }
+ assertFalse(replayCache.contains(wrapper.getId()));
+ }
+
+ @Test
+ public void testReplayIsNotCheckedWithoutReplayCache() throws Exception {
+ String tokenServiceAddress = "https://service.example.org/token";
+
+ SamlOAuthValidator validator = new SamlOAuthValidator();
+ validator.setAccessTokenServiceAddress(tokenServiceAddress);
+
+ SamlAssertionWrapper wrapper = createAssertion(tokenServiceAddress,
null);
+ validator.validate(null, wrapper);
+ validator.validate(null, wrapper);
+ }
+
private SamlAssertionWrapper createAssertion(String recipient, String
clientAddress) throws Exception {
SubjectConfirmationDataBean confirmationData = new
SubjectConfirmationDataBean();
confirmationData.setRecipient(recipient);
@@ -93,6 +154,30 @@ public class SamlOAuthValidatorTest {
return new SamlAssertionWrapper(samlCallback);
}
+ private static final class TestTokenReplayCache implements
TokenReplayCache<String> {
+ private final Set<String> ids = ConcurrentHashMap.newKeySet();
+
+ @Override
+ public boolean contains(String id) {
+ return ids.contains(id);
+ }
+
+ @Override
+ public void putId(String id) {
+ ids.add(id);
+ }
+
+ @Override
+ public void putId(String id, Instant expiry) {
+ ids.add(id);
+ }
+
+ @Override
+ public void close() {
+ ids.clear();
+ }
+ }
+
private static class TestSaml2CallbackHandler implements CallbackHandler {
private final ConditionsBean conditions;
private final SubjectConfirmationDataBean confirmationData;
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 51cb3dd0510..9af7602d0e9 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
@@ -19,42 +19,11 @@
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 {
-
- boolean contains(T id);
-
- void putId(T id);
-
- void putId(T id, Instant expiry);
-
- /**
- * Add the given identifier to the cache if, and only if, it is not
already contained in it, as a
- * single atomic operation, so that concurrent requests carrying the same
identifier cannot all
- * pass a replay check before any of them has cached it. This default
implementation only
- * serializes calls within a single JVM; an implementation backed by a
distributed cache should
- * override it with the atomic put-if-absent operation of the underlying
cache.
- * @param id 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.
- * @return true if the identifier was added, false if it was already
contained in the cache or is
- * null or empty
- */
- default boolean putIdIfAbsent(T id, Instant expiry) {
- if (id == null || "".equals(id)) {
- return false;
- }
- synchronized (this) {
- if (contains(id)) {
- return false;
- }
- putId(id, expiry);
- return true;
- }
- }
+/**
+ * The replay cache type used by the SAML SSO module. It extends the common
+ * {@link org.apache.cxf.rs.security.common.TokenReplayCache} (which defines
all of its methods), and is
+ * kept so that existing implementations and configurations continue to work
unchanged.
+ */
+public interface TokenReplayCache<T> extends
org.apache.cxf.rs.security.common.TokenReplayCache<T> {
- void close() throws IOException;
}
\ No newline at end of file
diff --git
a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/jcache/JCacheTokenReplayCache.java
b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/jcache/JCacheTokenReplayCache.java
index 853bb45c6ad..9c1a62ca026 100644
---
a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/jcache/JCacheTokenReplayCache.java
+++
b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/jcache/JCacheTokenReplayCache.java
@@ -19,41 +19,24 @@
package org.apache.cxf.rs.security.saml.sso.jcache;
-import java.io.IOException;
import java.net.URISyntaxException;
-import java.net.URL;
-import java.time.Instant;
-
-import javax.cache.Cache;
-import javax.cache.CacheManager;
-import javax.cache.Caching;
-import javax.cache.configuration.MutableConfiguration;
-import javax.cache.spi.CachingProvider;
import org.apache.cxf.Bus;
-import org.apache.cxf.BusFactory;
-import org.apache.cxf.jaxrs.utils.ResourceUtils;
import org.apache.cxf.rs.security.saml.sso.TokenReplayCache;
-import org.apache.wss4j.common.cache.EHCacheReplayCache;
-import org.apache.wss4j.common.cache.EHCacheValue;
-import org.apache.wss4j.common.util.Loader;
/**
- * An in-memory JCache based implementation of the TokenReplayCache interface.
+ * A JCache based implementation of the SSO TokenReplayCache interface, using
the SSO replay cache
+ * name and default configuration file. The implementation is inherited from
+ * {@link org.apache.cxf.rs.security.common.jcache.JCacheTokenReplayCache}.
* The default TTL is 60 minutes and the max TTL is 12 hours.
*/
-
-public class JCacheTokenReplayCache implements TokenReplayCache<String> {
+public class JCacheTokenReplayCache extends
org.apache.cxf.rs.security.common.jcache.JCacheTokenReplayCache
+ implements TokenReplayCache<String> {
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-jcache.xml";
- private final Cache<String, EHCacheValue> cache;
- private final CacheManager cacheManager;
-
public JCacheTokenReplayCache() throws URISyntaxException {
this(DEFAULT_CONFIG_URL, null);
}
@@ -67,112 +50,7 @@ public class JCacheTokenReplayCache implements
TokenReplayCache<String> {
}
public JCacheTokenReplayCache(String configFile, Bus bus) throws
URISyntaxException {
- if (bus == null) {
- bus = BusFactory.getThreadDefaultBus(true);
- }
- URL configFileURL = null;
- try {
- configFileURL =
- ResourceUtils.getClasspathResourceURL(configFile,
JCacheTokenReplayCache.class, bus);
- } catch (Exception ex) {
- // ignore
- }
-
- final CachingProvider cachingProvider = Caching.getCachingProvider();
-
- cacheManager = cachingProvider.getCacheManager(
- getConfigFileURL(configFileURL).toURI(),
- getClass().getClassLoader());
-
- cache = getOrCreate(cacheManager, CACHE_KEY, String.class,
EHCacheValue.class);
- }
-
- private static <K, V> Cache<K, V> getOrCreate(CacheManager cacheManager,
String name,
- Class<K> kclass, Class<V> vclass) {
-
- final Cache<K, V> cache = cacheManager.getCache(name, kclass, vclass);
- if (cache != null) {
- return cache;
- }
-
- final MutableConfiguration<K, V> cacheConfiguration = new
MutableConfiguration<>();
- cacheConfiguration.setTypes(kclass, vclass);
-
- return cacheManager.createCache(name, cacheConfiguration);
- }
-
- private URL getConfigFileURL(URL suppliedConfigFileURL) {
- if (suppliedConfigFileURL == null) {
- //using the default
- try {
- URL configFileURL = Loader.getResource(DEFAULT_CONFIG_URL);
- if (configFileURL == null) {
- configFileURL = new URL(DEFAULT_CONFIG_URL);
- }
- return configFileURL;
- } catch (IOException e) {
- // Do nothing
- LOG.debug(e.getMessage());
- }
- }
- return suppliedConfigFileURL;
+ super(CACHE_KEY, configFile, DEFAULT_CONFIG_URL, bus);
}
- /**
- * Add the given identifier to the cache. It will be cached for a default
amount of time.
- * @param identifier The identifier to be added
- */
- public void putId(String identifier) {
- putId(identifier, null);
- }
-
- /**
- * 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 identifier, Instant expiry) {
- if (identifier == null || "".equals(identifier)) {
- return;
- }
-
- cache.put(identifier, new EHCacheValue(identifier, expiry));
- }
-
- /**
- * Add the given identifier to the cache if, and only if, it is not
already contained in it, as a
- * single atomic operation. JCache's putIfAbsent is atomic for a
distributed provider as well.
- * @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.
- * @return true if the identifier was added, false if it was already
contained in the cache or is
- * null or empty
- */
- @Override
- public boolean putIdIfAbsent(String identifier, Instant expiry) {
- if (identifier == null || "".equals(identifier)) {
- return false;
- }
- return cache.putIfAbsent(identifier, new EHCacheValue(identifier,
expiry));
- }
-
- /**
- * Return true if the given identifier is contained in the cache
- * @param identifier The identifier to check
- */
- public boolean contains(String identifier) {
- if (cache == null) {
- return false;
- }
- EHCacheValue element = cache.get(identifier);
- return element != null;
- }
-
- public synchronized void close() {
- if (!cacheManager.isClosed()) {
- cacheManager.destroyCache(CACHE_KEY);
- cacheManager.close();
- }
- }
-
-
}
diff --git a/rt/rs/security/xml/pom.xml b/rt/rs/security/xml/pom.xml
index 7949120279e..f7b6fd7c1ae 100644
--- a/rt/rs/security/xml/pom.xml
+++ b/rt/rs/security/xml/pom.xml
@@ -34,6 +34,7 @@
<cxf.module.name>org.apache.cxf.rs.security.xml</cxf.module.name>
<cxf.osgi.import>
org.opensaml*;version="${cxf.opensaml.osgi.version}",
+ javax.cache*;resolution:=optional,
</cxf.osgi.import>
</properties>
<dependencies>
@@ -71,5 +72,24 @@
</exclusion>
</exclusions>
</dependency>
+ <dependency>
+ <groupId>javax.cache</groupId>
+ <artifactId>cache-api</artifactId>
+ <version>${cxf.jcache.version}</version>
+ <scope>provided</scope>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>org.ehcache</groupId>
+ <artifactId>ehcache</artifactId>
+ <classifier>jakarta</classifier>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.ehcache.modules</groupId>
+ <artifactId>ehcache-107</artifactId>
+ <version>${cxf.ehcache3.version}</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git
a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/TokenReplayCache.java
b/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/common/TokenReplayCache.java
similarity index 83%
copy from
rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/TokenReplayCache.java
copy to
rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/common/TokenReplayCache.java
index 51cb3dd0510..94c9cb80648 100644
---
a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/TokenReplayCache.java
+++
b/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/common/TokenReplayCache.java
@@ -17,12 +17,15 @@
* under the License.
*/
-package org.apache.cxf.rs.security.saml.sso;
+package org.apache.cxf.rs.security.common;
import java.io.Closeable;
import java.io.IOException;
import java.time.Instant;
+/**
+ * A cache of token identifiers, used to detect the replay of (bearer) tokens
such as SAML assertions.
+ */
public interface TokenReplayCache<T> extends Closeable {
boolean contains(T id);
@@ -34,9 +37,9 @@ public interface TokenReplayCache<T> extends Closeable {
/**
* Add the given identifier to the cache if, and only if, it is not
already contained in it, as a
* single atomic operation, so that concurrent requests carrying the same
identifier cannot all
- * pass a replay check before any of them has cached it. This default
implementation only
- * serializes calls within a single JVM; an implementation backed by a
distributed cache should
- * override it with the atomic put-if-absent operation of the underlying
cache.
+ * pass a replay check before any of them has cached it. This default
implementation is only
+ * atomic with respect to other callers of this method on the same
instance; an implementation
+ * should override it with the atomic put-if-absent operation of the
underlying cache.
* @param id 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.
@@ -57,4 +60,4 @@ public interface TokenReplayCache<T> extends Closeable {
}
void close() throws IOException;
-}
\ No newline at end of file
+}
diff --git
a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/jcache/JCacheTokenReplayCache.java
b/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/common/jcache/JCacheTokenReplayCache.java
similarity index 65%
copy from
rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/jcache/JCacheTokenReplayCache.java
copy to
rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/common/jcache/JCacheTokenReplayCache.java
index 853bb45c6ad..12502eccd4c 100644
---
a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/jcache/JCacheTokenReplayCache.java
+++
b/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/common/jcache/JCacheTokenReplayCache.java
@@ -17,12 +17,15 @@
* under the License.
*/
-package org.apache.cxf.rs.security.saml.sso.jcache;
+package org.apache.cxf.rs.security.common.jcache;
import java.io.IOException;
+import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.time.Instant;
+import java.util.logging.Level;
+import java.util.logging.Logger;
import javax.cache.Cache;
import javax.cache.CacheManager;
@@ -32,25 +35,24 @@ import javax.cache.spi.CachingProvider;
import org.apache.cxf.Bus;
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.saml.sso.TokenReplayCache;
-import org.apache.wss4j.common.cache.EHCacheReplayCache;
+import org.apache.cxf.rs.security.common.TokenReplayCache;
import org.apache.wss4j.common.cache.EHCacheValue;
import org.apache.wss4j.common.util.Loader;
/**
- * An in-memory JCache based implementation of the TokenReplayCache interface.
- * The default TTL is 60 minutes and the max TTL is 12 hours.
+ * 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.
*/
-
public class JCacheTokenReplayCache implements TokenReplayCache<String> {
- public static final String CACHE_KEY = "cxf.samlp.replay.cache";
+ public static final String CACHE_KEY = "cxf.rs.security.replay.cache";
- private static final org.slf4j.Logger LOG =
- org.slf4j.LoggerFactory.getLogger(EHCacheReplayCache.class);
- private static final String DEFAULT_CONFIG_URL = "/cxf-samlp-jcache.xml";
+ private static final Logger LOG =
LogUtils.getL7dLogger(JCacheTokenReplayCache.class);
+ private static final String DEFAULT_CONFIG_URL =
"/cxf-rs-security-replay-jcache.xml";
+ private final String cacheKey;
private final Cache<String, EHCacheValue> cache;
private final CacheManager cacheManager;
@@ -67,24 +69,37 @@ public class JCacheTokenReplayCache implements
TokenReplayCache<String> {
}
public JCacheTokenReplayCache(String configFile, Bus bus) throws
URISyntaxException {
+ this(CACHE_KEY, configFile, DEFAULT_CONFIG_URL, bus);
+ }
+
+ /**
+ * @param cacheKey The name of the cache
+ * @param configFile The JCache provider configuration file
+ * @param defaultConfigFile The configuration file to fall back to if
configFile cannot be loaded
+ * @param bus The Bus used to load the configuration file. Can be null, in
which case the thread
+ * default Bus is used.
+ */
+ protected JCacheTokenReplayCache(String cacheKey, String configFile,
String defaultConfigFile, Bus bus)
+ throws URISyntaxException {
+ this.cacheKey = cacheKey;
if (bus == null) {
bus = BusFactory.getThreadDefaultBus(true);
}
URL configFileURL = null;
try {
- configFileURL =
- ResourceUtils.getClasspathResourceURL(configFile,
JCacheTokenReplayCache.class, bus);
+ configFileURL = ResourceUtils.getClasspathResourceURL(configFile,
getClass(), bus);
} catch (Exception ex) {
// ignore
}
final CachingProvider cachingProvider = Caching.getCachingProvider();
- cacheManager = cachingProvider.getCacheManager(
- getConfigFileURL(configFileURL).toURI(),
- getClass().getClassLoader());
+ URL resolvedConfigFileURL = getConfigFileURL(configFileURL,
defaultConfigFile);
+ URI configURI = resolvedConfigFileURL != null
+ ? resolvedConfigFileURL.toURI() : cachingProvider.getDefaultURI();
+ cacheManager = cachingProvider.getCacheManager(configURI,
getClass().getClassLoader());
- cache = getOrCreate(cacheManager, CACHE_KEY, String.class,
EHCacheValue.class);
+ cache = getOrCreate(cacheManager, cacheKey, String.class,
EHCacheValue.class);
}
private static <K, V> Cache<K, V> getOrCreate(CacheManager cacheManager,
String name,
@@ -101,18 +116,18 @@ public class JCacheTokenReplayCache implements
TokenReplayCache<String> {
return cacheManager.createCache(name, cacheConfiguration);
}
- private URL getConfigFileURL(URL suppliedConfigFileURL) {
+ private static URL getConfigFileURL(URL suppliedConfigFileURL, String
defaultConfigFile) {
if (suppliedConfigFileURL == null) {
//using the default
try {
- URL configFileURL = Loader.getResource(DEFAULT_CONFIG_URL);
+ URL configFileURL = Loader.getResource(defaultConfigFile);
if (configFileURL == null) {
- configFileURL = new URL(DEFAULT_CONFIG_URL);
+ configFileURL = new URL(defaultConfigFile);
}
return configFileURL;
} catch (IOException e) {
// Do nothing
- LOG.debug(e.getMessage());
+ LOG.log(Level.FINE, e.getMessage(), e);
}
}
return suppliedConfigFileURL;
@@ -129,7 +144,8 @@ public class JCacheTokenReplayCache implements
TokenReplayCache<String> {
/**
* 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.
+ * @param expiry A custom expiry time for the identifier. Can be null in
which case, the default
+ * expiry is used.
*/
public void putId(String identifier, Instant expiry) {
if (identifier == null || "".equals(identifier)) {
@@ -140,18 +156,18 @@ public class JCacheTokenReplayCache implements
TokenReplayCache<String> {
}
/**
- * Add the given identifier to the cache if, and only if, it is not
already contained in it, as a
- * single atomic operation. JCache's putIfAbsent is atomic for a
distributed provider as well.
+ * Atomically add the given identifier to the cache if it is not already
present
* @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.
- * @return true if the identifier was added, false if it was already
contained in the cache or is
- * null or empty
+ * @param expiry A custom expiry time for the identifier. Can be null in
which case, the default
+ * expiry is used.
+ * @return true if the identifier was added, false if it was already
present (or is null or empty)
*/
@Override
public boolean putIdIfAbsent(String identifier, Instant expiry) {
if (identifier == null || "".equals(identifier)) {
return false;
}
+
return cache.putIfAbsent(identifier, new EHCacheValue(identifier,
expiry));
}
@@ -169,10 +185,9 @@ public class JCacheTokenReplayCache implements
TokenReplayCache<String> {
public synchronized void close() {
if (!cacheManager.isClosed()) {
- cacheManager.destroyCache(CACHE_KEY);
+ cacheManager.destroyCache(cacheKey);
cacheManager.close();
}
}
-
}
diff --git
a/rt/rs/security/xml/src/main/resources/cxf-rs-security-replay-jcache.xml
b/rt/rs/security/xml/src/main/resources/cxf-rs-security-replay-jcache.xml
new file mode 100644
index 00000000000..58be6e2f8fd
--- /dev/null
+++ b/rt/rs/security/xml/src/main/resources/cxf-rs-security-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/"
/>
+ <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>
+ <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/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
new file mode 100644
index 00000000000..f761ded111e
--- /dev/null
+++
b/rt/rs/security/xml/src/test/java/org/apache/cxf/rs/security/common/TokenReplayCacheTest.java
@@ -0,0 +1,169 @@
+/**
+ * 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.common;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicIntegerArray;
+
+import org.apache.cxf.rs.security.common.jcache.JCacheTokenReplayCache;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Some unit tests for the common TokenReplayCache implementations
+ */
+@RunWith(value = org.junit.runners.Parameterized.class)
+public class TokenReplayCacheTest {
+ private final Class<? extends TokenReplayCache<String>>
tokenReplayCacheClass;
+
+ public TokenReplayCacheTest(Class<? extends TokenReplayCache<String>>
tokenReplayCacheClass) {
+ this.tokenReplayCacheClass = tokenReplayCacheClass;
+ }
+
+ @Parameterized.Parameters(name = "{0}")
+ public static Collection<Class<? extends TokenReplayCache<String>>> data()
{
+ return Collections.singletonList(JCacheTokenReplayCache.class);
+ }
+
+ @Test
+ public void testTokenReplayCache() throws Exception {
+ TokenReplayCache<String> replayCache = createCache();
+ try {
+ // 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));
+ assertTrue(replayCache.putIdIfAbsent(id, null));
+ } finally {
+ replayCache.close();
+ }
+ }
+
+ // The negative and huge expiries are rejected and fall back to the default
+ @Test
+ public void testTokenReplayCacheInvalidExpiry() throws Exception {
+ TokenReplayCache<String> replayCache = createCache();
+ try {
+ String id = UUID.randomUUID().toString();
+ replayCache.putId(id, Instant.now().minusSeconds(100L));
+ assertTrue(replayCache.contains(id));
+
+ id = UUID.randomUUID().toString();
+ replayCache.putId(id, Instant.now().plus(14, ChronoUnit.HOURS));
+ assertTrue(replayCache.contains(id));
+ } finally {
+ replayCache.close();
+ }
+ }
+
+ @Test
+ public void testTokenReplayCachePutIdIfAbsent() throws Exception {
+ TokenReplayCache<String> replayCache = createCache();
+ try {
+ String id = UUID.randomUUID().toString();
+ assertTrue(replayCache.putIdIfAbsent(id, null));
+ assertTrue(replayCache.contains(id));
+ assertFalse(replayCache.putIdIfAbsent(id,
Instant.now().plusSeconds(100L)));
+
+ id = UUID.randomUUID().toString();
+ replayCache.putId(id);
+ assertFalse(replayCache.putIdIfAbsent(id, null));
+
+ assertFalse(replayCache.putIdIfAbsent(null, null));
+ assertFalse(replayCache.putIdIfAbsent("", null));
+ } finally {
+ replayCache.close();
+ }
+ }
+
+ // Exactly one putIdIfAbsent call per identifier may succeed, when called
concurrently
+ @Test
+ public void testTokenReplayCacheConcurrentPutIdIfAbsent() throws Exception
{
+ TokenReplayCache<String> replayCache = createCache();
+
+ final int threads = 8;
+ final int identifiers = 500;
+ List<String> ids = new ArrayList<>(identifiers);
+ for (int i = 0; i < identifiers; i++) {
+ ids.add(UUID.randomUUID().toString());
+ }
+ AtomicIntegerArray accepted = new AtomicIntegerArray(identifiers);
+ CyclicBarrier barrier = new CyclicBarrier(threads);
+ Instant expiry = Instant.now().plusSeconds(100L);
+
+ ExecutorService executor = Executors.newFixedThreadPool(threads);
+ try {
+ List<Future<?>> futures = new ArrayList<>(threads);
+ for (int t = 0; t < threads; t++) {
+ futures.add(executor.submit(() -> {
+ barrier.await(10, TimeUnit.SECONDS);
+ for (int i = 0; i < identifiers; i++) {
+ if (replayCache.putIdIfAbsent(ids.get(i), expiry)) {
+ accepted.incrementAndGet(i);
+ }
+ }
+ return null;
+ }));
+ }
+ for (Future<?> future : futures) {
+ future.get(30, TimeUnit.SECONDS);
+ }
+ } finally {
+ executor.shutdownNow();
+ replayCache.close();
+ }
+
+ for (int i = 0; i < identifiers; i++) {
+ assertEquals("Identifier " + i + " was accepted more than once",
1, accepted.get(i));
+ }
+ }
+
+ private TokenReplayCache<String> createCache() throws Exception {
+ return tokenReplayCacheClass.getDeclaredConstructor().newInstance();
+ }
+}
diff --git
a/systests/rs-security/src/test/java/org/apache/cxf/systest/jaxrs/security/oauth2/grants/JAXRSOAuth2Test.java
b/systests/rs-security/src/test/java/org/apache/cxf/systest/jaxrs/security/oauth2/grants/JAXRSOAuth2Test.java
index 855e8b24b5b..5299b9bfc9f 100644
---
a/systests/rs-security/src/test/java/org/apache/cxf/systest/jaxrs/security/oauth2/grants/JAXRSOAuth2Test.java
+++
b/systests/rs-security/src/test/java/org/apache/cxf/systest/jaxrs/security/oauth2/grants/JAXRSOAuth2Test.java
@@ -28,8 +28,10 @@ import java.util.Map;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
+import jakarta.ws.rs.core.Form;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.core.Response;
import org.apache.cxf.Bus;
import org.apache.cxf.BusFactory;
import org.apache.cxf.bus.spring.SpringBusFactory;
@@ -65,6 +67,7 @@ import org.apache.wss4j.common.saml.SamlAssertionWrapper;
import org.apache.wss4j.common.saml.builder.SAML2Constants;
import org.apache.wss4j.common.util.DOM2Writer;
+import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -177,6 +180,83 @@ public class JAXRSOAuth2Test extends
AbstractBusClientServerTestBase {
assertNotNull(at.getTokenKey());
}
+ // The same SAML2 bearer grant assertion can only be used once, when a
replay cache is configured
+ @Test
+ public void testSAML2BearerGrantReplay() throws Exception {
+ // The replay endpoints are only configured for the JCache server
+ Assume.assumeTrue(JCACHE_PORT.equals(port));
+
+ String address = "https://localhost:" + port + "/oauth2-replay/token";
+ Saml2BearerGrant grant = new
Saml2BearerGrant(createSignedAssertion(address, false));
+
+ ClientAccessToken at =
OAuthClientUtils.getAccessToken(createWebClient(address),
+ new
Consumer("alice", "alice"),
+ grant,
+ false);
+ assertNotNull(at.getTokenKey());
+
+ try {
+ OAuthClientUtils.getAccessToken(createWebClient(address),
+ new Consumer("alice", "alice"),
+ grant,
+ false);
+ fail("Failure expected on a replayed SAML2 bearer grant");
+ } catch (OAuthServiceException ex) {
+ assertEquals(OAuthConstants.INVALID_GRANT,
ex.getError().getError());
+ }
+
+ // A new assertion is still accepted
+ at = OAuthClientUtils.getAccessToken(createWebClient(address),
+ new Consumer("alice", "alice"),
+ new
Saml2BearerGrant(createSignedAssertion(address, false)),
+ false);
+ assertNotNull(at.getTokenKey());
+ }
+
+ // The same SAML2 client authentication assertion can only be used once,
when a replay cache is
+ // configured
+ @Test
+ public void testSAML2BearerAuthenticationReplay() throws Exception {
+ // The replay endpoints are only configured for the JCache server
+ Assume.assumeTrue(JCACHE_PORT.equals(port));
+
+ String address = "https://localhost:" + port +
"/oauth2-auth-replay/token";
+ String encodedAssertion =
Base64UrlUtility.encode(createSignedAssertion(address, true));
+
+ Map<String, String> extraParams = new HashMap<>();
+ extraParams.put(Constants.CLIENT_AUTH_ASSERTION_TYPE,
Constants.CLIENT_AUTH_SAML2_BEARER);
+ extraParams.put(Constants.CLIENT_AUTH_ASSERTION_PARAM,
encodedAssertion);
+
+ ClientAccessToken at =
OAuthClientUtils.getAccessToken(createWebClient(address),
+ new
CustomGrant(),
+ extraParams);
+ assertNotNull(at.getTokenKey());
+
+ // The replayed assertion is rejected by the client authentication
filter
+ Form form = new Form();
+ form.param(OAuthConstants.GRANT_TYPE, "custom_grant");
+ form.param(Constants.CLIENT_AUTH_ASSERTION_TYPE,
Constants.CLIENT_AUTH_SAML2_BEARER);
+ form.param(Constants.CLIENT_AUTH_ASSERTION_PARAM, encodedAssertion);
+ try (Response response = createWebClient(address).form(form)) {
+ assertEquals(401, response.getStatus());
+ }
+ }
+
+ private String createSignedAssertion(String audienceURI, boolean
clientAuthentication) throws Exception {
+ Crypto crypto = new
CryptoLoader().loadCrypto(CRYPTO_RESOURCE_PROPERTIES);
+ SelfSignInfo signInfo = new SelfSignInfo(crypto, "alice", "password");
+
+ SamlCallbackHandler samlCallbackHandler = new
SamlCallbackHandler(clientAuthentication);
+ if (clientAuthentication) {
+ samlCallbackHandler.setIssuer("alice");
+ }
+ samlCallbackHandler.setAudience(audienceURI);
+ SamlAssertionWrapper assertionWrapper =
SAMLUtils.createAssertion(samlCallbackHandler, signInfo);
+ Document doc = DOMUtils.newDocument();
+ Element assertionElement = assertionWrapper.toDOM(doc);
+ return DOM2Writer.nodeToString(assertionElement);
+ }
+
@Test()
public void testConfidentialClientIdOnly() throws Exception {
String address = "https://localhost:" + port + "/oauth2/token";
diff --git
a/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/oauth2/grants/server-jcache.xml
b/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/oauth2/grants/server-jcache.xml
index 72cb43c3579..72da54d6093 100644
---
a/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/oauth2/grants/server-jcache.xml
+++
b/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/oauth2/grants/server-jcache.xml
@@ -128,6 +128,48 @@ under the License.
<entry key="security.signature.properties"
value="org/apache/cxf/systest/jaxrs/security/alice.properties"/>
</jaxrs:properties>
</jaxrs:server>
+ <!-- SAML2 bearer grant and client authentication with replay detection,
using a JCache replay cache -->
+ <bean id="replayCache"
class="org.apache.cxf.rs.security.common.jcache.JCacheTokenReplayCache"
+ destroy-method="close"/>
+ <bean id="replaySamlOAuthValidator"
class="org.apache.cxf.rs.security.oauth2.saml.SamlOAuthValidator">
+ <property name="replayCache" ref="replayCache"/>
+ </bean>
+ <bean id="replaySamlGrantHandler"
class="org.apache.cxf.rs.security.oauth2.grants.saml.Saml2BearerGrantHandler">
+ <property name="dataProvider" ref="dataProvider"/>
+ <property name="samlOAuthValidator" ref="replaySamlOAuthValidator"/>
+ </bean>
+ <bean id="replaySamlAuthHandler"
class="org.apache.cxf.rs.security.oauth2.auth.saml.Saml2BearerAuthHandler">
+ <property name="samlOAuthValidator" ref="replaySamlOAuthValidator"/>
+ </bean>
+ <bean id="replayServiceBean"
class="org.apache.cxf.rs.security.oauth2.services.AccessTokenService">
+ <property name="dataProvider" ref="dataProvider"/>
+ <property name="grantHandlers">
+ <list>
+ <ref bean="replaySamlGrantHandler"/>
+ <ref bean="customGrantHandler"/>
+ </list>
+ </property>
+ <property name="canSupportPublicClients" value="true"/>
+ </bean>
+ <jaxrs:server
address="https://localhost:${testutil.ports.jaxrs-oauth2-jcache}/oauth2-replay">
+ <jaxrs:serviceBeans>
+ <ref bean="replayServiceBean"/>
+ </jaxrs:serviceBeans>
+ <jaxrs:properties>
+ <entry key="security.signature.properties"
value="org/apache/cxf/systest/jaxrs/security/alice.properties"/>
+ </jaxrs:properties>
+ </jaxrs:server>
+ <jaxrs:server
address="https://localhost:${testutil.ports.jaxrs-oauth2-jcache}/oauth2-auth-replay">
+ <jaxrs:serviceBeans>
+ <ref bean="replayServiceBean"/>
+ </jaxrs:serviceBeans>
+ <jaxrs:providers>
+ <ref bean="replaySamlAuthHandler"/>
+ </jaxrs:providers>
+ <jaxrs:properties>
+ <entry key="security.signature.properties"
value="org/apache/cxf/systest/jaxrs/security/alice.properties"/>
+ </jaxrs:properties>
+ </jaxrs:server>
<jaxrs:server
address="https://localhost:${testutil.ports.jaxrs-oauth2-jcache}/oauth2-auth-jwt">
<jaxrs:serviceBeans>
<ref bean="serviceBean"/>