Session Timeout if "Remember Me" is not checked (#11125)

* fix: convert loginDays and loginSeconds to number inputs

* feat: configurable session timeout for when "Remember Me" is not checked

closes #11124

* test: addition tests to check loginDays and sessionDuration settings

* test: also test loginSeconds override
isekai-main
Julian Lam 2 years ago committed by GitHub
parent 2ea1510f8e
commit 69806662e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -4,6 +4,7 @@
"defaultLang": "en-GB",
"loginDays": 14,
"loginSeconds": 0,
"sessionDuration": 0,
"loginAttempts": 5,
"lockoutDuration": 60,
"adminReloginDuration": 60,

@ -29,6 +29,8 @@
"session-time-days": "Days",
"session-time-seconds": "Seconds",
"session-time-help": "These values are used to govern how long a user stays logged in when they check &quot;Remember Me&quot; on login. Note that only one of these values will be used. If there is no <i>seconds</i> value we fall back to <i>days</i>. If there is no <i>days</i> value we default to <i>14 days</i>.",
"session-duration": "Session length if \"Remember Me\" is not checked (seconds)",
"session-duration-help": "By default — or if set to <code>0</code> — a user will stay logged in for the duration of the session (e.g. however long the browser window/tab remains open). Set this value to explicitly invalidate the session after the specified number of seconds.",
"online-cutoff": "Minutes after user is considered inactive",
"online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.",
"registration": "User Registration",

@ -294,8 +294,9 @@ function continueLogin(strategy, req, res, next) {
req.session.cookie.maxAge = duration;
req.session.cookie.expires = new Date(Date.now() + duration);
} else {
req.session.cookie.maxAge = false;
req.session.cookie.expires = false;
const duration = meta.config.sessionDuration * 1000;
req.session.cookie.maxAge = duration || false;
req.session.cookie.expires = duration ? new Date(Date.now() + duration) : false;
}
plugins.hooks.fire('action:login.continue', { req, strategy, userData, error: null });

@ -126,13 +126,13 @@
<div class="col-sm-6">
<div class="form-group">
<label for="loginDays">[[admin/settings/user:session-time-days]]</label>
<input id="loginDays" type="text" class="form-control" data-field="loginDays" placeholder="[[admin/settings/user:session-time-days]]" />
<input id="loginDays" type="number" min="0" class="form-control" data-field="loginDays" placeholder="[[admin/settings/user:session-time-days]]" />
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label for="loginSeconds">[[admin/settings/user:session-time-seconds]]</label>
<input id="loginSeconds" type="text" class="form-control" data-field="loginSeconds" placeholder="[[admin/settings/user:session-time-seconds]]" />
<input id="loginSeconds" type="number" min="0" step="60" class="form-control" data-field="loginSeconds" placeholder="[[admin/settings/user:session-time-seconds]]" />
</div>
</div>
<div class="col-xs-12">
@ -141,6 +141,13 @@
</p>
</div>
</div>
<div class="form-group">
<label for="sessionDuration">[[admin/settings/user:session-duration]]</label>
<input id="sessionDuration" type="number" step="60" min="0" class="form-control" data-field="sessionDuration">
<p class="help-block">[[admin/settings/user:session-duration-help]]</p>
</div>
<div class="form-group">
<label for="onlineCutoff">[[admin/settings/user:online-cutoff]]</label>
<input id="onlineCutoff" type="text" class="form-control" data-field="onlineCutoff">

@ -158,22 +158,6 @@ describe('authentication', () => {
});
});
it('should login a user', async () => {
const { jar, body: loginBody } = await helpers.loginUser('regular', 'regularpwd');
assert(loginBody);
const body = await requestAsync({
url: `${nconf.get('url')}/api/self`,
json: true,
jar,
});
assert(body);
assert.equal(body.username, 'regular');
assert.equal(body.email, 'regular@nodebb.org');
const sessions = await db.getObject(`uid:${regularUid}:sessionUUID:sessionId`);
assert(sessions);
assert(Object.keys(sessions).length > 0);
});
it('should regenerate the session identifier on successful login', async () => {
const matchRegexp = /express\.sid=s%3A(.+?);/;
const { hostname, path } = url.parse(nconf.get('url'));
@ -202,6 +186,114 @@ describe('authentication', () => {
});
});
describe('login', () => {
let username;
let password;
let uid;
function getCookieExpiry(res) {
assert(res.headers['set-cookie']);
assert.strictEqual(res.headers['set-cookie'][0].includes('Expires'), true);
const values = res.headers['set-cookie'][0].split(';');
return values.reduce((memo, cur) => {
if (!memo) {
const [name, value] = cur.split('=');
if (name === ' Expires') {
memo = new Date(value);
}
}
return memo;
}, undefined);
}
beforeEach(async () => {
([username, password] = [utils.generateUUID().slice(0, 10), utils.generateUUID()]);
uid = await user.create({ username, password });
});
it('should login a user', async () => {
const { jar, body: loginBody } = await helpers.loginUser(username, password);
assert(loginBody);
const body = await requestAsync({
url: `${nconf.get('url')}/api/self`,
json: true,
jar,
});
assert(body);
assert.equal(body.username, username);
const sessions = await db.getObject(`uid:${uid}:sessionUUID:sessionId`);
assert(sessions);
assert(Object.keys(sessions).length > 0);
});
it('should set a cookie that only lasts for the life of the browser session', async () => {
const { res } = await helpers.loginUser(username, password);
assert(res.headers);
assert(res.headers['set-cookie']);
assert.strictEqual(res.headers['set-cookie'][0].includes('Expires'), false);
});
it('should set a different expiry if sessionDuration is set', async () => {
const _sessionDuration = meta.config.sessionDuration;
const days = 1;
meta.config.sessionDuration = days * 24 * 60 * 60;
const { res } = await helpers.loginUser(username, password);
const expiry = getCookieExpiry(res);
const expected = new Date();
expected.setUTCDate(expected.getUTCDate() + days);
assert.strictEqual(expiry.getUTCDate(), expected.getUTCDate());
meta.config.sessionDuration = _sessionDuration;
});
it('should set a cookie that lasts for x days where x is loginDays setting, if asked to remember', async () => {
const { res } = await helpers.loginUser(username, password, { remember: 'on' });
const expiry = getCookieExpiry(res);
const expected = new Date();
expected.setUTCDate(expected.getUTCDate() + meta.config.loginDays);
assert.strictEqual(expiry.getUTCDate(), expected.getUTCDate());
});
it('should set the cookie expiry properly if loginDays setting is changed', async () => {
const _loginDays = meta.config.loginDays;
meta.config.loginDays = 5;
const { res } = await helpers.loginUser(username, password, { remember: 'on' });
const expiry = getCookieExpiry(res);
const expected = new Date();
expected.setUTCDate(expected.getUTCDate() + meta.config.loginDays);
assert.strictEqual(expiry.getUTCDate(), expected.getUTCDate());
meta.config.loginDays = _loginDays;
});
it('should ignore loginDays if loginSeconds is truthy', async () => {
const _loginSeconds = meta.config.loginSeconds;
meta.config.loginSeconds = 60;
const { res } = await helpers.loginUser(username, password, { remember: 'on' });
const expiry = getCookieExpiry(res);
const expected = new Date();
expected.setUTCSeconds(expected.getUTCSeconds() + meta.config.loginSeconds);
assert.strictEqual(expiry.getUTCDate(), expected.getUTCDate());
assert.strictEqual(expiry.getUTCMinutes(), expected.getUTCMinutes());
meta.config.loginSeconds = _loginSeconds;
});
});
it('should fail to login if ip address is invalid', (done) => {
const jar = request.jar();
request({

Loading…
Cancel
Save