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 c8ee87a1aa2 Porting locking fixes to revokeRefreshToken (#3364)
c8ee87a1aa2 is described below
commit c8ee87a1aa2ad5a639fc51a9e53c050d8bea4a36
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Wed Aug 12 08:51:39 2026 +0100
Porting locking fixes to revokeRefreshToken (#3364)
(cherry picked from commit c21d5d310bb58d7e0500ed245c680e958fdec0c2)
---
.../oauth2/grants/code/JPACMTCodeDataProvider.java | 25 ++++++++++++++++++++++
.../oauth2/provider/JPAOAuthDataProvider.java | 22 +++++++++++++++++++
.../provider/AbstractOAuthDataProviderTest.java | 25 ++++++++++++++++++++++
3 files changed, 72 insertions(+)
diff --git
a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/grants/code/JPACMTCodeDataProvider.java
b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/grants/code/JPACMTCodeDataProvider.java
index 18101769825..8745ffaf3a4 100644
---
a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/grants/code/JPACMTCodeDataProvider.java
+++
b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/grants/code/JPACMTCodeDataProvider.java
@@ -25,9 +25,12 @@ import java.util.Map;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.LockModeType;
+import org.apache.cxf.rs.security.oauth2.common.Client;
import org.apache.cxf.rs.security.oauth2.common.ServerAccessToken;
+import org.apache.cxf.rs.security.oauth2.common.UserSubject;
import org.apache.cxf.rs.security.oauth2.provider.OAuthServiceException;
import org.apache.cxf.rs.security.oauth2.tokens.refresh.RefreshToken;
+import org.apache.cxf.rs.security.oauth2.utils.OAuthConstants;
/**
* Same as {@link JPACodeDataProvider} (stores Clients and tokens in a rdbms
using
@@ -122,6 +125,28 @@ public class JPACMTCodeDataProvider extends
JPACodeDataProvider {
protected void closeIfNeeded(EntityManager em) {
}
+ @Override
+ protected RefreshToken revokeRefreshToken(Client client, UserSubject
callerSubject, String refreshTokenKey) {
+ // Atomic find + validate + delete with lock timeout hint, parallel to
removeCodeGrant.
+ final Map<String, Object> options = new HashMap<>();
+ options.put(JPA_LOCK_TIMEOUT_HINT, pessimisticLockTimeout);
+ return executeInTransaction(em -> {
+ RefreshToken refreshToken = em.find(RefreshToken.class,
refreshTokenKey,
+
LockModeType.PESSIMISTIC_WRITE, options);
+ if (refreshToken != null) {
+ if
(!refreshToken.getClient().getClientId().equals(client.getClientId())) {
+ throw new
OAuthServiceException(OAuthConstants.INVALID_GRANT);
+ }
+ if (callerSubject != null && refreshToken.getSubject() != null
+ &&
!callerSubject.getLogin().equals(refreshToken.getSubject().getLogin())) {
+ throw new
OAuthServiceException(OAuthConstants.INVALID_GRANT);
+ }
+ em.remove(refreshToken);
+ }
+ return refreshToken;
+ });
+ }
+
@Override
protected RefreshToken updateExistingRefreshToken(RefreshToken rt,
ServerAccessToken at) {
if (useJpaLockForExistingRefreshToken) {
diff --git
a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/provider/JPAOAuthDataProvider.java
b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/provider/JPAOAuthDataProvider.java
index 9f3fbb96388..eb5ed948f38 100644
---
a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/provider/JPAOAuthDataProvider.java
+++
b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/provider/JPAOAuthDataProvider.java
@@ -28,6 +28,7 @@ import java.util.function.Function;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
+import jakarta.persistence.LockModeType;
import jakarta.persistence.TypedQuery;
import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.rs.security.oauth2.common.AccessTokenRegistration;
@@ -37,6 +38,7 @@ import
org.apache.cxf.rs.security.oauth2.common.ServerAccessToken;
import org.apache.cxf.rs.security.oauth2.common.UserSubject;
import org.apache.cxf.rs.security.oauth2.tokens.bearer.BearerAccessToken;
import org.apache.cxf.rs.security.oauth2.tokens.refresh.RefreshToken;
+import org.apache.cxf.rs.security.oauth2.utils.OAuthConstants;
/**
* Provides a Jpa BMT implementation for OAuthDataProvider.
@@ -190,6 +192,26 @@ public class JPAOAuthDataProvider extends
AbstractOAuthDataProvider {
});
}
+ @Override
+ protected RefreshToken revokeRefreshToken(Client client, UserSubject
callerSubject, String refreshTokenKey) {
+ // Atomic find + validate + delete in one transaction prevents
concurrent replay of the same refresh token.
+ return executeInTransaction(em -> {
+ RefreshToken refreshToken = em.find(RefreshToken.class,
refreshTokenKey,
+
LockModeType.PESSIMISTIC_WRITE);
+ if (refreshToken != null) {
+ if
(!refreshToken.getClient().getClientId().equals(client.getClientId())) {
+ throw new
OAuthServiceException(OAuthConstants.INVALID_GRANT);
+ }
+ if (callerSubject != null && refreshToken.getSubject() != null
+ &&
!callerSubject.getLogin().equals(refreshToken.getSubject().getLogin())) {
+ throw new
OAuthServiceException(OAuthConstants.INVALID_GRANT);
+ }
+ em.remove(refreshToken);
+ }
+ return refreshToken;
+ });
+ }
+
@Override
protected void doRevokeRefreshToken(final RefreshToken rt) {
executeInTransaction(em -> {
diff --git
a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/provider/AbstractOAuthDataProviderTest.java
b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/provider/AbstractOAuthDataProviderTest.java
index 782cad88b07..732fb18e58c 100644
---
a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/provider/AbstractOAuthDataProviderTest.java
+++
b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/provider/AbstractOAuthDataProviderTest.java
@@ -350,6 +350,31 @@ abstract class AbstractOAuthDataProviderTest {
* Regression test for cross-client refresh token acceptance when
recycleRefreshTokens=false.
* Client B must not be able to exchange Client A's refresh token for an
access token.
*/
+ @Test
+ public void testRefreshTokenSingleUseEnforcedWhenRecycled() {
+ Client c = addClient("101", "bob");
+
+ AccessTokenRegistration atr = new AccessTokenRegistration();
+ atr.setClient(c);
+ atr.setApprovedScope(Arrays.asList("a", "refreshToken"));
+ atr.setSubject(c.getResourceOwnerSubject());
+
+ ServerAccessToken at = getProvider().createAccessToken(atr);
+ String rtKey = at.getRefreshToken();
+ assertNotNull("Expected a refresh token to be issued", rtKey);
+
+ // First use must succeed and invalidate the original token.
+ getProvider().refreshAccessToken(c, rtKey, Collections.emptyList());
+
+ // Second use of the same (now consumed) refresh token must be denied.
+ try {
+ getProvider().refreshAccessToken(c, rtKey,
Collections.emptyList());
+ fail("Replayed refresh token must be rejected");
+ } catch (OAuthServiceException ex) {
+ assertEquals(OAuthConstants.ACCESS_DENIED, ex.getMessage());
+ }
+ }
+
@Test
public void testCrossClientRefreshTokenRejectedWhenRecycleDisabled() {
getProvider().setRecycleRefreshTokens(false);