diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java index c02d6a424c2..2571af1b0a9 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java @@ -252,16 +252,16 @@ public class TokenRevocationEndpoint { if (userSession != null) { new UserSessionManager(session).removeClientFromOfflineUserSession(realm, userSession, client, user); } - } else { - UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionId()); - if (userSession != null) { - AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); - if (clientSession != null) { - TokenManager.dettachClientSession(clientSession); - // TODO: Might need optimization to prevent loading client sessions from cache in getAuthenticatedClientSessions() - if (userSession.getAuthenticatedClientSessions().isEmpty()) { - session.sessions().removeUserSession(realm, userSession); - } + } + // Always remove "online" session as well if exists to make sure that issued access-tokens are revoked as well + UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionId()); + if (userSession != null) { + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); + if (clientSession != null) { + TokenManager.dettachClientSession(clientSession); + // TODO: Might need optimization to prevent loading client sessions from cache in getAuthenticatedClientSessions() + if (userSession.getAuthenticatedClientSessions().isEmpty()) { + session.sessions().removeUserSession(realm, userSession); } } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenRevocationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenRevocationTest.java index f6ff1946995..7e0cae99124 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenRevocationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenRevocationTest.java @@ -211,6 +211,26 @@ public class TokenRevocationTest extends AbstractKeycloakTest { isTokenDisabled(tokenResponse, "test-app"); } + @Test + public void testRevokeOfflineTokenWithOnlineSSOSession() throws Exception { + OAuthClient.AccessTokenResponse tokenResponse1 = login("test-app", "test-user@localhost", "password"); + + // Offline login of same client in same SSO session as previous login + oauth.scope(OAuth2Constants.OFFLINE_ACCESS); + OAuthClient.AccessTokenResponse tokenResponse2 = login("test-app", "test-user@localhost", "password"); + + // Session IDs of "offline" and online session are same for now. This may change in the future + Assert.assertEquals(tokenResponse1.getSessionState(), tokenResponse2.getSessionState()); + + isTokenEnabled(tokenResponse2, "test-app"); + + // Disable both offline and refresh + CloseableHttpResponse response = oauth.doTokenRevoke(tokenResponse2.getRefreshToken(), "refresh_token", "password"); + assertThat(response, Matchers.statusCodeIsHC(Status.OK)); + + isTokenDisabled(tokenResponse2, "test-app"); + } + @Test public void testTokenTypeHint() throws Exception { // different token_type_hint