This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/main by this push:
new c95fb636693 Make sure the refresh token client matches that of the
authenticated client (#3346)
c95fb636693 is described below
commit c95fb6366930ce16c32481f230a5afa89489bd25
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Wed Jul 29 11:24:18 2026 +0100
Make sure the refresh token client matches that of the authenticated client
(#3346)
---
.../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"));