fix: missing brace in users.js, ws double-increment, b64url stack overflow, docs storage
This commit is contained in:
@@ -62,12 +62,12 @@ The `lib/` modules auto-discover the project root at runtime via `Path(__file__)
|
|||||||
|
|
||||||
## JWT Token Model
|
## JWT Token Model
|
||||||
|
|
||||||
Vacuum Wall uses JWT-based authentication with access/refresh token rotation. Tokens are stored in browser `localStorage` and injected as `Authorization: Bearer <token>` headers. The API never reads cookies — authentication is header-only.
|
Vacuum Wall uses JWT-based authentication with access/refresh token rotation. Tokens are stored in browser `sessionStorage` and injected as `Authorization: Bearer <token>` headers. The API never reads cookies — authentication is header-only.
|
||||||
|
|
||||||
| Token | Lifetime | Storage | Purpose |
|
| Token | Lifetime | Storage | Purpose |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Access | 15 min | localStorage | API auth, permission checks |
|
| Access | 15 min | sessionStorage / memory | API auth, permission checks |
|
||||||
| Refresh | 7 days | localStorage | Token rotation, new access tokens |
|
| Refresh | 7 days | sessionStorage | Token rotation, new access tokens |
|
||||||
|
|
||||||
JWT payload contains `sub` (username), `exp` (expiry), `iat` (issued at), `jti` (unique identifier), `type` (`"access"` or `"refresh"`), `permissions` (per-subsystem permissions), and `session_id` (session binding). Access tokens additionally contain `permissions` and `session_id`.
|
JWT payload contains `sub` (username), `exp` (expiry), `iat` (issued at), `jti` (unique identifier), `type` (`"access"` or `"refresh"`), `permissions` (per-subsystem permissions), and `session_id` (session binding). Access tokens additionally contain `permissions` and `session_id`.
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -72,7 +72,7 @@ The `daemon/client.py` module resolves `<param>` placeholders in URL paths befor
|
|||||||
|
|
||||||
The Flask WebUI binds exclusively to `127.0.0.1:9090`. It is not exposed directly to any network interface. All external access to the management UI is routed through an nginx reverse proxy on the designated management domain, which provides SSL termination. Authentication is handled at the Flask layer via JWT validation — no nginx-level `auth_basic` is applied to the management domain.
|
The Flask WebUI binds exclusively to `127.0.0.1:9090`. It is not exposed directly to any network interface. All external access to the management UI is routed through an nginx reverse proxy on the designated management domain, which provides SSL termination. Authentication is handled at the Flask layer via JWT validation — no nginx-level `auth_basic` is applied to the management domain.
|
||||||
|
|
||||||
JWT tokens are stored in browser `localStorage` and injected as `Authorization: Bearer <token>` headers. The API **never** reads cookies — authentication is header-only. This eliminates CSRF concerns: cross-origin requests cannot set custom headers.
|
JWT tokens are stored in browser `sessionStorage` and injected as `Authorization: Bearer <token>` headers. The API **never** reads cookies — authentication is header-only. This eliminates CSRF concerns: cross-origin requests cannot set custom headers.
|
||||||
|
|
||||||
The management interface does not set security hardening headers (e.g., `X-Content-Type-Options`, `X-Frame-Options`, HSTS) on proxied responses, as the SPA requires flexibility for its operation. It relies on JWT authentication, SSL termination, and the systemd sandbox for its security boundary.
|
The management interface does not set security hardening headers (e.g., `X-Content-Type-Options`, `X-Frame-Options`, HSTS) on proxied responses, as the SPA requires flexibility for its operation. It relies on JWT authentication, SSL termination, and the systemd sandbox for its security boundary.
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ The API exclusively reads the `Authorization` header — never cookies. This arc
|
|||||||
- No cookie-based session to exploit
|
- No cookie-based session to exploit
|
||||||
- No SameSite, double-submit, or origin checking needed
|
- No SameSite, double-submit, or origin checking needed
|
||||||
|
|
||||||
**XSS as the primary attack surface**: With header-only auth, XSS is the primary attack vector since `localStorage` is accessible to page scripts. Mitigations include:
|
**XSS as the primary attack surface**: With header-only auth, XSS is the primary attack vector since `sessionStorage` is accessible to page scripts. Mitigations include:
|
||||||
- CSP headers on the management domain (configured in nginx)
|
- CSP headers on the management domain (configured in nginx)
|
||||||
- `X-XSS-Protection` header
|
- `X-XSS-Protection` header
|
||||||
- Short-lived access tokens (15 min) with blacklist on logout
|
- Short-lived access tokens (15 min) with blacklist on logout
|
||||||
|
|||||||
@@ -166,7 +166,11 @@ function b64urlToArrayBuffer(b64url) {
|
|||||||
*/
|
*/
|
||||||
function arrayBufferToB64url(buffer) {
|
function arrayBufferToB64url(buffer) {
|
||||||
const bytes = new Uint8Array(buffer);
|
const bytes = new Uint8Array(buffer);
|
||||||
const bin = String.fromCharCode.apply(null, Array.from(bytes));
|
const chunks = [];
|
||||||
|
for (let i = 0; i < bytes.length; i += 0x8000) {
|
||||||
|
chunks.push(String.fromCharCode.apply(null, bytes.slice(i, i + 0x8000)));
|
||||||
|
}
|
||||||
|
const bin = chunks.join('');
|
||||||
return btoa(bin)
|
return btoa(bin)
|
||||||
.replace(/\+/g, '-')
|
.replace(/\+/g, '-')
|
||||||
.replace(/\//g, '_')
|
.replace(/\//g, '_')
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ let _wsConn = null;
|
|||||||
let _wsReconnectMs = 0;
|
let _wsReconnectMs = 0;
|
||||||
let _wsFailCount = 0;
|
let _wsFailCount = 0;
|
||||||
let _wsRefreshing = false;
|
let _wsRefreshing = false;
|
||||||
|
let _wsClosingHandled = false;
|
||||||
|
|
||||||
/** Direct onMessage handlers — { topics, handler, unsubscribed }[] */
|
/** Direct onMessage handlers — { topics, handler, unsubscribed }[] */
|
||||||
const _directHandlers = [];
|
const _directHandlers = [];
|
||||||
@@ -35,6 +36,7 @@ function _wsUrl() {
|
|||||||
function _wsConnect() {
|
function _wsConnect() {
|
||||||
if (_wsConn && _wsConn.readyState <= 1) return;
|
if (_wsConn && _wsConn.readyState <= 1) return;
|
||||||
|
|
||||||
|
_wsClosingHandled = false;
|
||||||
const token = window.__auth_token__;
|
const token = window.__auth_token__;
|
||||||
if (token) {
|
if (token) {
|
||||||
_wsConn = new WebSocket(_wsUrl(), ['Bearer ' + token]);
|
_wsConn = new WebSocket(_wsUrl(), ['Bearer ' + token]);
|
||||||
@@ -46,9 +48,12 @@ function _wsConnect() {
|
|||||||
_wsReconnectMs = 0;
|
_wsReconnectMs = 0;
|
||||||
_wsFailCount = 0;
|
_wsFailCount = 0;
|
||||||
_wsRefreshing = false;
|
_wsRefreshing = false;
|
||||||
|
_wsClosingHandled = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
_wsConn.onclose = () => {
|
_wsConn.onclose = () => {
|
||||||
|
if (_wsClosingHandled) return;
|
||||||
|
_wsClosingHandled = true;
|
||||||
if (!window.__auth_token__) return;
|
if (!window.__auth_token__) return;
|
||||||
_wsFailCount++;
|
_wsFailCount++;
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ async function loadUsers(abortController) {
|
|||||||
state.refreshing = false;
|
state.refreshing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function permissionLevel(perms, subsystem) {
|
function permissionLevel(perms, subsystem) {
|
||||||
return perms[subsystem] || '—';
|
return perms[subsystem] || '—';
|
||||||
|
|||||||
Reference in New Issue
Block a user