From 7709b9fd333a698ef5c1b7915808c995358fef4a Mon Sep 17 00:00:00 2001 From: jbergner Date: Mon, 24 Aug 2026 22:54:02 +0200 Subject: [PATCH] Bugfix autologoff --- CHANGELOG.md | 6 ++ docs/ACCESS-AUTH.md | 4 +- .../main/resources/js/sessionguard-access.js | 82 +++++++++++++++---- 3 files changed, 76 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f23baa..7f46eac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v0.5.2 Guacamole logout/recovery follow-up + +- Fixed an over-aggressive Guacamole browser helper which treated every Guacamole `loggedOut` state as an explicit user logout. +- Explicit clicks on Guacamole logout actions still perform full SessionGuard/Pocket ID RP-initiated logout. +- Guacamole-only token loss (for example after a worker restart/failover) now preserves the SessionGuard/Pocket ID session and re-enters Guacamole through header authentication. + ## v0.5.2 PKCE / logout hotfix - Added PKCE S256 (`code_challenge` / `code_verifier`) to both SessionGuard OIDC authorization-code flows. diff --git a/docs/ACCESS-AUTH.md b/docs/ACCESS-AUTH.md index bdb21f8..4a96ea5 100644 --- a/docs/ACCESS-AUTH.md +++ b/docs/ACCESS-AUTH.md @@ -124,7 +124,7 @@ Use the SessionGuard-built Guacamole image from 0.5.0 or later. The extension co - every 30 seconds it checks `/_sessionguard/auth/status`; - if the server-side SessionGuard access session has been revoked/expired, it navigates away from Guacamole, closing browser tunnels/WebSockets; -- after Guacamole's own logout reaches its logged-out state, it redirects to `/_sessionguard/auth/logout`, which also ends the SessionGuard/PocketID session. +- an explicit click on a Guacamole logout action redirects to `/_sessionguard/auth/logout`, which also ends the SessionGuard/PocketID session; generic Guacamole token loss/failover does not trigger IdP logout. ## Migration from traefik-forward-auth @@ -143,4 +143,4 @@ Do not run both auth middlewares on the Guacamole router. A safe migration is: - Valid access session: Guacamole receives exactly the PocketID username through `X-Guacamole-User`. - Session older than `session_hours`: denied even if Guacamole still holds an old auth token. - PocketID back-channel logout: corresponding SessionGuard sessions are revoked immediately; the browser-side poll closes an already-open Guacamole page within about 30 seconds. -- Guacamole logout button: Guacamole destroys its own token, then the SessionGuard helper performs full OIDC logout. +- Guacamole logout button: an explicit user click performs full SessionGuard/PocketID OIDC logout. If Guacamole merely loses its local token (for example after worker failover/restart), SessionGuard keeps the upstream access session and re-enters Guacamole through header auth. diff --git a/guacamole-extension/src/main/resources/js/sessionguard-access.js b/guacamole-extension/src/main/resources/js/sessionguard-access.js index e53e68d..6aef9c8 100644 --- a/guacamole-extension/src/main/resources/js/sessionguard-access.js +++ b/guacamole-extension/src/main/resources/js/sessionguard-access.js @@ -31,44 +31,76 @@ window.location.replace(url); } - function checkAccessSession() { - if (redirecting || !window.fetch) { - return; + function getAccessSessionStatus() { + if (!window.fetch) { + return Promise.reject(new Error('fetch unavailable')); } - window.fetch(AUTH_BASE + '/status', { + return window.fetch(AUTH_BASE + '/status', { method: 'GET', credentials: 'same-origin', cache: 'no-store', headers: { 'Accept': 'application/json' } - }).then(function (response) { + }); + } + + function checkAccessSession() { + if (redirecting) { + return; + } + + getAccessSessionStatus().then(function (response) { if (response.status === 401 || response.status === 403) { - // Navigating away also closes Guacamole WebSocket/tunnel - // connections, so a revoked PocketID/SessionGuard access - // session cannot keep an already-open browser client alive. + // The SessionGuard/PocketID access session itself is no longer + // valid. Start a fresh OIDC flow, but do not treat this as an + // explicit logout request. redirect(loginURL()); } }).catch(function () { // A transient auth-status outage must not destroy an active RDP - // session. Traefik still fail-closes all new HTTP requests through - // ForwardAuth; retry this browser-side check on the next interval. + // session. ForwardAuth still fail-closes new requests and this + // browser-side check will retry on the next interval. }); } - function watchForGuacamoleLogout() { + function recoverFromGuacamoleLogout() { + if (redirecting) { + return; + } + + // Guacamole authentication tokens live in the selected webapp process. + // A worker failover/restart can therefore cause Guacamole to enter its + // logged-out state even though the upstream SessionGuard/PocketID + // browser session is still valid. Never turn that condition into a + // full IdP logout. Re-enter Guacamole and let header auth mint a fresh + // Guacamole token instead. + getAccessSessionStatus().then(function (response) { + if (response.ok) { + redirect(safeReturnURL()); + } + else if (response.status === 401 || response.status === 403) { + redirect(loginURL()); + } + }).catch(function () { + // Keep Guacamole's normal logged-out UI visible during a transient + // SessionGuard outage instead of forcing any logout/login loop. + }); + } + + function watchForGuacamoleLoggedOutState() { function loggedOutModalPresent() { return document.querySelector('.logged-out-modal') !== null; } if (loggedOutModalPresent()) { - redirect(logoutURL()); + recoverFromGuacamoleLogout(); return; } var observer = new MutationObserver(function () { if (loggedOutModalPresent()) { observer.disconnect(); - redirect(logoutURL()); + recoverFromGuacamoleLogout(); } }); @@ -78,8 +110,30 @@ }); } + function watchForExplicitLogout() { + // Guacamole assigns the CSS class "logout" to its explicit logout + // actions. Observe the user's click rather than the generic + // .logged-out-modal state: the latter can also result from worker + // failover, token loss, expiry or other non-user-initiated events. + document.addEventListener('click', function (event) { + var element = event.target; + if (!element || typeof element.closest !== 'function') { + return; + } + + if (element.closest('.logout')) { + // Let Guacamole's own click handler invalidate its local auth + // token first, then perform SessionGuard/PocketID logout. + window.setTimeout(function () { + redirect(logoutURL()); + }, 150); + } + }, false); + } + function start() { - watchForGuacamoleLogout(); + watchForExplicitLogout(); + watchForGuacamoleLoggedOutState(); checkAccessSession(); window.setInterval(checkAccessSession, STATUS_INTERVAL_MS); }