Skip to content

Commit

Permalink
Invalidating offline token is not working from client sessions tab
Browse files Browse the repository at this point in the history
Closes #27275

Signed-off-by: Martin Kanis <mkanis@redhat.com>
  • Loading branch information
martin-kanis authored and pedroigor committed Mar 21, 2024
1 parent 0542554 commit 4154d27
Show file tree
Hide file tree
Showing 11 changed files with 54 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -268,7 +269,7 @@ Response testLDAPConnection(@FormParam("action") String action, @FormParam("conn

@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();
Expand Down
4 changes: 2 additions & 2 deletions js/apps/admin-ui/cypress/e2e/sessions_test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
33 changes: 30 additions & 3 deletions js/apps/admin-ui/src/sessions/SessionsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: "" });
Expand Down Expand Up @@ -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<UserSessionRepresentation>,
];
}
return [
{
Expand Down
3 changes: 2 additions & 1 deletion js/libs/keycloak-admin-client/src/resources/realms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,8 @@ public static BackchannelLogoutResponse backchannelLogout(KeycloakSession sessio
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());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
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;
Expand Down Expand Up @@ -625,14 +627,19 @@ public GlobalRequestResult logoutAll() {
@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");
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);
adminEvent.operation(OperationType.DELETE).resource(ResourceType.USER_SESSION).resourcePath(session.getContext().getUri()).success();

Map<String, Object> eventRep = new HashMap<>();
eventRep.put("offline", offline);
adminEvent.operation(OperationType.DELETE).resource(ResourceType.USER_SESSION).resourcePath(session.getContext().getUri()).representation(eventRep).success();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ public void invoke(RealmResource realm) {
}, Resource.REALM, true);
invoke(new Invocation() {
public void invoke(RealmResource realm) {
realm.deleteSession("nosuch");
realm.deleteSession("nosuch", false);
}
}, Resource.USER, true);
invoke(new Invocation() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -947,10 +947,10 @@ public void deleteSession() {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ public void testLogoutUser(
// 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ public void userInfoCorsInvalidSession() throws Exception {

// 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);
Expand Down

0 comments on commit 4154d27

Please sign in to comment.