Invalidating offline token is not working from client sessions tab

Closes #27275

Signed-off-by: Martin Kanis <mkanis@redhat.com>
This commit is contained in:
Martin Kanis 2024-03-08 13:51:45 +01:00 committed by Pedro Igor
parent 0542554984
commit 4154d27941
11 changed files with 55 additions and 36 deletions

View file

@ -17,6 +17,7 @@
package org.keycloak.admin.client.resource; package org.keycloak.admin.client.resource;
import jakarta.ws.rs.DefaultValue;
import org.keycloak.representations.adapters.action.GlobalRequestResult; import org.keycloak.representations.adapters.action.GlobalRequestResult;
import org.keycloak.representations.idm.AdminEventRepresentation; import org.keycloak.representations.idm.AdminEventRepresentation;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
@ -268,7 +269,7 @@ public interface RealmResource {
@Path("sessions/{session}") @Path("sessions/{session}")
@DELETE @DELETE
void deleteSession(@PathParam("session") String sessionId); void deleteSession(@PathParam("session") String sessionId, @DefaultValue("false") @QueryParam("isOffline") boolean offline);
@Path("components") @Path("components")
ComponentsResource components(); ComponentsResource components();

View file

@ -96,11 +96,11 @@ describe("Sessions test", () => {
sidebarPage.waitForPageLoad(); sidebarPage.waitForPageLoad();
// Now check that offline session exists (online one has been logged off above) // 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 commonPage
.tableUtils() .tableUtils()
.checkRowItemExists(username) .checkRowItemExists(username)
.assertRowItemActionDoesNotExist(username, "Sign out"); .selectRowItemAction(username, "Revoke");
}); });
}); });

View file

@ -53,24 +53,6 @@ export default class TablePage extends CommonElements {
return this; 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) { #getRowItemAction(itemName: string, actionItemName: string) {
return cy return cy
.get( .get(

View file

@ -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( async function onClickSignOut(
event: MouseEvent, event: MouseEvent,
rowIndex: number, rowIndex: number,
rowData: IRowData, rowData: IRowData,
) { ) {
const session = rowData.data as UserSessionRepresentation; 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()) { if (session.userId === whoAmI.getUserId()) {
await keycloak.logout({ redirectUri: "" }); await keycloak.logout({ redirectUri: "" });
@ -185,8 +204,16 @@ export default function SessionsTable({
} }
columns={columns} columns={columns}
actionResolver={(rowData: IRowData) => { actionResolver={(rowData: IRowData) => {
if (rowData.data.type === "OFFLINE") { if (
return []; rowData.data.type === "Offline" ||
rowData.data.type === "OFFLINE"
) {
return [
{
title: t("revoke"),
onClick: onClickRevoke,
} as Action<UserSessionRepresentation>,
];
} }
return [ return [
{ {

View file

@ -311,12 +311,13 @@ export class Realms extends Resource {
}); });
public deleteSession = this.makeRequest< public deleteSession = this.makeRequest<
{ realm: string; session: string }, { realm: string; session: string; isOffline: boolean },
void void
>({ >({
method: "DELETE", method: "DELETE",
path: "/{realm}/sessions/{session}", path: "/{realm}/sessions/{session}",
urlParamKeys: ["realm", "session"], urlParamKeys: ["realm", "session"],
queryParamKeys: ["isOffline"],
}); });
public pushRevocation = this.makeRequest< public pushRevocation = this.makeRequest<

View file

@ -251,7 +251,8 @@ public class AuthenticationManager {
UserSessionModel userSession, UriInfo uriInfo, UserSessionModel userSession, UriInfo uriInfo,
ClientConnection connection, HttpHeaders headers, ClientConnection connection, HttpHeaders headers,
boolean logoutBroker) { 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());
} }
/** /**

View file

@ -30,6 +30,8 @@ import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; 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.Operation;
import org.eclipse.microprofile.openapi.annotations.extensions.Extension; import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
@ -625,14 +627,19 @@ public class RealmAdminResource {
@DELETE @DELETE
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) @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.") @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(); auth.users().requireManage();
UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId); UserSessionModel userSession = offline ? session.sessions().getOfflineUserSession(realm, sessionId) : session.sessions().getUserSession(realm, sessionId);
if (userSession == null) throw new NotFoundException("Sesssion not found"); if (userSession == null) {
AuthenticationManager.backchannelLogout(session, realm, userSession, session.getContext().getUri(), connection, headers, true); throw new NotFoundException("Sesssion not found");
adminEvent.operation(OperationType.DELETE).resource(ResourceType.USER_SESSION).resourcePath(session.getContext().getUri()).success(); }
AuthenticationManager.backchannelLogout(session, realm, userSession, session.getContext().getUri(), connection, headers, true);
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();
} }
/** /**

View file

@ -336,7 +336,7 @@ public class PermissionsTest extends AbstractKeycloakTest {
}, Resource.REALM, true); }, Resource.REALM, true);
invoke(new Invocation() { invoke(new Invocation() {
public void invoke(RealmResource realm) { public void invoke(RealmResource realm) {
realm.deleteSession("nosuch"); realm.deleteSession("nosuch", false);
} }
}, Resource.USER, true); }, Resource.USER, true);
invoke(new Invocation() { invoke(new Invocation() {

View file

@ -947,10 +947,10 @@ public class RealmTest extends AbstractAdminTest {
EventRepresentation event = events.poll(); EventRepresentation event = events.poll();
assertNotNull(event); assertNotNull(event);
realm.deleteSession(event.getSessionId()); realm.deleteSession(event.getSessionId(), false);
assertAdminEvents.assertEvent(realmId, OperationType.DELETE, AdminEventPaths.deleteSessionPath(event.getSessionId()), ResourceType.USER_SESSION); assertAdminEvents.assertEvent(realmId, OperationType.DELETE, AdminEventPaths.deleteSessionPath(event.getSessionId()), ResourceType.USER_SESSION);
try { try {
realm.deleteSession(event.getSessionId()); realm.deleteSession(event.getSessionId(), false);
fail("Expected 404"); fail("Expected 404");
} catch (NotFoundException e) { } catch (NotFoundException e) {
// Expected // Expected

View file

@ -425,7 +425,7 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
// Logout single session of user first // Logout single session of user first
UserResource user = ApiUtil.findUserByUsernameId(getAdminClient().realm(REALM_NAME), "login-test"); UserResource user = ApiUtil.findUserByUsernameId(getAdminClient().realm(REALM_NAME), "login-test");
UserSessionRepresentation userSession = user.getUserSessions().get(0); 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. // Just one session expired.
assertStatisticsExpected("After logout single session", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, assertStatisticsExpected("After logout single session", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME,

View file

@ -123,7 +123,7 @@ public class UserInfoEndpointCorsTest extends AbstractKeycloakTest {
// remove the session in keycloak // remove the session in keycloak
AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken()); AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken());
adminClient.realm("test").deleteSession(accessToken.getSessionState()); adminClient.realm("test").deleteSession(accessToken.getSessionState(), false);
try (ResteasyClient resteasyClient = AdminClientUtil.createResteasyClient()) { try (ResteasyClient resteasyClient = AdminClientUtil.createResteasyClient()) {
WebTarget userInfoTarget = UserInfoClientUtil.getUserInfoWebTarget(resteasyClient); WebTarget userInfoTarget = UserInfoClientUtil.getUserInfoWebTarget(resteasyClient);