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"/>

Reply via email to