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

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


The following commit(s) were added to refs/heads/3.6.x-fixes by this push:
     new 2631b9d023d Porting locking fixes to revokeRefreshToken (#3364)
2631b9d023d is described below

commit 2631b9d023d342ba3440c2d7bf43473e6f0ea41f
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 73524ddbb81..686db5892d5 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
@@ -26,9 +26,12 @@ import javax.persistence.EntityManager;
 import javax.persistence.EntityTransaction;
 import javax.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
@@ -123,6 +126,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 dcb5be9c62f..c209ba8925b 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 javax.persistence.EntityManager;
 import javax.persistence.EntityManagerFactory;
 import javax.persistence.EntityTransaction;
+import javax.persistence.LockModeType;
 import javax.persistence.TypedQuery;
 
 import org.apache.cxf.helpers.CastUtils;
@@ -38,6 +39,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.
@@ -191,6 +193,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