enforce mandatory X-Session-Id header for access token validation
Session binding was bypassable: if the X-Session-Id header was absent, validate_token skipped the check entirely, allowing a stolen JWT to be used without the originating session. Server-side: reject 401 early in Flask middleware and daemon WebSocket handler when X-Session-Id is missing, before calling validate_token. Updated validate_token to always enforce session_id matching for access tokens (refresh tokens are unaffected as they carry no session_id claim). Frontend: removed dead if (stored.session_id) guards in api.js since the header is now always required. Added X-Session-Id to logout request headers and always store session_id on login/refresh.
This commit is contained in:
@@ -196,6 +196,8 @@ def _auth_middleware():
|
||||
|
||||
token_string = auth_header[7:] # strip "Bearer "
|
||||
session_header = request.headers.get("X-Session-Id")
|
||||
if not session_header:
|
||||
return jsonify({"ok": False, "error": "unauthorized"}), 401
|
||||
payload = validate_token(
|
||||
token_string, token_type="access", session_id=session_header
|
||||
)
|
||||
|
||||
@@ -107,9 +107,7 @@ async function tryRefreshToken() {
|
||||
window.__auth_token__ = tokens.access_token;
|
||||
sessionStorage.setItem('vw:refresh', tokens.refresh_token);
|
||||
sessionStorage.setItem('vw:access_ttl', String((json.data.access_ttl || 300) * 1000));
|
||||
if (tokens.session_id) {
|
||||
sessionStorage.setItem('vw:session_id', tokens.session_id);
|
||||
}
|
||||
sessionStorage.setItem('vw:session_id', tokens.session_id);
|
||||
if (json.data.user) {
|
||||
sessionStorage.setItem('vw:user', JSON.stringify(json.data.user));
|
||||
}
|
||||
@@ -151,9 +149,7 @@ export async function apiFetch(url, options = {}) {
|
||||
if (token) {
|
||||
headers['Authorization'] = 'Bearer ' + token;
|
||||
const stored = getStoredAuth();
|
||||
if (stored.session_id) {
|
||||
headers['X-Session-Id'] = stored.session_id;
|
||||
}
|
||||
headers['X-Session-Id'] = stored.session_id;
|
||||
}
|
||||
|
||||
if (body && typeof body === 'object' && !(body instanceof FormData)) {
|
||||
@@ -171,9 +167,7 @@ export async function apiFetch(url, options = {}) {
|
||||
if (refreshed) {
|
||||
const refreshedStored = getStoredAuth();
|
||||
headers['Authorization'] = 'Bearer ' + getAuthToken();
|
||||
if (refreshedStored.session_id) {
|
||||
headers['X-Session-Id'] = refreshedStored.session_id;
|
||||
}
|
||||
headers['X-Session-Id'] = refreshedStored.session_id;
|
||||
const retryRes = await fetch(url, { method, headers, body: options.body, credentials: 'same-origin', ...opts });
|
||||
if (retryRes.ok) {
|
||||
const json = await retryRes.json().catch(() => null);
|
||||
|
||||
@@ -67,6 +67,7 @@ export async function logout() {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'X-Session-Id': sessionStorage.getItem('vw:session_id'),
|
||||
};
|
||||
const refresh = sessionStorage.getItem('vw:refresh');
|
||||
await fetch('/api/auth/logout', {
|
||||
@@ -120,9 +121,7 @@ export function handleLoginSuccess(data, redirectPath = '/dashboard') {
|
||||
setAuthToken(tokens.access_token);
|
||||
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);
|
||||
}
|
||||
sessionStorage.setItem('vw:session_id', tokens.session_id);
|
||||
if (user) {
|
||||
sessionStorage.setItem('vw:user', JSON.stringify(user));
|
||||
if (permissions) {
|
||||
|
||||
Reference in New Issue
Block a user