You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cxf.apache.org by co...@apache.org on 2026/07/29 10:25:01 UTC

(cxf) 02/02: Make sure the refresh token client matches that of the authenticated client (#3346)

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

commit a2a604a4ffeef1b21a5ddda95f82059da33dafd6
Author: Colm O hEigeartaigh <co...@users.noreply.github.com>
AuthorDate: Wed Jul 29 11:24:18 2026 +0100

    Make sure the refresh token client matches that of the authenticated client (#3346)
    
    (cherry picked from commit c95fb6366930ce16c32481f230a5afa89489bd25)
---
 .../oauth2/provider/AbstractOAuthDataProvider.java |  3 ++
 .../provider/AbstractOAuthDataProviderTest.java    | 32 ++++++++++++++++++++++
 2 files changed, 35 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 8a35996a968..d425d3c8fbe 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
@@ -240,6 +240,9 @@ public abstract class AbstractOAuthDataProvider implements OAuthDataProvider, Cl
         if (currentRefreshToken == null) {
             throw new OAuthServiceException(OAuthConstants.ACCESS_DENIED);
         }
+        if (!currentRefreshToken.getClient().getClientId().equals(client.getClientId())) {
+            throw new OAuthServiceException(OAuthConstants.INVALID_GRANT);
+        }
         if (OAuthUtils.isExpired(currentRefreshToken.getIssuedAt(), currentRefreshToken.getExpiresIn())) {
             if (!recycleRefreshTokens) {
                 revokeRefreshToken(client, refreshTokenKey);
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 0d160d352cd..782cad88b07 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
@@ -346,6 +346,38 @@ abstract class AbstractOAuthDataProviderTest {
         assertNull(getProvider().getRefreshToken(rt.getTokenKey()));
     }
 
+    /**
+     * 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 testCrossClientRefreshTokenRejectedWhenRecycleDisabled() {
+        getProvider().setRecycleRefreshTokens(false);
+
+        // Client A obtains a refresh token for its resource owner.
+        Client clientA = addClient("101", "alice");
+        AccessTokenRegistration atrA = new AccessTokenRegistration();
+        atrA.setClient(clientA);
+        atrA.setApprovedScope(Arrays.asList("a", "refreshToken"));
+        atrA.setSubject(clientA.getResourceOwnerSubject());
+        ServerAccessToken atA = getProvider().createAccessToken(atrA);
+        assertNotNull(atA.getRefreshToken());
+
+        // Client B is a separate registered client controlled by an attacker.
+        Client clientB = addClient("102", "bob");
+
+        // Attacker uses Client B credentials to present Client A's refresh token.
+        try {
+            getProvider().refreshAccessToken(clientB, atA.getRefreshToken(), Collections.emptyList());
+            fail("Cross-client refresh token use must be rejected with OAuthServiceException");
+        } catch (OAuthServiceException ex) {
+            assertEquals(OAuthConstants.INVALID_GRANT, ex.getMessage());
+        }
+
+        // Client A's original access token must remain usable.
+        assertNotNull(getProvider().getAccessToken(atA.getTokenKey()));
+    }
+
     protected Client addClient(String clientId, String userLogin) {
         Client c = new Client();
         c.setRedirectUris(Collections.singletonList("http://client/redirect"));