Skip to content

Commit cf8b7a8

Browse files
committed
Implement cache clearance for original token scopes during revocation
1 parent 3f1dc08 commit cf8b7a8

File tree

3 files changed

+291
-0
lines changed

3 files changed

+291
-0
lines changed

components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth/OAuthUtil.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import org.wso2.carbon.identity.oauth.cache.OAuthCache;
5151
import org.wso2.carbon.identity.oauth.cache.OAuthCacheKey;
5252
import org.wso2.carbon.identity.oauth.common.OAuthConstants;
53+
import org.wso2.carbon.identity.oauth.config.OAuthServerConfiguration;
5354
import org.wso2.carbon.identity.oauth.dao.OAuthAppDO;
5455
import org.wso2.carbon.identity.oauth.dto.OAuthConsumerAppDTO;
5556
import org.wso2.carbon.identity.oauth.event.OAuthEventInterceptor;
@@ -62,6 +63,7 @@
6263
import org.wso2.carbon.identity.oauth2.IdentityOAuth2ServerException;
6364
import org.wso2.carbon.identity.oauth2.dao.OAuthTokenPersistenceFactory;
6465
import org.wso2.carbon.identity.oauth2.dao.SharedAppResolveDAO;
66+
import org.wso2.carbon.identity.oauth2.dto.OAuthRevocationRequestDTO;
6567
import org.wso2.carbon.identity.oauth2.model.AccessTokenDO;
6668
import org.wso2.carbon.identity.oauth2.model.AuthzCodeDO;
6769
import org.wso2.carbon.identity.oauth2.util.OAuth2Util;
@@ -1573,4 +1575,49 @@ private static String getUserStoreDomainOfParentUser(String parentUserId, String
15731575
+ parentUserId + " in tenant domain: " + tenantDomain, e);
15741576
}
15751577
}
1578+
1579+
/**
1580+
* Triggers a cache clearance for the original scopes associated with a token.
1581+
* This is necessary because the scopes in the provided {@code accessTokenDO} might have been
1582+
* filtered or mutated during the request lifecycle. Fetch the original scopes from the
1583+
* database to ensure the cache is cleared using the correct keys.
1584+
*
1585+
* @param tokenBindingReference The token binding identifier.
1586+
* @param accessTokenDO The current (potentially mutated) access token object.
1587+
* @param revokeRequestDTO The revocation request details containing the consumer key.
1588+
*/
1589+
public static void clearOAuthCacheUsingPersistedScopes(String tokenBindingReference, AccessTokenDO accessTokenDO,
1590+
OAuthRevocationRequestDTO revokeRequestDTO) {
1591+
1592+
if (OAuthServerConfiguration.getInstance().getAllowedScopes().isEmpty()) {
1593+
return;
1594+
}
1595+
1596+
String accessToken = accessTokenDO.getAccessToken();
1597+
if (StringUtils.isBlank(accessToken)) {
1598+
return;
1599+
}
1600+
1601+
try {
1602+
// The in-memory scopes may be mutated during validation. To avoid cache-key
1603+
// mismatches, retrieve the original scopes from the database before clearing
1604+
// the OAuth cache.
1605+
AccessTokenDO dbTokenDO = OAuthTokenPersistenceFactory.getInstance()
1606+
.getAccessTokenDAO()
1607+
.getAccessToken(accessToken, true);
1608+
if (dbTokenDO == null || dbTokenDO.getScope() == null || dbTokenDO.getScope().length == 0) {
1609+
return;
1610+
}
1611+
1612+
String dbTokenScopeString = OAuth2Util.buildScopeString(dbTokenDO.getScope());
1613+
OAuthUtil.clearOAuthCache(revokeRequestDTO.getConsumerKey(),
1614+
accessTokenDO.getAuthzUser(), dbTokenScopeString, tokenBindingReference);
1615+
OAuthUtil.clearOAuthCache(revokeRequestDTO.getConsumerKey(),
1616+
accessTokenDO.getAuthzUser(), dbTokenScopeString);
1617+
1618+
} catch (IdentityOAuth2Exception e) {
1619+
LOG.error("Error while clearing cache entries for extended scopes. Consumer key: "
1620+
+ revokeRequestDTO.getConsumerKey(), e);
1621+
}
1622+
}
15761623
}

