Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Invalidating offline token is not working from client sessions tab #28129

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
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
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
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
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
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
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
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
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
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
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