From ae9f25cb6e90090cc23527e8ada480215e0fa1ab Mon Sep 17 00:00:00 2001 From: Jeff <138259519+jdotc1@users.noreply.github.com> Date: Sun, 8 Nov 2026 21:46:58 -0500 Subject: [PATCH 1/3] test(security): add regression coverage before remediation --- src/utils/keycloak.test.ts | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/utils/keycloak.test.ts diff --git a/src/utils/keycloak.test.ts b/src/utils/keycloak.test.ts new file mode 100644 index 0000000..95d2a9c --- /dev/null +++ b/src/utils/keycloak.test.ts @@ -0,0 +1,36 @@ +import { getPostLogoutUrl, prepareKeycloakLogout } from "./keycloak"; + +describe("Keycloak logout", () => { + beforeEach(() => { + sessionStorage.clear(); + localStorage.clear(); + }); + + afterEach(() => { + sessionStorage.clear(); + localStorage.clear(); + }); + + it("captures the ID token hint before removing session and legacy tokens", () => { + sessionStorage.setItem("kc_id_token", "session-id-token"); + sessionStorage.setItem("kc_token", "session-access-token"); + sessionStorage.setItem("kc_refresh_token", "session-refresh-token"); + localStorage.setItem("kc_token", "legacy-access-token"); + localStorage.setItem("kc_refresh_token", "legacy-refresh-token"); + + const url = new URL(prepareKeycloakLogout()); + expect(url.searchParams.get("id_token_hint")).toBe("session-id-token"); + + for (const storage of [sessionStorage, localStorage]) { + expect(storage.getItem("kc_token")).toBeNull(); + expect(storage.getItem("kc_refresh_token")).toBeNull(); + expect(storage.getItem("kc_id_token")).toBeNull(); + } + }); + + it("uses a legacy localStorage ID token when sessionStorage has none", () => { + localStorage.setItem("kc_id_token", "legacy-id-token"); + const url = new URL(getPostLogoutUrl()); + expect(url.searchParams.get("id_token_hint")).toBe("legacy-id-token"); + }); +}); From b726e54715e2e3ee26c9a18ccf7c6fe782dca6e0 Mon Sep 17 00:00:00 2001 From: Jeff <138259519+jdotc1@users.noreply.github.com> Date: Sun, 8 Nov 2026 21:47:59 -0500 Subject: [PATCH 2/3] fix(auth): retain Keycloak logout hint and clear legacy tokens (#267) --- src/utils/keycloak.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/utils/keycloak.ts b/src/utils/keycloak.ts index 55db931..83010bc 100644 --- a/src/utils/keycloak.ts +++ b/src/utils/keycloak.ts @@ -53,7 +53,22 @@ export function getPostLogoutUrl(): string { const params = new URLSearchParams({ post_logout_redirect_uri: typeof window !== "undefined" ? window.location.origin : "", - id_token_hint: sessionStorage.getItem("kc_id_token") || "", + id_token_hint: + sessionStorage.getItem("kc_id_token") || + localStorage.getItem("kc_id_token") || + "", }); return `${config.url}/realms/${config.realm}/protocol/openid-connect/logout?${params.toString()}`; } + +// Preserve the logout hint before removing credentials from both storage locations. +// Some older sessions still have tokens in localStorage; never leave those usable. +export function prepareKeycloakLogout(): string { + const logoutUrl = getPostLogoutUrl(); + for (const storage of [sessionStorage, localStorage]) { + storage.removeItem("kc_token"); + storage.removeItem("kc_refresh_token"); + storage.removeItem("kc_id_token"); + } + return logoutUrl; +} From 674825190ea5d9351a4c1d7320e85c2622d58e8c Mon Sep 17 00:00:00 2001 From: Jeff <138259519+jdotc1@users.noreply.github.com> Date: Sun, 8 Nov 2026 21:48:00 -0500 Subject: [PATCH 3/3] fix(auth): retain Keycloak logout hint and clear legacy tokens (#267) --- src/components/ProfileButton.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/components/ProfileButton.tsx b/src/components/ProfileButton.tsx index bdaabcc..6bd2634 100644 --- a/src/components/ProfileButton.tsx +++ b/src/components/ProfileButton.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef } from "react"; import { Tooltip } from "@mui/material"; -import { getLoginUrl, getPostLogoutUrl } from "@/utils/keycloak"; +import { getLoginUrl, prepareKeycloakLogout } from "@/utils/keycloak"; const GUEST_AVATAR_PATH = "/assets/img/guest.png"; @@ -99,10 +99,7 @@ const ProfileButton: React.FC = () => { }; const handleLogout = () => { - // Clear tokens from sessionStorage - sessionStorage.removeItem("kc_token"); - sessionStorage.removeItem("kc_refresh_token"); - sessionStorage.removeItem("kc_id_token"); + const logoutUrl = prepareKeycloakLogout(); // Reset avatar setAvatarUrl(GUEST_AVATAR_PATH); @@ -114,7 +111,7 @@ const ProfileButton: React.FC = () => { // Notify other components about token changes window.dispatchEvent(new Event("tokensUpdated")); - window.location.href = getPostLogoutUrl(); + window.location.href = logoutUrl; }; const toggleDropdown = () => {