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
This commit is contained in:
@@ -308,9 +308,19 @@ def _seed_builtin_admin(db: Database) -> None:
|
|||||||
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Builtin admin user created. THIS IS A FALLBACK — bootstrap_auth.py "
|
"Builtin admin user created. THIS IS A FALLBACK — bootstrap_auth.py "
|
||||||
"should have run during install. Admin password: %s",
|
"should have run during install. Admin password: %s... (check data/auth.log)",
|
||||||
random_password,
|
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:
|
def reset_db_for_test() -> None:
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import { isModalProcessing, setModalProcessing, refreshModals } from './componen
|
|||||||
/**
|
/**
|
||||||
* Global state — shared with auth.js component.
|
* 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:refresh']`` — refresh token (tab-scoped, cleared on close).
|
||||||
* ``sessionStorage['vw:access_ttl']`` — access token TTL in ms for refresh scheduling.
|
* ``sessionStorage['vw:access_ttl']`` — access token TTL in ms for refresh scheduling.
|
||||||
* ``sessionStorage['vw:session_id']`` — session binding ID for token validation.
|
* ``sessionStorage['vw:session_id']`` — session binding ID for token validation.
|
||||||
@@ -26,7 +27,7 @@ import { isModalProcessing, setModalProcessing, refreshModals } from './componen
|
|||||||
* @returns {string|undefined}
|
* @returns {string|undefined}
|
||||||
*/
|
*/
|
||||||
function getAuthToken() {
|
function getAuthToken() {
|
||||||
return window.__auth_token__;
|
return window.__auth_token__ || sessionStorage.getItem('vw:access');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,6 +37,7 @@ function getAuthToken() {
|
|||||||
*/
|
*/
|
||||||
function setAuthToken(token) {
|
function setAuthToken(token) {
|
||||||
window.__auth_token__ = token;
|
window.__auth_token__ = token;
|
||||||
|
sessionStorage.setItem('vw:access', token);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,6 +50,7 @@ function clearAuthTokens() {
|
|||||||
sessionStorage.removeItem('vw:session_id');
|
sessionStorage.removeItem('vw:session_id');
|
||||||
sessionStorage.removeItem('vw:user');
|
sessionStorage.removeItem('vw:user');
|
||||||
sessionStorage.removeItem('vw:permissions');
|
sessionStorage.removeItem('vw:permissions');
|
||||||
|
sessionStorage.removeItem('vw:access');
|
||||||
if (typeof window.__authRefreshTimer__ !== 'undefined') {
|
if (typeof window.__authRefreshTimer__ !== 'undefined') {
|
||||||
clearTimeout(window.__authRefreshTimer__);
|
clearTimeout(window.__authRefreshTimer__);
|
||||||
window.__authRefreshTimer__ = undefined;
|
window.__authRefreshTimer__ = undefined;
|
||||||
@@ -81,9 +84,7 @@ let _refreshPromise = null;
|
|||||||
* @returns {Promise<boolean>} ``true`` if refresh succeeded
|
* @returns {Promise<boolean>} ``true`` if refresh succeeded
|
||||||
*/
|
*/
|
||||||
async function tryRefreshToken() {
|
async function tryRefreshToken() {
|
||||||
if (_refreshPromise) return _refreshPromise;
|
_refreshPromise = _refreshPromise || (async () => {
|
||||||
|
|
||||||
_refreshPromise = (async () => {
|
|
||||||
try {
|
try {
|
||||||
const stored = getStoredAuth();
|
const stored = getStoredAuth();
|
||||||
if (!stored.refresh) return false;
|
if (!stored.refresh) return false;
|
||||||
@@ -104,7 +105,7 @@ async function tryRefreshToken() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const tokens = json.data.tokens;
|
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:refresh', tokens.refresh_token);
|
||||||
sessionStorage.setItem('vw:access_ttl', String((json.data.access_ttl || 300) * 1000));
|
sessionStorage.setItem('vw:access_ttl', String((json.data.access_ttl || 300) * 1000));
|
||||||
sessionStorage.setItem('vw:session_id', tokens.session_id);
|
sessionStorage.setItem('vw:session_id', tokens.session_id);
|
||||||
@@ -117,8 +118,7 @@ async function tryRefreshToken() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
_refreshPromise = _refreshPromise.finally(() => { _refreshPromise = null; });
|
return _refreshPromise.finally(() => { _refreshPromise = null; });
|
||||||
return _refreshPromise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -165,10 +165,6 @@ export async function apiFetch(url, options = {}) {
|
|||||||
if (res.status === 401 && getAuthToken()) {
|
if (res.status === 401 && getAuthToken()) {
|
||||||
const refreshed = await tryRefreshToken();
|
const refreshed = await tryRefreshToken();
|
||||||
if (refreshed) {
|
if (refreshed) {
|
||||||
if (method === 'POST') {
|
|
||||||
redirectLogin();
|
|
||||||
return { ok: false, data: null, error: 'Session expired', status: 401 };
|
|
||||||
}
|
|
||||||
const refreshedStored = getStoredAuth();
|
const refreshedStored = getStoredAuth();
|
||||||
headers['Authorization'] = 'Bearer ' + getAuthToken();
|
headers['Authorization'] = 'Bearer ' + getAuthToken();
|
||||||
headers['X-Session-Id'] = refreshedStored.session_id;
|
headers['X-Session-Id'] = refreshedStored.session_id;
|
||||||
|
|||||||
@@ -185,9 +185,11 @@ const passkeyClickHandler = async () => {
|
|||||||
errEl.textContent = err.message || 'Passkey authentication failed';
|
errEl.textContent = err.message || 'Passkey authentication failed';
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
if (btn) {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = 'Sign in with passkey';
|
btn.textContent = 'Sign in with passkey';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const passkeyMouseEnterHandler = () => {
|
const passkeyMouseEnterHandler = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user