Remove online session for offline access in direct access grants and client credentials

Closes #32650

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc 2024-10-16 09:44:09 +02:00 committed by Marek Posolda
parent 96b6cb4506
commit 13655007a6
6 changed files with 43 additions and 24 deletions

View file

@ -84,14 +84,6 @@ describe("Sessions test", () => {
it("check offline token", () => {
sidebarPage.waitForPageLoad();
listingPage.searchItem(clientId, false);
sidebarPage.waitForPageLoad();
// Log out the associated online session of the user
commonPage
.tableUtils()
.checkRowItemExists(username)
.selectRowItemAction(username, "Sign out");
listingPage.searchItem(clientId, false);
sidebarPage.waitForPageLoad();

View file

@ -145,6 +145,10 @@ public class ClientCredentialsGrantType extends OAuth2GrantTypeBase {
// Make refresh token generation optional, see KEYCLOAK-9551
if (useRefreshToken) {
responseBuilder = responseBuilder.generateRefreshToken();
if (TokenUtil.TOKEN_TYPE_OFFLINE.equals(responseBuilder.getRefreshToken().getType())) {
// for client credentials the online session can be removed
session.sessions().removeUserSession(realm, userSession);
}
} else {
responseBuilder.getAccessToken().setSessionId(null);
}

View file

@ -138,6 +138,10 @@ public class ResourceOwnerPasswordCredentialsGrantType extends OAuth2GrantTypeBa
boolean useRefreshToken = clientConfig.isUseRefreshToken();
if (useRefreshToken) {
responseBuilder.generateRefreshToken();
if (TokenUtil.TOKEN_TYPE_OFFLINE.equals(responseBuilder.getRefreshToken().getType())) {
// for direct access grants the online session can be removed
session.sessions().removeUserSession(realm, userSession);
}
}
String scopeParam = clientSessionCtx.getClientSession().getNote(OAuth2Constants.SCOPE);

View file

@ -1181,8 +1181,8 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
Map<String, ClientRepresentation> apps = applications.stream().collect(Collectors.toMap(x -> x.getClientId(), x -> x));
assertThat(apps.keySet(), containsInAnyOrder("offline-client", "offline-client-without-base-url", "always-display-client", "direct-grant"));
assertClientRep(apps.get("offline-client"), "Offline Client", null, false, true, true, null, offlineClientAppUri);
assertClientRep(apps.get("offline-client-without-base-url"), "Offline Client Without Base URL", null, false, true, true, null, null);
assertClientRep(apps.get("offline-client"), "Offline Client", null, false, false, true, null, offlineClientAppUri);
assertClientRep(apps.get("offline-client-without-base-url"), "Offline Client Without Base URL", null, false, false, true, null, null);
}
@Test
@ -1709,9 +1709,9 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
assertFalse(applications.isEmpty());
Map<String, ClientRepresentation> apps = applications.stream().collect(Collectors.toMap(x -> x.getClientId(), x -> x));
assertThat(apps.keySet(), containsInAnyOrder("offline-client", "always-display-client", "direct-grant"));
assertThat(apps.keySet(), containsInAnyOrder("always-display-client", "direct-grant"));
assertClientRep(apps.get("offline-client"), "Offline Client", null, false, true, false, null, offlineClientAppUri);
assertNull(apps.get("offline-client"));
}
@Test

View file

@ -204,7 +204,7 @@ public class ClientStorageTest extends AbstractTestRealmKeycloakTest {
boolean directTested = false;
for (Map<String, String> entry : list) {
if (entry.get("clientId").equals("hardcoded-client")) {
Assert.assertEquals("4", entry.get("active"));
Assert.assertEquals("2", entry.get("active"));
Assert.assertEquals("2", entry.get("offline"));
hardTested = true;
} else if (entry.get("clientId").equals("test-app")) {
@ -212,7 +212,7 @@ public class ClientStorageTest extends AbstractTestRealmKeycloakTest {
Assert.assertEquals("0", entry.get("offline"));
testAppTested = true;
} else if (entry.get("clientId").equals("direct-grant")) {
Assert.assertEquals("3", entry.get("active"));
Assert.assertEquals("1", entry.get("active"));
Assert.assertEquals("2", entry.get("offline"));
directTested = true;
}
@ -225,13 +225,13 @@ public class ClientStorageTest extends AbstractTestRealmKeycloakTest {
ClientModel hardcoded = realm.getClientByClientId("hardcoded-client");
long activeUserSessions = session.sessions().getActiveUserSessions(realm, hardcoded);
long offlineSessionsCount = session.sessions().getOfflineSessionsCount(realm, hardcoded);
Assert.assertEquals(4, activeUserSessions);
Assert.assertEquals(2, activeUserSessions);
Assert.assertEquals(2, offlineSessionsCount);
ClientModel direct = realm.getClientByClientId("direct-grant");
activeUserSessions = session.sessions().getActiveUserSessions(realm, direct);
offlineSessionsCount = session.sessions().getOfflineSessionsCount(realm, direct);
Assert.assertEquals(3, activeUserSessions);
Assert.assertEquals(1, activeUserSessions);
Assert.assertEquals(2, offlineSessionsCount);
});
}

View file

@ -333,6 +333,15 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
return newRefreshToken;
}
private void checkNumberOfSessions(String userId, String clientId, String sessionId, int onlineSessions, int offlineSessions) {
RealmResource realm = adminClient.realm("test");
String clientUuid = ApiUtil.findClientByClientId(realm, clientId).toRepresentation().getId();
Assert.assertEquals(onlineSessions, realm.users().get(userId).getUserSessions()
.stream().filter(s -> sessionId.equals(s.getId())).count());
Assert.assertEquals(offlineSessions, realm.users().get(userId).getOfflineSessions(clientUuid)
.stream().filter(s -> sessionId.equals(s.getId())).count());
}
@Test
public void offlineTokenDirectGrantFlow() throws Exception {
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
@ -346,7 +355,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
events.expectLogin()
.client("offline-client")
.user(userId)
.session(token.getSessionState())
.session(token.getSessionId())
.detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.TOKEN_ID, token.getId())
.detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
@ -360,10 +369,14 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
Assert.assertNull(offlineToken.getExp());
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId);
// check only the offline session is created
checkNumberOfSessions(userId, "offline-client", offlineToken.getSessionId(), 0, 1);
// refresh token
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionId(), userId);
// Assert same token can be refreshed again
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId);
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionId(), userId);
}
@Test
@ -434,7 +447,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
events.expectClientLogin()
.client("offline-client")
.user(serviceAccountUserId)
.session(token.getSessionState())
.session(token.getSessionId())
.detail(Details.TOKEN_ID, token.getId())
.detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
@ -444,7 +457,10 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
Assert.assertNull(offlineToken.getExp());
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId);
// check only the offline session is created
checkNumberOfSessions(serviceAccountUserId, "offline-client", offlineToken.getSessionId(), 0, 1);
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionId(), serviceAccountUserId);
// Now retrieve another offline token and verify that previous offline token is still valid
tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
@ -456,16 +472,19 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
events.expectClientLogin()
.client("offline-client")
.user(serviceAccountUserId)
.session(token2.getSessionState())
.session(token2.getSessionId())
.detail(Details.TOKEN_ID, token2.getId())
.detail(Details.REFRESH_TOKEN_ID, offlineToken2.getId())
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
.detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client")
.assertEvent();
// check only the offline session is created
checkNumberOfSessions(serviceAccountUserId, "offline-client", offlineToken2.getSessionId(), 0, 1);
// Refresh with both offline tokens is fine
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId);
testRefreshWithOfflineToken(token2, offlineToken2, offlineTokenString2, token2.getSessionState(), serviceAccountUserId);
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionId(), serviceAccountUserId);
testRefreshWithOfflineToken(token2, offlineToken2, offlineTokenString2, token2.getSessionId(), serviceAccountUserId);
}
@Test