Files
BetterDesk/web-nodejs/lib/sessionPolicy.js
T
UNITRONIX db07c5c5af feat(auth): enforce idle timeout and single panel session per account
Server-side session policy replaces long-lived idle console logins: sliding
idle timeout, absolute max age, atomic replacement of prior sessions, IP/UA
audit metadata, and consistent expired-login redirects.

Thanks: INSOLVE (Honorary); Marco Jakobs (@jacotec); MyNameisStitch (@MyNameisStitch); Redspin (@playerumpknow); muopc (@muopc)
2026-10-19 10:19:56 +02:00

96 lines
2.6 KiB
JavaScript

'use strict';
/**
* Panel console session policy helpers.
* Idle timeout slides with activity; absolute maxAge is hard from login time.
*/
const MIN_IDLE_MS = 60 * 1000; // 1 minute
const MAX_IDLE_MS = 24 * 60 * 60 * 1000; // 24 hours
const DEFAULT_IDLE_MS = 30 * 60 * 1000; // 30 minutes
const MIN_MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes
const MAX_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
const DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
const DEFAULT_WARN_BEFORE_MS = 60 * 1000; // 1 minute
function parsePositiveInt(raw, fallback) {
const n = parseInt(raw, 10);
return Number.isFinite(n) && n > 0 ? n : fallback;
}
function clamp(n, min, max) {
return Math.min(max, Math.max(min, n));
}
/**
* Resolve session policy from environment / config overrides.
* @param {object} [env]
* @returns {{ idleTimeoutMs: number, maxAgeMs: number, warnBeforeMs: number }}
*/
function resolveSessionPolicy(env = process.env) {
const idleTimeoutMs = clamp(
parsePositiveInt(env.SESSION_IDLE_TIMEOUT, DEFAULT_IDLE_MS),
MIN_IDLE_MS,
MAX_IDLE_MS
);
let maxAgeMs = clamp(
parsePositiveInt(env.SESSION_MAX_AGE, DEFAULT_MAX_AGE_MS),
MIN_MAX_AGE_MS,
MAX_MAX_AGE_MS
);
// Absolute lifetime must always be at least idle timeout.
if (maxAgeMs < idleTimeoutMs) {
maxAgeMs = idleTimeoutMs;
}
const warnBeforeMs = clamp(
parsePositiveInt(env.SESSION_IDLE_WARN_MS, DEFAULT_WARN_BEFORE_MS),
15 * 1000,
Math.min(idleTimeoutMs, 10 * 60 * 1000)
);
return { idleTimeoutMs, maxAgeMs, warnBeforeMs };
}
const END_REASONS = Object.freeze({
LOGOUT: 'logout',
REPLACED: 'replaced',
IDLE: 'idle',
MAX_AGE: 'max_age',
REVOKED: 'revoked',
DESTROYED: 'destroyed'
});
function sanitizeUserAgent(ua) {
const s = String(ua || '').trim();
if (!s) return '';
return s.length > 512 ? s.slice(0, 512) : s;
}
function sanitizeIp(ip) {
let s = String(ip || '').trim();
if (s.startsWith('::ffff:')) s = s.slice(7);
return s.length > 128 ? s.slice(0, 128) : s;
}
function sanitizeFingerprint(fp) {
const s = String(fp || '').trim();
if (!s) return '';
// Accept only opaque hex/base64url-like fingerprints from the browser.
if (!/^[A-Za-z0-9_.=+/-]{8,128}$/.test(s)) return '';
return s;
}
module.exports = {
MIN_IDLE_MS,
MAX_IDLE_MS,
DEFAULT_IDLE_MS,
MIN_MAX_AGE_MS,
MAX_MAX_AGE_MS,
DEFAULT_MAX_AGE_MS,
DEFAULT_WARN_BEFORE_MS,
END_REASONS,
resolveSessionPolicy,
sanitizeUserAgent,
sanitizeIp,
sanitizeFingerprint
};