security: harden JWT auth with session binding, CSP headers, and sessionStorage

- Reduce access_token_ttl from 900s to 300s (5 min) to shrink XSS exploit window
- Add session_id claim to JWT tokens tied to browser session (X-Session-Id header)
- Flask middleware validates session_id matches header on every request
- CSP headers: default-src/script-src 'self', no unsafe-inline/eval, frame-ancestors none
- X-Content-Type-Options: nosniff on all responses
- Move refresh token from localStorage to sessionStorage (tab-scoped, cleared on close)
- Timing-safe password verification (dummy Argon2id for unknown users)
- WebSocket auth also validates session_id header
- Add 5 session_id tests and 3 CSP header tests
This commit is contained in:
2026-07-24 02:51:54 +00:00
parent 56b200d233
commit a365059976
13 changed files with 317 additions and 96 deletions
+11 -5
View File
@@ -7,14 +7,14 @@
import { apiFetch, setAuthToken, clearAuthTokens, redirectLogin, getAuthToken, tryRefreshToken, toast } from '../api.js?v=12';
/**
* Schedule a token refresh based on the access token TTL stored in localStorage.
* Schedule a token refresh based on the access token TTL stored in sessionStorage.
* The refresh fires at TTL - 60 seconds to allow the browser to refresh smoothly.
*/
export function scheduleTokenRefresh() {
if (typeof window.__authRefreshTimer__ !== 'undefined') {
clearTimeout(window.__authRefreshTimer__);
}
const ttl = parseInt(localStorage.getItem('vw:access_ttl'), 10) || 900000;
const ttl = parseInt(sessionStorage.getItem('vw:access_ttl'), 10) || 300000;
const delay = Math.max(ttl - 60000, 30000);
window.__authRefreshTimer__ = setTimeout(async () => {
@@ -47,6 +47,7 @@ export async function checkSession() {
const { user, permissions } = result.data || {};
if (user) {
localStorage.setItem('vw:user', JSON.stringify(user));
sessionStorage.setItem('vw:user', JSON.stringify(user));
if (permissions) {
localStorage.setItem('vw:permissions', JSON.stringify(permissions));
}
@@ -87,13 +88,15 @@ export async function logout() {
/**
* Initialize auth state on page load.
* Checks stored tokens, validates session, and schedules refresh.
* Since sessionStorage is cleared on tab close, a closed/reopened tab
* will always fall through to reauth.
*
* @returns {Promise<boolean>} true if authenticated
*/
export async function initAuth() {
const token = getAuthToken();
if (token) {
const saved = JSON.parse(localStorage.getItem('vw:user') || 'null');
const saved = JSON.parse(sessionStorage.getItem('vw:user') || 'null');
if (saved) {
const ok = await checkSession();
if (ok) {
@@ -116,8 +119,11 @@ export function handleLoginSuccess(data, redirectPath = '/dashboard') {
const { tokens, user, permissions } = data || {};
if (tokens) {
setAuthToken(tokens.access_token);
localStorage.setItem('vw:refresh', tokens.refresh_token);
localStorage.setItem('vw:access_ttl', String((data.access_ttl || 900) * 1000));
sessionStorage.setItem('vw:refresh', tokens.refresh_token);
sessionStorage.setItem('vw:access_ttl', String((data.access_ttl || 300) * 1000));
if (tokens.session_id) {
sessionStorage.setItem('vw:session_id', tokens.session_id);
}
if (user) {
localStorage.setItem('vw:user', JSON.stringify(user));
if (permissions) {