Sessions: 180-day lifetime with sliding renewal
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1566ac2897
commit
7dea2734ea
1 file changed
+18
-4
+18
-4
@@ -35,8 +35,14 @@ function parseCookies(req) {
|
|||||||
}).filter(([k]) => k));
|
}).filter(([k]) => k));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sessions last SESSION_DAYS and slide: any signed-in request more than a day after the
|
||||||
|
// cookie was issued gets a fresh one, so people who keep using the app never sign in again.
|
||||||
|
const SESSION_DAYS = 180;
|
||||||
|
const SESSION_MS = SESSION_DAYS * 24 * 60 * 60 * 1000;
|
||||||
|
const SESSION_RENEW_AFTER_MS = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
function signSession(username, epoch = 0) {
|
function signSession(username, epoch = 0) {
|
||||||
const expiry = Date.now() + 30 * 24 * 60 * 60 * 1000;
|
const expiry = Date.now() + SESSION_MS;
|
||||||
const payload = Buffer.from(`${username}|${expiry}|${epoch}`).toString('base64url');
|
const payload = Buffer.from(`${username}|${expiry}|${epoch}`).toString('base64url');
|
||||||
const sig = crypto.createHmac('sha256', SESSION_SECRET).update(payload).digest('base64url');
|
const sig = crypto.createHmac('sha256', SESSION_SECRET).update(payload).digest('base64url');
|
||||||
return `${payload}.${sig}`;
|
return `${payload}.${sig}`;
|
||||||
@@ -56,12 +62,12 @@ function verifySession(token) {
|
|||||||
const decoded = Buffer.from(payload, 'base64url').toString();
|
const decoded = Buffer.from(payload, 'base64url').toString();
|
||||||
const [username, expiry, epoch] = decoded.split('|');
|
const [username, expiry, epoch] = decoded.split('|');
|
||||||
if (!username || !expiry || Date.now() > parseInt(expiry)) return null;
|
if (!username || !expiry || Date.now() > parseInt(expiry)) return null;
|
||||||
return { username, epoch: parseInt(epoch) || 0 };
|
return { username, epoch: parseInt(epoch) || 0, expiry: parseInt(expiry) };
|
||||||
}
|
}
|
||||||
|
|
||||||
function sessionCookie(req, token) {
|
function sessionCookie(req, token) {
|
||||||
const secure = req.secure || req.headers['x-forwarded-proto'] === 'https' ? '; Secure' : '';
|
const secure = req.secure || req.headers['x-forwarded-proto'] === 'https' ? '; Secure' : '';
|
||||||
return `dsession=${token}; HttpOnly; Path=/; Max-Age=${30 * 24 * 3600}; SameSite=Lax${secure}`;
|
return `dsession=${token}; HttpOnly; Path=/; Max-Age=${SESSION_DAYS * 24 * 3600}; SameSite=Lax${secure}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hashPassword(password, salt) {
|
function hashPassword(password, salt) {
|
||||||
@@ -142,12 +148,20 @@ function currentUser(req) {
|
|||||||
if (!s) return null;
|
if (!s) return null;
|
||||||
const u = findUser(s.username);
|
const u = findUser(s.username);
|
||||||
if (!u || u.disabled || (u.epoch || 0) !== s.epoch) return null;
|
if (!u || u.disabled || (u.epoch || 0) !== s.epoch) return null;
|
||||||
|
req.sessionExpiry = s.expiry;
|
||||||
return u;
|
return u;
|
||||||
}
|
}
|
||||||
|
|
||||||
function requireDashboardAuth(req, res, next) {
|
function requireDashboardAuth(req, res, next) {
|
||||||
const user = currentUser(req);
|
const user = currentUser(req);
|
||||||
if (user) { req.dashUser = user; return next(); }
|
if (user) {
|
||||||
|
req.dashUser = user;
|
||||||
|
// Sliding renewal (routes that set their own cookie later simply overwrite this header)
|
||||||
|
if (req.sessionExpiry - Date.now() < SESSION_MS - SESSION_RENEW_AFTER_MS) {
|
||||||
|
res.setHeader('Set-Cookie', sessionCookie(req, signSession(user.username, user.epoch || 0)));
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
}
|
||||||
if (req.path.startsWith('/api/')) return res.status(401).json({ error: 'Unauthorized' });
|
if (req.path.startsWith('/api/')) return res.status(401).json({ error: 'Unauthorized' });
|
||||||
res.redirect('/dashboard/login');
|
res.redirect('/dashboard/login');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user