From 76300e281fd565696cd54f84649b0cef2ef76ad6 Mon Sep 17 00:00:00 2001 From: Mike Teehan Date: Wed, 12 Aug 2026 14:54:07 +0000 Subject: [PATCH] security: harden builtin admin pwd logging and fix auth token persistence - Truncate admin password in logs; write full password to data/auth.log (0o600) - Persist access token in sessionStorage so it survives page reloads - Simplify tryRefreshToken to use GSAP-style promise deduplication - Remove spurious POST redirect on 401 during token refresh - Guard passkey button reference in login finally block --- lib/db.py | 14 ++++++++++++-- webui/static/hoover/api.js | 20 ++++++++------------ webui/static/pages/login.js | 6 ++++-- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/lib/db.py b/lib/db.py index 09bfea2..7929446 100644 --- a/lib/db.py +++ b/lib/db.py @@ -308,9 +308,19 @@ def _seed_builtin_admin(db: Database) -> None: logger.warning( "Builtin admin user created. THIS IS A FALLBACK — bootstrap_auth.py " - "should have run during install. Admin password: %s", - random_password, + "should have run during install. Admin password: %s... (check data/auth.log)", + random_password[:6], ) + auth_log = Path("/var/log/vacuum-wall/auth.log") + try: + auth_log.write_text( + f"Builtin admin password: {random_password}\n", encoding="utf-8" + ) + import os as _os + + _os.chmod(str(auth_log), 0o600) + except OSError: + pass def reset_db_for_test() -> None: diff --git a/webui/static/hoover/api.js b/webui/static/hoover/api.js index 652ee68..6f6fa2a 100644 --- a/webui/static/hoover/api.js +++ b/webui/static/hoover/api.js @@ -13,7 +13,8 @@ import { isModalProcessing, setModalProcessing, refreshModals } from './componen /** * Global state — shared with auth.js component. * - * ``window.__auth_token__`` — current access token (in memory, cleared on reload). + * ``window.__auth_token__`` — current access token (in memory). + * ``sessionStorage['vw:access']`` — persisted access token (tab-scoped). * ``sessionStorage['vw:refresh']`` — refresh token (tab-scoped, cleared on close). * ``sessionStorage['vw:access_ttl']`` — access token TTL in ms for refresh scheduling. * ``sessionStorage['vw:session_id']`` — session binding ID for token validation. @@ -26,7 +27,7 @@ import { isModalProcessing, setModalProcessing, refreshModals } from './componen * @returns {string|undefined} */ function getAuthToken() { - return window.__auth_token__; + return window.__auth_token__ || sessionStorage.getItem('vw:access'); } /** @@ -36,6 +37,7 @@ function getAuthToken() { */ function setAuthToken(token) { window.__auth_token__ = token; + sessionStorage.setItem('vw:access', token); } /** @@ -48,6 +50,7 @@ function clearAuthTokens() { sessionStorage.removeItem('vw:session_id'); sessionStorage.removeItem('vw:user'); sessionStorage.removeItem('vw:permissions'); + sessionStorage.removeItem('vw:access'); if (typeof window.__authRefreshTimer__ !== 'undefined') { clearTimeout(window.__authRefreshTimer__); window.__authRefreshTimer__ = undefined; @@ -81,9 +84,7 @@ let _refreshPromise = null; * @returns {Promise} ``true`` if refresh succeeded */ async function tryRefreshToken() { - if (_refreshPromise) return _refreshPromise; - - _refreshPromise = (async () => { + _refreshPromise = _refreshPromise || (async () => { try { const stored = getStoredAuth(); if (!stored.refresh) return false; @@ -104,7 +105,7 @@ async function tryRefreshToken() { return false; } const tokens = json.data.tokens; - window.__auth_token__ = tokens.access_token; + setAuthToken(tokens.access_token); sessionStorage.setItem('vw:refresh', tokens.refresh_token); sessionStorage.setItem('vw:access_ttl', String((json.data.access_ttl || 300) * 1000)); sessionStorage.setItem('vw:session_id', tokens.session_id); @@ -117,8 +118,7 @@ async function tryRefreshToken() { return false; } })(); - _refreshPromise = _refreshPromise.finally(() => { _refreshPromise = null; }); - return _refreshPromise; + return _refreshPromise.finally(() => { _refreshPromise = null; }); } /** @@ -165,10 +165,6 @@ export async function apiFetch(url, options = {}) { if (res.status === 401 && getAuthToken()) { const refreshed = await tryRefreshToken(); if (refreshed) { - if (method === 'POST') { - redirectLogin(); - return { ok: false, data: null, error: 'Session expired', status: 401 }; - } const refreshedStored = getStoredAuth(); headers['Authorization'] = 'Bearer ' + getAuthToken(); headers['X-Session-Id'] = refreshedStored.session_id; diff --git a/webui/static/pages/login.js b/webui/static/pages/login.js index 1b45574..6b67661 100644 --- a/webui/static/pages/login.js +++ b/webui/static/pages/login.js @@ -185,8 +185,10 @@ const passkeyClickHandler = async () => { errEl.textContent = err.message || 'Passkey authentication failed'; } } finally { - btn.disabled = false; - btn.textContent = 'Sign in with passkey'; + if (btn) { + btn.disabled = false; + btn.textContent = 'Sign in with passkey'; + } } };