From 4154d27941e4cc7ccb0452e6f43978897b5dc3d3 Mon Sep 17 00:00:00 2001 From: Martin Kanis Date: Fri, 8 Mar 2024 13:51:45 +0100 Subject: [PATCH] Invalidating offline token is not working from client sessions tab Closes #27275 Signed-off-by: Martin Kanis --- .../admin/client/resource/RealmResource.java | 3 +- .../cypress/e2e/sessions_test.spec.ts | 4 +-- .../pages/admin-ui/components/TablePage.ts | 18 ---------- .../admin-ui/src/sessions/SessionsTable.tsx | 33 +++++++++++++++++-- .../src/resources/realms.ts | 3 +- .../managers/AuthenticationManager.java | 3 +- .../resources/admin/RealmAdminResource.java | 17 +++++++--- .../testsuite/admin/PermissionsTest.java | 2 +- .../testsuite/admin/realm/RealmTest.java | 4 +-- .../crossdc/SessionExpirationCrossDCTest.java | 2 +- .../oauth/UserInfoEndpointCorsTest.java | 2 +- 11 files changed, 55 insertions(+), 36 deletions(-) diff --git a/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/RealmResource.java b/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/RealmResource.java index a60ce1b5cf..cfecc1fa17 100644 --- a/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/RealmResource.java +++ b/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/RealmResource.java @@ -17,6 +17,7 @@ package org.keycloak.admin.client.resource; +import jakarta.ws.rs.DefaultValue; import org.keycloak.representations.adapters.action.GlobalRequestResult; import org.keycloak.representations.idm.AdminEventRepresentation; import org.keycloak.representations.idm.ClientRepresentation; @@ -268,7 +269,7 @@ public interface RealmResource { @Path("sessions/{session}") @DELETE - void deleteSession(@PathParam("session") String sessionId); + void deleteSession(@PathParam("session") String sessionId, @DefaultValue("false") @QueryParam("isOffline") boolean offline); @Path("components") ComponentsResource components(); diff --git a/js/apps/admin-ui/cypress/e2e/sessions_test.spec.ts b/js/apps/admin-ui/cypress/e2e/sessions_test.spec.ts index a3efd75994..113523f2f2 100644 --- a/js/apps/admin-ui/cypress/e2e/sessions_test.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/sessions_test.spec.ts @@ -96,11 +96,11 @@ describe("Sessions test", () => { sidebarPage.waitForPageLoad(); // Now check that offline session exists (online one has been logged off above) - // and that it is not possible to sign it out + // and that it is possible to revoke it commonPage .tableUtils() .checkRowItemExists(username) - .assertRowItemActionDoesNotExist(username, "Sign out"); + .selectRowItemAction(username, "Revoke"); }); }); diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/components/TablePage.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/components/TablePage.ts index fcae54db78..9b42844ff9 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/components/TablePage.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/components/TablePage.ts @@ -53,24 +53,6 @@ export default class TablePage extends CommonElements { return this; } - assertRowItemActionDoesNotExist(itemName: string, actionItemName: string) { - cy.get( - (this.#tableInModal ? ".pf-c-modal-box.pf-m-md " : "") + - this.#tableRowItem, - ) - .contains(itemName) - .parentsUntil("tbody") - .then(($tbody) => { - if ($tbody.find(".pf-c-dropdown__toggle").length > 0) { - $tbody.find(".pf-c-dropdown__toggle").click(); - cy.get(this.dropdownMenuItem) - .contains(actionItemName) - .should("not.exist"); - } - }); - return this; - } - #getRowItemAction(itemName: string, actionItemName: string) { return cy .get( diff --git a/js/apps/admin-ui/src/sessions/SessionsTable.tsx b/js/apps/admin-ui/src/sessions/SessionsTable.tsx index 3eb6f594b6..b5328ba4b0 100644 --- a/js/apps/admin-ui/src/sessions/SessionsTable.tsx +++ b/js/apps/admin-ui/src/sessions/SessionsTable.tsx @@ -146,13 +146,32 @@ export default function SessionsTable({ }, }); + async function onClickRevoke( + event: MouseEvent, + rowIndex: number, + rowData: IRowData, + ) { + const session = rowData.data as UserSessionRepresentation; + await adminClient.realms.deleteSession({ + realm, + session: session.id!, + isOffline: true, + }); + + refresh(); + } + async function onClickSignOut( event: MouseEvent, rowIndex: number, rowData: IRowData, ) { const session = rowData.data as UserSessionRepresentation; - await adminClient.realms.deleteSession({ realm, session: session.id! }); + await adminClient.realms.deleteSession({ + realm, + session: session.id!, + isOffline: false, + }); if (session.userId === whoAmI.getUserId()) { await keycloak.logout({ redirectUri: "" }); @@ -185,8 +204,16 @@ export default function SessionsTable({ } columns={columns} actionResolver={(rowData: IRowData) => { - if (rowData.data.type === "OFFLINE") { - return []; + if ( + rowData.data.type === "Offline" || + rowData.data.type === "OFFLINE" + ) { + return [ + { + title: t("revoke"), + onClick: onClickRevoke, + } as Action, + ]; } return [ { diff --git a/js/libs/keycloak-admin-client/src/resources/realms.ts b/js/libs/keycloak-admin-client/src/resources/realms.ts index d760cfbf8d..49b10dc46e 100644 --- a/js/libs/keycloak-admin-client/src/resources/realms.ts +++ b/js/libs/keycloak-admin-client/src/resources/realms.ts @@ -311,12 +311,13 @@ export class Realms extends Resource { }); public deleteSession = this.makeRequest< - { realm: string; session: string }, + { realm: string; session: string; isOffline: boolean }, void >({ method: "DELETE", path: "/{realm}/sessions/{session}", urlParamKeys: ["realm", "session"], + queryParamKeys: ["isOffline"], }); public pushRevocation = this.makeRequest< diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index 409c74b5ef..9dce10cee1 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -251,7 +251,8 @@ public class AuthenticationManager { UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers, boolean logoutBroker) { - return backchannelLogout(session, realm, userSession, uriInfo, connection, headers, logoutBroker, false); + + return backchannelLogout(session, realm, userSession, uriInfo, connection, headers, logoutBroker, userSession == null ? false : userSession.isOffline()); } /** diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index 2abdfa896d..5cc70a3ecf 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -30,6 +30,8 @@ import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; +import jakarta.enterprise.inject.Default; +import jakarta.ws.rs.DefaultValue; import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.extensions.Extension; import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; @@ -625,14 +627,19 @@ public class RealmAdminResource { @DELETE @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) @Operation( summary = "Remove a specific user session.", description = "Any client that has an admin url will also be told to invalidate this particular session.") - public void deleteSession(@PathParam("session") String sessionId) { + public void deleteSession(@PathParam("session") String sessionId, @DefaultValue("false") @QueryParam("isOffline") boolean offline) { auth.users().requireManage(); - UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId); - if (userSession == null) throw new NotFoundException("Sesssion not found"); - AuthenticationManager.backchannelLogout(session, realm, userSession, session.getContext().getUri(), connection, headers, true); - adminEvent.operation(OperationType.DELETE).resource(ResourceType.USER_SESSION).resourcePath(session.getContext().getUri()).success(); + UserSessionModel userSession = offline ? session.sessions().getOfflineUserSession(realm, sessionId) : session.sessions().getUserSession(realm, sessionId); + if (userSession == null) { + throw new NotFoundException("Sesssion not found"); + } + AuthenticationManager.backchannelLogout(session, realm, userSession, session.getContext().getUri(), connection, headers, true); + + Map eventRep = new HashMap<>(); + eventRep.put("offline", offline); + adminEvent.operation(OperationType.DELETE).resource(ResourceType.USER_SESSION).resourcePath(session.getContext().getUri()).representation(eventRep).success(); } /** diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java index 905a1841cc..3830e85378 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java @@ -336,7 +336,7 @@ public class PermissionsTest extends AbstractKeycloakTest { }, Resource.REALM, true); invoke(new Invocation() { public void invoke(RealmResource realm) { - realm.deleteSession("nosuch"); + realm.deleteSession("nosuch", false); } }, Resource.USER, true); invoke(new Invocation() { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java index 509f1356f8..09f096a19e 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java @@ -947,10 +947,10 @@ public class RealmTest extends AbstractAdminTest { EventRepresentation event = events.poll(); assertNotNull(event); - realm.deleteSession(event.getSessionId()); + realm.deleteSession(event.getSessionId(), false); assertAdminEvents.assertEvent(realmId, OperationType.DELETE, AdminEventPaths.deleteSessionPath(event.getSessionId()), ResourceType.USER_SESSION); try { - realm.deleteSession(event.getSessionId()); + realm.deleteSession(event.getSessionId(), false); fail("Expected 404"); } catch (NotFoundException e) { // Expected diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java index e9f8e29d49..316f207923 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java @@ -425,7 +425,7 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest { // Logout single session of user first UserResource user = ApiUtil.findUserByUsernameId(getAdminClient().realm(REALM_NAME), "login-test"); UserSessionRepresentation userSession = user.getUserSessions().get(0); - getAdminClient().realm(REALM_NAME).deleteSession(userSession.getId()); + getAdminClient().realm(REALM_NAME).deleteSession(userSession.getId(), false); // Just one session expired. assertStatisticsExpected("After logout single session", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/UserInfoEndpointCorsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/UserInfoEndpointCorsTest.java index 9ff1a5cfa7..13a593b16a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/UserInfoEndpointCorsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/UserInfoEndpointCorsTest.java @@ -123,7 +123,7 @@ public class UserInfoEndpointCorsTest extends AbstractKeycloakTest { // remove the session in keycloak AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken()); - adminClient.realm("test").deleteSession(accessToken.getSessionState()); + adminClient.realm("test").deleteSession(accessToken.getSessionState(), false); try (ResteasyClient resteasyClient = AdminClientUtil.createResteasyClient()) { WebTarget userInfoTarget = UserInfoClientUtil.getUserInfoWebTarget(resteasyClient);