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 7d13d067dd8 Add a lock when revoking tokens (#3348)
7d13d067dd8 is described below

commit 7d13d067dd8d9ab8b8c30cc23ea6620599bd6b7b
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Wed Jul 29 16:28:20 2026 +0100

    Add a lock when revoking tokens (#3348)
    
    * Add a lock when revoking tokens
    
    * Apply suggestion from @reta
    
    Co-authored-by: Andriy Redko <[email protected]>
    
    * Fixup
    
    ---------
    
    Co-authored-by: Andriy Redko <[email protected]>
    (cherry picked from commit 396096f84930e1924477e5975c2e57d779527e48)
---
 .../oauth2/provider/AbstractOAuthDataProvider.java |  11 +++
 .../DefaultEncryptingOAuthDataProviderTest.java    | 110 +++++++++++++++++++++
 2 files changed, 121 insertions(+)

diff --git 
a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/provider/AbstractOAuthDataProvider.java
 
b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/provider/AbstractOAuthDataProvider.java
index 94ba4ae9f74..4003d40772f 100644
--- 
a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/provider/AbstractOAuthDataProvider.java
+++ 
b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/provider/AbstractOAuthDataProvider.java
@@ -272,6 +272,17 @@ public abstract class AbstractOAuthDataProvider implements 
OAuthDataProvider, Cl
     @Override
     public void revokeToken(Client client, UserSubject callerSubject, String 
tokenKey, String tokenTypeHint)
         throws OAuthServiceException {
+        if (!recycleRefreshTokens) {
+            synchronized (refreshTokenLock) {
+                doRevokeToken(client, callerSubject, tokenKey, tokenTypeHint);
+            }
+        } else {
+            doRevokeToken(client, callerSubject, tokenKey, tokenTypeHint);
+        }
+    }
+
+    private void doRevokeToken(Client client, UserSubject callerSubject,
+                               String tokenKey, String tokenTypeHint) {
         ServerAccessToken accessToken = null;
         if (!OAuthConstants.REFRESH_TOKEN.equals(tokenTypeHint)) {
             accessToken = revokeAccessToken(client, callerSubject, tokenKey);
diff --git 
a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/provider/DefaultEncryptingOAuthDataProviderTest.java
 
b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/provider/DefaultEncryptingOAuthDataProviderTest.java
index 24025b1fa85..d181ae719ec 100644
--- 
a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/provider/DefaultEncryptingOAuthDataProviderTest.java
+++ 
b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/provider/DefaultEncryptingOAuthDataProviderTest.java
@@ -20,6 +20,10 @@ package org.apache.cxf.rs.security.oauth2.provider;
 
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
 
 import org.apache.cxf.rs.security.oauth2.common.AccessTokenRegistration;
 import org.apache.cxf.rs.security.oauth2.common.Client;
@@ -35,9 +39,43 @@ import org.junit.Test;
 
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 
 
 public class DefaultEncryptingOAuthDataProviderTest {
+    private static class BlockingRefreshProvider extends 
DefaultEncryptingOAuthDataProvider {
+        private final CountDownLatch enteredUpdate = new CountDownLatch(1);
+        private final CountDownLatch allowUpdate = new CountDownLatch(1);
+
+        BlockingRefreshProvider() {
+            super(new KeyProperties("AES", 128));
+        }
+
+        @Override
+        protected RefreshToken updateExistingRefreshToken(RefreshToken rt, 
ServerAccessToken at) {
+            enteredUpdate.countDown();
+            try {
+                if (!allowUpdate.await(5, TimeUnit.SECONDS)) {
+                    throw new OAuthServiceException("Timed out waiting to 
continue refresh update");
+                }
+            } catch (InterruptedException ex) {
+                Thread.currentThread().interrupt();
+                throw new OAuthServiceException("Interrupted while waiting to 
continue refresh update", ex);
+            }
+            // For this lock-ordering regression we only need to hold and 
release the lock,
+            // not to exercise refresh-token list mutation semantics.
+            return rt;
+        }
+
+        boolean awaitEnteredUpdate(long timeout, TimeUnit unit) throws 
InterruptedException {
+            return enteredUpdate.await(timeout, unit);
+        }
+
+        void continueUpdate() {
+            allowUpdate.countDown();
+        }
+    }
+
     private DefaultEncryptingOAuthDataProvider provider;
 
     @Before
@@ -264,4 +302,76 @@ public class DefaultEncryptingOAuthDataProviderTest {
         assertNull("Revoked refresh token must return null, preventing token 
minting", 
                    provider.getRefreshToken(refreshTokenKey));
     }
+
+    @Test
+    public void testRevokeWaitsForRefreshWhenRecycleDisabled() throws 
Exception {
+        BlockingRefreshProvider blockingProvider = new 
BlockingRefreshProvider();
+        try {
+            
blockingProvider.setSupportedScopes(Collections.singletonMap("read", "Read 
Scope"));
+            
blockingProvider.setSupportedScopes(Collections.singletonMap("refreshToken", 
"RefreshToken"));
+            blockingProvider.setRecycleRefreshTokens(false);
+
+            Client c = new Client();
+            
c.setRedirectUris(Collections.singletonList("http://client/redirect";));
+            c.setClientId("race-client");
+            c.setClientSecret("secret");
+            c.setResourceOwnerSubject(new UserSubject("race-user"));
+            blockingProvider.setClient(c);
+
+            AccessTokenRegistration atr = new AccessTokenRegistration();
+            atr.setClient(c);
+            atr.setApprovedScope(Arrays.asList("read", "refreshToken"));
+            atr.setSubject(c.getResourceOwnerSubject());
+
+            ServerAccessToken initial = 
blockingProvider.createAccessToken(atr);
+            String refreshTokenKey = initial.getRefreshToken();
+
+            AtomicReference<Throwable> refreshFailure = new 
AtomicReference<>();
+            AtomicReference<Throwable> revokeFailure = new AtomicReference<>();
+
+            Thread refreshThread = new Thread(() -> {
+                try {
+                    blockingProvider.refreshAccessToken(c, refreshTokenKey, 
Collections.emptyList());
+                } catch (Throwable ex) {
+                    refreshFailure.set(ex);
+                }
+            });
+
+            refreshThread.start();
+            assertTrue("Refresh thread should reach the refresh-token update 
phase",
+                       blockingProvider.awaitEnteredUpdate(5, 
TimeUnit.SECONDS));
+
+            Thread revokeThread = new Thread(() -> {
+                try {
+                    blockingProvider.revokeToken(c, refreshTokenKey, 
OAuthConstants.REFRESH_TOKEN);
+                } catch (Throwable ex) {
+                    revokeFailure.set(ex);
+                }
+            });
+
+            revokeThread.start();
+            revokeThread.join(200);
+            assertTrue("Revoke must block while refresh holds 
refreshTokenLock", revokeThread.isAlive());
+
+            blockingProvider.continueUpdate();
+
+            refreshThread.join(5000);
+            revokeThread.join(5000);
+
+            assertTrue("Refresh thread must finish", !refreshThread.isAlive());
+            assertTrue("Revoke thread must finish after refresh releases 
lock", !revokeThread.isAlive());
+            assertNull("Refresh thread must not fail", refreshFailure.get());
+            assertNull("Revoke thread must not fail", revokeFailure.get());
+            assertNull("Refresh token should be revoked after revoke thread 
completes",
+                       blockingProvider.getRefreshToken(refreshTokenKey));
+
+            List<ServerAccessToken> remaining = 
blockingProvider.getAccessTokens(c, c.getResourceOwnerSubject());
+            for (ServerAccessToken token : remaining) {
+                assertNull("No remaining access token should reference a 
revoked refresh token",
+                           token.getRefreshToken());
+            }
+        } finally {
+            blockingProvider.close();
+        }
+    }
 }

Reply via email to