Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@
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
Expand Down Expand Up @@ -122,6 +125,28 @@ protected void commitIfNeeded(EntityManager em) {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
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;
Expand All @@ -37,6 +38,7 @@
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.
Expand Down Expand Up @@ -190,6 +192,26 @@ protected RefreshToken getRefreshToken(final String refreshTokenKey) {
});
}

@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 -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,31 @@ public void testAddGetDeleteRefreshToken() {
* 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);
Expand Down
Loading