Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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 @@ -50,6 +50,7 @@
import org.wso2.carbon.identity.oauth.cache.OAuthCache;
import org.wso2.carbon.identity.oauth.cache.OAuthCacheKey;
import org.wso2.carbon.identity.oauth.common.OAuthConstants;
import org.wso2.carbon.identity.oauth.config.OAuthServerConfiguration;
import org.wso2.carbon.identity.oauth.dao.OAuthAppDO;
import org.wso2.carbon.identity.oauth.dto.OAuthConsumerAppDTO;
import org.wso2.carbon.identity.oauth.event.OAuthEventInterceptor;
Expand All @@ -62,6 +63,7 @@
import org.wso2.carbon.identity.oauth2.IdentityOAuth2ServerException;
import org.wso2.carbon.identity.oauth2.dao.OAuthTokenPersistenceFactory;
import org.wso2.carbon.identity.oauth2.dao.SharedAppResolveDAO;
import org.wso2.carbon.identity.oauth2.dto.OAuthRevocationRequestDTO;
import org.wso2.carbon.identity.oauth2.model.AccessTokenDO;
import org.wso2.carbon.identity.oauth2.model.AuthzCodeDO;
import org.wso2.carbon.identity.oauth2.util.OAuth2Util;
Expand Down Expand Up @@ -1573,4 +1575,58 @@ private static String getUserStoreDomainOfParentUser(String parentUserId, String
+ parentUserId + " in tenant domain: " + tenantDomain, e);
}
}

