This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch coheigea/jpa-refresh
in repository https://gitbox.apache.org/repos/asf/cxf.git

commit cf50327db9904bbce4cc4fc78d171053b1ea7af3
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Tue Aug 11 12:06:55 2026 +0100

    Porting locking fixes to revokeRefreshToken
---
 .../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);

Reply via email to