components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/OAuth2Service.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,8 @@ public OAuthRevocationResponseDTO revokeTokenByOAuthClient(OAuthRevocationReques
743743
OAuth2Util.buildScopeString(accessTokenDO.getScope()));
744744
OAuthUtil.clearOAuthCache(revokeRequestDTO.getConsumerKey(), accessTokenDO.getAuthzUser());
745745
OAuthUtil.clearOAuthCache(accessTokenDO);
746+
OAuthUtil.clearOAuthCacheUsingPersistedScopes(tokenBindingReference,
747+
accessTokenDO, revokeRequestDTO);
746748
String scope = OAuth2Util.buildScopeString(accessTokenDO.getScope());
747749
synchronized ((revokeRequestDTO.getConsumerKey() + ":" + userId + ":" + scope + ":"
748750
+ tokenBindingReference).intern()) {

components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth/OAuthUtilTest.java

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.wso2.carbon.identity.oauth.cache.CacheEntry;
4747
import org.wso2.carbon.identity.oauth.cache.OAuthCache;
4848
import org.wso2.carbon.identity.oauth.cache.OAuthCacheKey;
49+
import org.wso2.carbon.identity.oauth.config.OAuthServerConfiguration;
4950
import org.wso2.carbon.identity.oauth.internal.OAuthComponentServiceHolder;
5051
import org.wso2.carbon.identity.oauth.internal.util.AccessTokenEventUtil;
5152
import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception;
@@ -54,6 +55,7 @@
5455
import org.wso2.carbon.identity.oauth2.dao.AuthorizationCodeDAOImpl;
5556
import org.wso2.carbon.identity.oauth2.dao.OAuthTokenPersistenceFactory;
5657
import org.wso2.carbon.identity.oauth2.dao.TokenManagementDAO;
58+
import org.wso2.carbon.identity.oauth2.dto.OAuthRevocationRequestDTO;
5759
import org.wso2.carbon.identity.oauth2.model.AccessTokenDO;
5860
import org.wso2.carbon.identity.oauth2.model.AuthzCodeDO;
5961
import org.wso2.carbon.identity.oauth2.util.OAuth2Util;
@@ -808,6 +810,246 @@ public void testRemoveAuthzGrantCacheForUser_WithIdentityOAuth2Exception() throw
808810
}
809811
}
810812