/**
* Triggers a cache clearance for the original scopes associated with a token.
* This is necessary because the scopes in the provided {@code accessTokenDO} might have been
* filtered or mutated during the request lifecycle. Fetch the original scopes from the
* database to ensure the cache is cleared using the correct keys.
*
* @param tokenBindingReference The token binding identifier.
* @param accessTokenDO The current (potentially mutated) access token object.
* @param revokeRequestDTO The revocation request details containing the consumer key.
*/
public static void clearOAuthCacheUsingPersistedScopes(String tokenBindingReference, AccessTokenDO accessTokenDO,
OAuthRevocationRequestDTO revokeRequestDTO) {

if (LOG.isDebugEnabled()) {
LOG.debug("Clearing OAuth cache for persisted scopes. Consumer key: " + revokeRequestDTO.getConsumerKey());
}

if (OAuthServerConfiguration.getInstance().getAllowedScopes().isEmpty()) {
return;
}

String accessToken = accessTokenDO.getAccessToken();
if (StringUtils.isBlank(accessToken)) {
return;
}

try {
// The in-memory scopes may be mutated during validation. To avoid cache-key
// mismatches, retrieve the original scopes from the database before clearing
// the OAuth cache.
AccessTokenDO dbTokenDO = OAuthTokenPersistenceFactory.getInstance()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accessing dao layer from util method may not be the correct way. Don't we have service methods to fetch the access token.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed with 5584aaf

.getAccessTokenDAO()
.getAccessToken(accessToken, true);
if (dbTokenDO == null || dbTokenDO.getScope() == null || dbTokenDO.getScope().length == 0) {
return;
}

String dbTokenScopeString = OAuth2Util.buildScopeString(dbTokenDO.getScope());
OAuthUtil.clearOAuthCache(revokeRequestDTO.getConsumerKey(),
accessTokenDO.getAuthzUser(), dbTokenScopeString, tokenBindingReference);
OAuthUtil.clearOAuthCache(revokeRequestDTO.getConsumerKey(),
accessTokenDO.getAuthzUser(), dbTokenScopeString);

if (LOG.isDebugEnabled()) {
LOG.debug("Successfully cleared OAuth cache entries for consumer key: " +
revokeRequestDTO.getConsumerKey());
}

} catch (IdentityOAuth2Exception e) {
LOG.error("Error while clearing cache entries for extended scopes. Consumer key: "
+ revokeRequestDTO.getConsumerKey(), e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,8 @@ public OAuthRevocationResponseDTO revokeTokenByOAuthClient(OAuthRevocationReques
OAuth2Util.buildScopeString(accessTokenDO.getScope()));
OAuthUtil.clearOAuthCache(revokeRequestDTO.getConsumerKey(), accessTokenDO.getAuthzUser());
OAuthUtil.clearOAuthCache(accessTokenDO);
OAuthUtil.clearOAuthCacheUsingPersistedScopes(tokenBindingReference,
accessTokenDO, revokeRequestDTO);
String scope = OAuth2Util.buildScopeString(accessTokenDO.getScope());
synchronized ((revokeRequestDTO.getConsumerKey() + ":" + userId + ":" + scope + ":"
+ tokenBindingReference).intern()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import org.wso2.carbon.identity.oauth.cache.CacheEntry;
import org.wso2.carbon.identity.oauth.cache.OAuthCache;
import org.wso2.carbon.identity.oauth.cache.OAuthCacheKey;
import org.wso2.carbon.identity.oauth.config.OAuthServerConfiguration;
import org.wso2.carbon.identity.oauth.internal.OAuthComponentServiceHolder;
import org.wso2.carbon.identity.oauth.internal.util.AccessTokenEventUtil;
import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception;
Expand All @@ -54,6 +55,7 @@
import org.wso2.carbon.identity.oauth2.dao.AuthorizationCodeDAOImpl;
import org.wso2.carbon.identity.oauth2.dao.OAuthTokenPersistenceFactory;
import org.wso2.carbon.identity.oauth2.dao.TokenManagementDAO;
import org.wso2.carbon.identity.oauth2.dto.OAuthRevocationRequestDTO;
import org.wso2.carbon.identity.oauth2.model.AccessTokenDO;
import org.wso2.carbon.identity.oauth2.model.AuthzCodeDO;
import org.wso2.carbon.identity.oauth2.util.OAuth2Util;
Expand Down Expand Up @@ -808,6 +810,246 @@ public void testRemoveAuthzGrantCacheForUser_WithIdentityOAuth2Exception() throw
}
}

@Test
public void testClearOAuthCacheUsingPersistedScopes_WhenAllowedScopesIsEmpty() throws Exception {

try (MockedStatic<OAuthServerConfiguration> oAuthServerConfigStatic =
mockStatic(OAuthServerConfiguration.class)) {
OAuthServerConfiguration mockServerConfig = mock(OAuthServerConfiguration.class);
oAuthServerConfigStatic.when(OAuthServerConfiguration::getInstance).thenReturn(mockServerConfig);
when(mockServerConfig.getAllowedScopes()).thenReturn(Collections.emptyList());

OAuthTokenPersistenceFactory mockFactory = mock(OAuthTokenPersistenceFactory.class);
oAuthTokenPersistenceFactory.when(OAuthTokenPersistenceFactory::getInstance).thenReturn(mockFactory);
AccessTokenDAO mockAccessTokenDAO = mock(AccessTokenDAO.class);

AccessTokenDO accessTokenDO = new AccessTokenDO();
accessTokenDO.setAccessToken("testAccessToken");
OAuthRevocationRequestDTO revokeRequestDTO = mock(OAuthRevocationRequestDTO.class);

OAuthUtil.clearOAuthCacheUsingPersistedScopes("tokenBindingRef",
accessTokenDO, revokeRequestDTO);

verify(mockAccessTokenDAO, never()).getAccessToken(anyString(), anyBoolean());
}
}

@Test
public void testClearOAuthCacheUsingPersistedScopes_WhenAccessTokenIsBlank() throws Exception {

try (MockedStatic<OAuthServerConfiguration> oAuthServerConfigStatic =
mockStatic(OAuthServerConfiguration.class)) {
OAuthServerConfiguration mockServerConfig = mock(OAuthServerConfiguration.class);
oAuthServerConfigStatic.when(OAuthServerConfiguration::getInstance).thenReturn(mockServerConfig);
when(mockServerConfig.getAllowedScopes()).thenReturn(Collections.singletonList("openid"));

OAuthTokenPersistenceFactory mockFactory = mock(OAuthTokenPersistenceFactory.class);
oAuthTokenPersistenceFactory.when(OAuthTokenPersistenceFactory::getInstance).thenReturn(mockFactory);
AccessTokenDAO mockAccessTokenDAO = mock(AccessTokenDAO.class);

// Access token is not set — defaults to null (blank).
AccessTokenDO accessTokenDO = new AccessTokenDO();
OAuthRevocationRequestDTO revokeRequestDTO = mock(OAuthRevocationRequestDTO.class);

OAuthUtil.clearOAuthCacheUsingPersistedScopes("tokenBindingRef",
accessTokenDO, revokeRequestDTO);

verify(mockAccessTokenDAO, never()).getAccessToken(anyString(), anyBoolean());
}
}

@Test
public void testClearOAuthCacheUsingPersistedScopes_WhenDbTokenIsNull() throws Exception {

try (MockedStatic<OAuthServerConfiguration> oAuthServerConfigStatic =
mockStatic(OAuthServerConfiguration.class);
MockedStatic<OAuthUtil> mockedOAuthUtil = mockStatic(OAuthUtil.class)) {
OAuthServerConfiguration mockServerConfig = mock(OAuthServerConfiguration.class);
oAuthServerConfigStatic.when(OAuthServerConfiguration::getInstance).thenReturn(mockServerConfig);
when(mockServerConfig.getAllowedScopes()).thenReturn(Collections.singletonList("openid"));

OAuthTokenPersistenceFactory mockFactory = mock(OAuthTokenPersistenceFactory.class);
oAuthTokenPersistenceFactory.when(OAuthTokenPersistenceFactory::getInstance).thenReturn(mockFactory);
AccessTokenDAO mockAccessTokenDAO = mock(AccessTokenDAO.class);
when(mockFactory.getAccessTokenDAO()).thenReturn(mockAccessTokenDAO);
when(mockAccessTokenDAO.getAccessToken("testAccessToken", true)).thenReturn(null);

mockedOAuthUtil.when(() -> OAuthUtil.clearOAuthCacheUsingPersistedScopes(
anyString(), any(AccessTokenDO.class), any(OAuthRevocationRequestDTO.class)))
.thenCallRealMethod();

AccessTokenDO accessTokenDO = new AccessTokenDO();
accessTokenDO.setAccessToken("testAccessToken");
OAuthRevocationRequestDTO revokeRequestDTO = mock(OAuthRevocationRequestDTO.class);

OAuthUtil.clearOAuthCacheUsingPersistedScopes("tokenBindingRef", accessTokenDO, revokeRequestDTO);

mockedOAuthUtil.verify(() -> OAuthUtil.clearOAuthCache(
anyString(), any(AuthenticatedUser.class), anyString(), anyString()), never());
mockedOAuthUtil.verify(() -> OAuthUtil.clearOAuthCache(
anyString(), any(AuthenticatedUser.class), anyString()), never());
}
}

@Test
public void testClearOAuthCacheUsingPersistedScopes_WhenDbTokenScopeIsNull() throws Exception {

try (MockedStatic<OAuthServerConfiguration> oAuthServerConfigStatic =
mockStatic(OAuthServerConfiguration.class);
MockedStatic<OAuthUtil> mockedOAuthUtil = mockStatic(OAuthUtil.class)) {
OAuthServerConfiguration mockServerConfig = mock(OAuthServerConfiguration.class);
oAuthServerConfigStatic.when(OAuthServerConfiguration::getInstance).thenReturn(mockServerConfig);
when(mockServerConfig.getAllowedScopes()).thenReturn(Collections.singletonList("openid"));

OAuthTokenPersistenceFactory mockFactory = mock(OAuthTokenPersistenceFactory.class);
oAuthTokenPersistenceFactory.when(OAuthTokenPersistenceFactory::getInstance).thenReturn(mockFactory);
AccessTokenDAO mockAccessTokenDAO = mock(AccessTokenDAO.class);
when(mockFactory.getAccessTokenDAO()).thenReturn(mockAccessTokenDAO);

AccessTokenDO dbTokenDO = new AccessTokenDO();
dbTokenDO.setScope(null);
when(mockAccessTokenDAO.getAccessToken("testAccessToken",
true)).thenReturn(dbTokenDO);

mockedOAuthUtil.when(() -> OAuthUtil.clearOAuthCacheUsingPersistedScopes(
anyString(), any(AccessTokenDO.class), any(OAuthRevocationRequestDTO.class)))
.thenCallRealMethod();

AccessTokenDO accessTokenDO = new AccessTokenDO();
accessTokenDO.setAccessToken("testAccessToken");
OAuthRevocationRequestDTO revokeRequestDTO = mock(OAuthRevocationRequestDTO.class);

OAuthUtil.clearOAuthCacheUsingPersistedScopes("tokenBindingRef",
accessTokenDO, revokeRequestDTO);

mockedOAuthUtil.verify(() -> OAuthUtil.clearOAuthCache(
anyString(), any(AuthenticatedUser.class), anyString(), anyString()), never());
mockedOAuthUtil.verify(() -> OAuthUtil.clearOAuthCache(
anyString(), any(AuthenticatedUser.class), anyString()), never());
}
}

@Test
public void testClearOAuthCacheUsingPersistedScopes_WhenDbTokenScopeIsEmpty() throws Exception {

try (MockedStatic<OAuthServerConfiguration> oAuthServerConfigStatic =
mockStatic(OAuthServerConfiguration.class);
MockedStatic<OAuthUtil> mockedOAuthUtil = mockStatic(OAuthUtil.class)) {
OAuthServerConfiguration mockServerConfig = mock(OAuthServerConfiguration.class);
oAuthServerConfigStatic.when(OAuthServerConfiguration::getInstance).thenReturn(mockServerConfig);
when(mockServerConfig.getAllowedScopes()).thenReturn(Collections.singletonList("openid"));

OAuthTokenPersistenceFactory mockFactory = mock(OAuthTokenPersistenceFactory.class);
oAuthTokenPersistenceFactory.when(OAuthTokenPersistenceFactory::getInstance).thenReturn(mockFactory);
AccessTokenDAO mockAccessTokenDAO = mock(AccessTokenDAO.class);
when(mockFactory.getAccessTokenDAO()).thenReturn(mockAccessTokenDAO);

AccessTokenDO dbTokenDO = new AccessTokenDO();
dbTokenDO.setScope(new String[0]);
when(mockAccessTokenDAO.getAccessToken("testAccessToken",
true)).thenReturn(dbTokenDO);

mockedOAuthUtil.when(() -> OAuthUtil.clearOAuthCacheUsingPersistedScopes(
anyString(), any(AccessTokenDO.class), any(OAuthRevocationRequestDTO.class)))
.thenCallRealMethod();

AccessTokenDO accessTokenDO = new AccessTokenDO();
accessTokenDO.setAccessToken("testAccessToken");
OAuthRevocationRequestDTO revokeRequestDTO = mock(OAuthRevocationRequestDTO.class);

OAuthUtil.clearOAuthCacheUsingPersistedScopes("tokenBindingRef",
accessTokenDO, revokeRequestDTO);

mockedOAuthUtil.verify(() -> OAuthUtil.clearOAuthCache(
anyString(), any(AuthenticatedUser.class), anyString(), anyString()), never());
mockedOAuthUtil.verify(() -> OAuthUtil.clearOAuthCache(
anyString(), any(AuthenticatedUser.class), anyString()), never());
}
}

@Test
public void testClearOAuthCacheUsingPersistedScopes_ClearsCacheWithPersistedScopes() throws Exception {

String tokenBindingReference = "tokenBindingRef";
String accessToken = "testAccessToken";
String consumerKey = "testConsumerKey";
String[] dbScopes = {"scope1", "scope2"};
String dbScopeString = "scope1 scope2";

try (MockedStatic<OAuthServerConfiguration> oAuthServerConfigStatic =
mockStatic(OAuthServerConfiguration.class);
MockedStatic<OAuthUtil> mockedOAuthUtil = mockStatic(OAuthUtil.class)) {
OAuthServerConfiguration mockServerConfig = mock(OAuthServerConfiguration.class);
oAuthServerConfigStatic.when(OAuthServerConfiguration::getInstance).thenReturn(mockServerConfig);
when(mockServerConfig.getAllowedScopes()).thenReturn(Collections.singletonList("scope1"));

OAuthTokenPersistenceFactory mockFactory = mock(OAuthTokenPersistenceFactory.class);
oAuthTokenPersistenceFactory.when(OAuthTokenPersistenceFactory::getInstance).thenReturn(mockFactory);
AccessTokenDAO mockAccessTokenDAO = mock(AccessTokenDAO.class);
when(mockFactory.getAccessTokenDAO()).thenReturn(mockAccessTokenDAO);

AccessTokenDO dbTokenDO = new AccessTokenDO();
dbTokenDO.setScope(dbScopes);
when(mockAccessTokenDAO.getAccessToken(accessToken, true)).thenReturn(dbTokenDO);

oAuth2Util.when(() -> OAuth2Util.buildScopeString(dbScopes)).thenReturn(dbScopeString);

mockedOAuthUtil.when(() -> OAuthUtil.clearOAuthCacheUsingPersistedScopes(
anyString(), any(AccessTokenDO.class), any(OAuthRevocationRequestDTO.class)))
.thenCallRealMethod();

AuthenticatedUser authzUser = mock(AuthenticatedUser.class);
AccessTokenDO accessTokenDO = new AccessTokenDO();
accessTokenDO.setAccessToken(accessToken);
accessTokenDO.setAuthzUser(authzUser);

OAuthRevocationRequestDTO revokeRequestDTO = mock(OAuthRevocationRequestDTO.class);
when(revokeRequestDTO.getConsumerKey()).thenReturn(consumerKey);

OAuthUtil.clearOAuthCacheUsingPersistedScopes(tokenBindingReference, accessTokenDO, revokeRequestDTO);

mockedOAuthUtil.verify(() -> OAuthUtil.clearOAuthCache(
eq(consumerKey), eq(authzUser), eq(dbScopeString), eq(tokenBindingReference)), times(1));
mockedOAuthUtil.verify(() -> OAuthUtil.clearOAuthCache(
eq(consumerKey), eq(authzUser), eq(dbScopeString)), times(1));
}
}

@Test
public void testClearOAuthCacheUsingPersistedScopes_WhenDaoThrowsException() throws Exception {

try (MockedStatic<OAuthServerConfiguration> oAuthServerConfigStatic =
mockStatic(OAuthServerConfiguration.class);
MockedStatic<OAuthUtil> mockedOAuthUtil = mockStatic(OAuthUtil.class)) {
OAuthServerConfiguration mockServerConfig = mock(OAuthServerConfiguration.class);
oAuthServerConfigStatic.when(OAuthServerConfiguration::getInstance).thenReturn(mockServerConfig);
when(mockServerConfig.getAllowedScopes()).thenReturn(Collections.singletonList("openid"));

OAuthTokenPersistenceFactory mockFactory = mock(OAuthTokenPersistenceFactory.class);
oAuthTokenPersistenceFactory.when(OAuthTokenPersistenceFactory::getInstance).thenReturn(mockFactory);
AccessTokenDAO mockAccessTokenDAO = mock(AccessTokenDAO.class);
when(mockFactory.getAccessTokenDAO()).thenReturn(mockAccessTokenDAO);
when(mockAccessTokenDAO.getAccessToken(anyString(), anyBoolean()))
.thenThrow(new IdentityOAuth2Exception("DAO error"));

mockedOAuthUtil.when(() -> OAuthUtil.clearOAuthCacheUsingPersistedScopes(
anyString(), any(AccessTokenDO.class), any(OAuthRevocationRequestDTO.class)))
.thenCallRealMethod();

AccessTokenDO accessTokenDO = new AccessTokenDO();
accessTokenDO.setAccessToken("testAccessToken");
OAuthRevocationRequestDTO revokeRequestDTO = mock(OAuthRevocationRequestDTO.class);
when(revokeRequestDTO.getConsumerKey()).thenReturn("testConsumerKey");

// Exception should be caught and logged internally, not propagated.
OAuthUtil.clearOAuthCacheUsingPersistedScopes("tokenBindingRef",
accessTokenDO, revokeRequestDTO);

mockedOAuthUtil.verify(() -> OAuthUtil.clearOAuthCache(
anyString(), any(AuthenticatedUser.class), anyString(), anyString()), never());
}
}

private OAuthCache getOAuthCache(OAuthCacheKey oAuthCacheKey) {

// Add some value to OAuthCache.
Expand Down
Loading