813+
@Test
814+
public void testClearOAuthCacheUsingPersistedScopes_WhenAllowedScopesIsEmpty() throws Exception {
815+
816+
try (MockedStatic<OAuthServerConfiguration> oAuthServerConfigStatic =
817+
mockStatic(OAuthServerConfiguration.class)) {
818+
OAuthServerConfiguration mockServerConfig = mock(OAuthServerConfiguration.class);
819+
oAuthServerConfigStatic.when(OAuthServerConfiguration::getInstance).thenReturn(mockServerConfig);
820+
when(mockServerConfig.getAllowedScopes()).thenReturn(Collections.emptyList());
821+
822+
OAuthTokenPersistenceFactory mockFactory = mock(OAuthTokenPersistenceFactory.class);
823+
oAuthTokenPersistenceFactory.when(OAuthTokenPersistenceFactory::getInstance).thenReturn(mockFactory);
824+
AccessTokenDAO mockAccessTokenDAO = mock(AccessTokenDAO.class);
825+
826+
AccessTokenDO accessTokenDO = new AccessTokenDO();
827+
accessTokenDO.setAccessToken("testAccessToken");
828+
OAuthRevocationRequestDTO revokeRequestDTO = mock(OAuthRevocationRequestDTO.class);
829+
830+
OAuthUtil.clearOAuthCacheUsingPersistedScopes("tokenBindingRef",
831+
accessTokenDO, revokeRequestDTO);
832+
833+
verify(mockAccessTokenDAO, never()).getAccessToken(anyString(), anyBoolean());
834+
}
835+
}
836+
837+
@Test
838+
public void testClearOAuthCacheUsingPersistedScopes_WhenAccessTokenIsBlank() throws Exception {
839+
840+
try (MockedStatic<OAuthServerConfiguration> oAuthServerConfigStatic =
841+
mockStatic(OAuthServerConfiguration.class)) {
842+
OAuthServerConfiguration mockServerConfig = mock(OAuthServerConfiguration.class);
843+
oAuthServerConfigStatic.when(OAuthServerConfiguration::getInstance).thenReturn(mockServerConfig);
844+
when(mockServerConfig.getAllowedScopes()).thenReturn(Collections.singletonList("openid"));
845+
846+
OAuthTokenPersistenceFactory mockFactory = mock(OAuthTokenPersistenceFactory.class);
847+
oAuthTokenPersistenceFactory.when(OAuthTokenPersistenceFactory::getInstance).thenReturn(mockFactory);
848+
AccessTokenDAO mockAccessTokenDAO = mock(AccessTokenDAO.class);
849+
850+
// Access token is not set — defaults to null (blank).
851+
AccessTokenDO accessTokenDO = new AccessTokenDO();
852+
OAuthRevocationRequestDTO revokeRequestDTO = mock(OAuthRevocationRequestDTO.class);
853+
854+
OAuthUtil.clearOAuthCacheUsingPersistedScopes("tokenBindingRef",
855+
accessTokenDO, revokeRequestDTO);
856+
857+
verify(mockAccessTokenDAO, never()).getAccessToken(anyString(), anyBoolean());
858+
}
859+
}
860+
861+
@Test
862+
public void testClearOAuthCacheUsingPersistedScopes_WhenDbTokenIsNull() throws Exception {
863+
864+
try (MockedStatic<OAuthServerConfiguration> oAuthServerConfigStatic =
865+
mockStatic(OAuthServerConfiguration.class);
866+
MockedStatic<OAuthUtil> mockedOAuthUtil = mockStatic(OAuthUtil.class)) {
867+
OAuthServerConfiguration mockServerConfig = mock(OAuthServerConfiguration.class);
868+
oAuthServerConfigStatic.when(OAuthServerConfiguration::getInstance).thenReturn(mockServerConfig);
869+
when(mockServerConfig.getAllowedScopes()).thenReturn(Collections.singletonList("openid"));
870+
871+
OAuthTokenPersistenceFactory mockFactory = mock(OAuthTokenPersistenceFactory.class);
872+
oAuthTokenPersistenceFactory.when(OAuthTokenPersistenceFactory::getInstance).thenReturn(mockFactory);
873+
AccessTokenDAO mockAccessTokenDAO = mock(AccessTokenDAO.class);
874+
when(mockFactory.getAccessTokenDAO()).thenReturn(mockAccessTokenDAO);
875+
when(mockAccessTokenDAO.getAccessToken("testAccessToken", true)).thenReturn(null);
876+
877+
mockedOAuthUtil.when(() -> OAuthUtil.clearOAuthCacheUsingPersistedScopes(
878+
anyString(), any(AccessTokenDO.class), any(OAuthRevocationRequestDTO.class)))
879+
.thenCallRealMethod();
880+
881+
AccessTokenDO accessTokenDO = new AccessTokenDO();
882+
accessTokenDO.setAccessToken("testAccessToken");
883+
OAuthRevocationRequestDTO revokeRequestDTO = mock(OAuthRevocationRequestDTO.class);
884+
885+
OAuthUtil.clearOAuthCacheUsingPersistedScopes("tokenBindingRef", accessTokenDO, revokeRequestDTO);
886+
887+
mockedOAuthUtil.verify(() -> OAuthUtil.clearOAuthCache(
888+
anyString(), any(AuthenticatedUser.class), anyString(), anyString()), never());
889+
mockedOAuthUtil.verify(() -> OAuthUtil.clearOAuthCache(
890+
anyString(), any(AuthenticatedUser.class), anyString()), never());
891+
}
892+
}
893+
894+
@Test
895+
public void testClearOAuthCacheUsingPersistedScopes_WhenDbTokenScopeIsNull() throws Exception {
896+
897+
try (MockedStatic<OAuthServerConfiguration> oAuthServerConfigStatic =
898+
mockStatic(OAuthServerConfiguration.class);
899+
MockedStatic<OAuthUtil> mockedOAuthUtil = mockStatic(OAuthUtil.class)) {
900+
OAuthServerConfiguration mockServerConfig = mock(OAuthServerConfiguration.class);
901+
oAuthServerConfigStatic.when(OAuthServerConfiguration::getInstance).thenReturn(mockServerConfig);
902+
when(mockServerConfig.getAllowedScopes()).thenReturn(Collections.singletonList("openid"));
903+
904+
OAuthTokenPersistenceFactory mockFactory = mock(OAuthTokenPersistenceFactory.class);
905+
oAuthTokenPersistenceFactory.when(OAuthTokenPersistenceFactory::getInstance).thenReturn(mockFactory);
906+
AccessTokenDAO mockAccessTokenDAO = mock(AccessTokenDAO.class);
907+
when(mockFactory.getAccessTokenDAO()).thenReturn(mockAccessTokenDAO);
908+
909+
AccessTokenDO dbTokenDO = new AccessTokenDO();
910+
dbTokenDO.setScope(null);
911+
when(mockAccessTokenDAO.getAccessToken("testAccessToken",
912+
true)).thenReturn(dbTokenDO);
913+
914+
mockedOAuthUtil.when(() -> OAuthUtil.clearOAuthCacheUsingPersistedScopes(
915+
anyString(), any(AccessTokenDO.class), any(OAuthRevocationRequestDTO.class)))
916+
.thenCallRealMethod();
917+
918+
AccessTokenDO accessTokenDO = new AccessTokenDO();
919+
accessTokenDO.setAccessToken("testAccessToken");
920+
OAuthRevocationRequestDTO revokeRequestDTO = mock(OAuthRevocationRequestDTO.class);
921+
922+
OAuthUtil.clearOAuthCacheUsingPersistedScopes("tokenBindingRef",
923+
accessTokenDO, revokeRequestDTO);
924+
925+
mockedOAuthUtil.verify(() -> OAuthUtil.clearOAuthCache(
926+
anyString(), any(AuthenticatedUser.class), anyString(), anyString()), never());
927+
mockedOAuthUtil.verify(() -> OAuthUtil.clearOAuthCache(
928+
anyString(), any(AuthenticatedUser.class), anyString()), never());
929+
}
930+
}
931+
932+
@Test
933+
public void testClearOAuthCacheUsingPersistedScopes_WhenDbTokenScopeIsEmpty() throws Exception {
934+
935+
try (MockedStatic<OAuthServerConfiguration> oAuthServerConfigStatic =
936+
mockStatic(OAuthServerConfiguration.class);
937+
MockedStatic<OAuthUtil> mockedOAuthUtil = mockStatic(OAuthUtil.class)) {
938+
OAuthServerConfiguration mockServerConfig = mock(OAuthServerConfiguration.class);
939+
oAuthServerConfigStatic.when(OAuthServerConfiguration::getInstance).thenReturn(mockServerConfig);
940+
when(mockServerConfig.getAllowedScopes()).thenReturn(Collections.singletonList("openid"));
941+
942+
OAuthTokenPersistenceFactory mockFactory = mock(OAuthTokenPersistenceFactory.class);
943+
oAuthTokenPersistenceFactory.when(OAuthTokenPersistenceFactory::getInstance).thenReturn(mockFactory);
944+
AccessTokenDAO mockAccessTokenDAO = mock(AccessTokenDAO.class);
945+
when(mockFactory.getAccessTokenDAO()).thenReturn(mockAccessTokenDAO);
946+
947+
AccessTokenDO dbTokenDO = new AccessTokenDO();
948+
dbTokenDO.setScope(new String[0]);
949+
when(mockAccessTokenDAO.getAccessToken("testAccessToken",
950+
true)).thenReturn(dbTokenDO);
951+
952+
mockedOAuthUtil.when(() -> OAuthUtil.clearOAuthCacheUsingPersistedScopes(
953+
anyString(), any(AccessTokenDO.class), any(OAuthRevocationRequestDTO.class)))
954+
.thenCallRealMethod();
955+
956+
AccessTokenDO accessTokenDO = new AccessTokenDO();
957+
accessTokenDO.setAccessToken("testAccessToken");
958+
OAuthRevocationRequestDTO revokeRequestDTO = mock(OAuthRevocationRequestDTO.class);
959+
960+
OAuthUtil.clearOAuthCacheUsingPersistedScopes("tokenBindingRef",
961+
accessTokenDO, revokeRequestDTO);
962+
963+
mockedOAuthUtil.verify(() -> OAuthUtil.clearOAuthCache(
964+
anyString(), any(AuthenticatedUser.class), anyString(), anyString()), never());
965+
mockedOAuthUtil.verify(() -> OAuthUtil.clearOAuthCache(
966+
anyString(), any(AuthenticatedUser.class), anyString()), never());
967+
}
968+
}
969+
970+
@Test
971+
public void testClearOAuthCacheUsingPersistedScopes_ClearsCacheWithPersistedScopes() throws Exception {
972+
973+
String tokenBindingReference = "tokenBindingRef";
974+
String accessToken = "testAccessToken";
975+
String consumerKey = "testConsumerKey";
976+
String[] dbScopes = {"scope1", "scope2"};
977+
String dbScopeString = "scope1 scope2";
978+
979+
try (MockedStatic<OAuthServerConfiguration> oAuthServerConfigStatic =
980+
mockStatic(OAuthServerConfiguration.class);
981+
MockedStatic<OAuthUtil> mockedOAuthUtil = mockStatic(OAuthUtil.class)) {
982+
OAuthServerConfiguration mockServerConfig = mock(OAuthServerConfiguration.class);
983+
oAuthServerConfigStatic.when(OAuthServerConfiguration::getInstance).thenReturn(mockServerConfig);
984+
when(mockServerConfig.getAllowedScopes()).thenReturn(Collections.singletonList("scope1"));
985+
986+
OAuthTokenPersistenceFactory mockFactory = mock(OAuthTokenPersistenceFactory.class);
987+
oAuthTokenPersistenceFactory.when(OAuthTokenPersistenceFactory::getInstance).thenReturn(mockFactory);
988+
AccessTokenDAO mockAccessTokenDAO = mock(AccessTokenDAO.class);
989+
when(mockFactory.getAccessTokenDAO()).thenReturn(mockAccessTokenDAO);
990+
991+
AccessTokenDO dbTokenDO = new AccessTokenDO();
992+
dbTokenDO.setScope(dbScopes);
993+
when(mockAccessTokenDAO.getAccessToken(accessToken, true)).thenReturn(dbTokenDO);
994+
995+
oAuth2Util.when(() -> OAuth2Util.buildScopeString(dbScopes)).thenReturn(dbScopeString);
996+
997+
mockedOAuthUtil.when(() -> OAuthUtil.clearOAuthCacheUsingPersistedScopes(
998+
anyString(), any(AccessTokenDO.class), any(OAuthRevocationRequestDTO.class)))
999+
.thenCallRealMethod();
1000+
1001+
AuthenticatedUser authzUser = mock(AuthenticatedUser.class);
1002+
AccessTokenDO accessTokenDO = new AccessTokenDO();
1003+
accessTokenDO.setAccessToken(accessToken);
1004+
accessTokenDO.setAuthzUser(authzUser);
1005+
1006+
OAuthRevocationRequestDTO revokeRequestDTO = mock(OAuthRevocationRequestDTO.class);
1007+
when(revokeRequestDTO.getConsumerKey()).thenReturn(consumerKey);
1008+
1009+
OAuthUtil.clearOAuthCacheUsingPersistedScopes(tokenBindingReference, accessTokenDO, revokeRequestDTO);
1010+
1011+
mockedOAuthUtil.verify(() -> OAuthUtil.clearOAuthCache(
1012+
eq(consumerKey), eq(authzUser), eq(dbScopeString), eq(tokenBindingReference)), times(1));
1013+
mockedOAuthUtil.verify(() -> OAuthUtil.clearOAuthCache(
1014+
eq(consumerKey), eq(authzUser), eq(dbScopeString)), times(1));
1015+
}
1016+
}
1017+
1018+
@Test
1019+
public void testClearOAuthCacheUsingPersistedScopes_WhenDaoThrowsException() throws Exception {
1020+
1021+
try (MockedStatic<OAuthServerConfiguration> oAuthServerConfigStatic =
1022+
mockStatic(OAuthServerConfiguration.class);
1023+
MockedStatic<OAuthUtil> mockedOAuthUtil = mockStatic(OAuthUtil.class)) {
1024+
OAuthServerConfiguration mockServerConfig = mock(OAuthServerConfiguration.class);
1025+
oAuthServerConfigStatic.when(OAuthServerConfiguration::getInstance).thenReturn(mockServerConfig);
1026+
when(mockServerConfig.getAllowedScopes()).thenReturn(Collections.singletonList("openid"));
1027+
1028+
OAuthTokenPersistenceFactory mockFactory = mock(OAuthTokenPersistenceFactory.class);
1029+
oAuthTokenPersistenceFactory.when(OAuthTokenPersistenceFactory::getInstance).thenReturn(mockFactory);
1030+
AccessTokenDAO mockAccessTokenDAO = mock(AccessTokenDAO.class);
1031+
when(mockFactory.getAccessTokenDAO()).thenReturn(mockAccessTokenDAO);
1032+
when(mockAccessTokenDAO.getAccessToken(anyString(), anyBoolean()))
1033+
.thenThrow(new IdentityOAuth2Exception("DAO error"));
1034+
1035+
mockedOAuthUtil.when(() -> OAuthUtil.clearOAuthCacheUsingPersistedScopes(
1036+
anyString(), any(AccessTokenDO.class), any(OAuthRevocationRequestDTO.class)))
1037+
.thenCallRealMethod();
1038+
1039+
AccessTokenDO accessTokenDO = new AccessTokenDO();
1040+
accessTokenDO.setAccessToken("testAccessToken");
1041+
OAuthRevocationRequestDTO revokeRequestDTO = mock(OAuthRevocationRequestDTO.class);
1042+
when(revokeRequestDTO.getConsumerKey()).thenReturn("testConsumerKey");
1043+
1044+
// Exception should be caught and logged internally, not propagated.
1045+
OAuthUtil.clearOAuthCacheUsingPersistedScopes("tokenBindingRef",
1046+
accessTokenDO, revokeRequestDTO);
1047+
1048+
mockedOAuthUtil.verify(() -> OAuthUtil.clearOAuthCache(
1049+
anyString(), any(AuthenticatedUser.class), anyString(), anyString()), never());
1050+
}
1051+
}
1052+
8111053
private OAuthCache getOAuthCache(OAuthCacheKey oAuthCacheKey) {
8121054

8131055
// Add some value to OAuthCache.

0 commit comments

Comments
 (0)