Skip to content

Commit d08394c

Browse files
garethbowendianabarsan
authored andcommitted
Refresh the userCtx expiry when getting session
This keeps pushing the userCtx expiry date a year ahead so it's unlikely to ever expire in normal usage conditions. If this cookie expires then the user is redirected to the login page despite having a valid CouchDB session and AuthSession cookie. #6583 (cherry picked from commit e5ba490)
1 parent d63d296 commit d08394c

File tree

4 files changed

+112
-32
lines changed

4 files changed

+112
-32
lines changed

api/src/controllers/login.js

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,8 @@ const fs = require('fs'),
88
environment = require('../environment'),
99
config = require('../config'),
1010
cookie = require('../services/cookie'),
11-
SESSION_COOKIE_RE = /AuthSession=([^;]*);/,
12-
ONE_YEAR = 31536000000,
1311
logger = require('../logger'),
14-
db = require('../db'),
15-
production = process.env.NODE_ENV === 'production';
12+
db = require('../db');
1613

1714
let loginTemplate;
1815

@@ -89,30 +86,8 @@ const createSession = req => {
8986
});
9087
};
9188

92-
const getCookieOptions = () => {
93-
return {
94-
sameSite: 'lax', // prevents the browser from sending this cookie along with some cross-site requests
95-
secure: production, // only transmit when requesting via https unless in development mode
96-
};
97-
};
98-
99-
const setSessionCookie = (res, cookie) => {
100-
const sessionId = SESSION_COOKIE_RE.exec(cookie)[1];
101-
const options = getCookieOptions();
102-
options.httpOnly = true; // don't allow javascript access to stop xss
103-
res.cookie('AuthSession', sessionId, options);
104-
};
105-
10689
const setUserCtxCookie = (res, userCtx) => {
107-
const options = getCookieOptions();
108-
options.maxAge = ONE_YEAR;
109-
res.cookie('userCtx', JSON.stringify(userCtx), options);
110-
};
111-
112-
const setLocaleCookie = (res, locale) => {
113-
const options = getCookieOptions();
114-
options.maxAge = ONE_YEAR;
115-
res.cookie('locale', locale, options);
90+
cookie.setUserCtx(res, JSON.stringify(userCtx));
11691
};
11792

11893
const getRedirectUrl = userCtx => {
@@ -136,13 +111,13 @@ const setCookies = (req, res, sessionRes) => {
136111
return auth
137112
.getUserCtx(options)
138113
.then(userCtx => {
139-
setSessionCookie(res, sessionCookie);
114+
cookie.setSession(res, sessionCookie);
140115
setUserCtxCookie(res, userCtx);
141116
// Delete login=force cookie
142117
res.clearCookie('login');
143118
return auth.getUserSettings(userCtx).then(({ language }={}) => {
144119
if (language) {
145-
setLocaleCookie(res, language);
120+
cookie.setLocale(res, language);
146121
}
147122
res.status(302).send(getRedirectUrl(userCtx));
148123
});

api/src/routing.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const _ = require('underscore'),
3535
authorization = require('./middleware/authorization'),
3636
createUserDb = require('./controllers/create-user-db'),
3737
purgedDocsController = require('./controllers/purged-docs'),
38+
cookie = require('./services/cookie'),
3839
staticResources = /\/(templates|static)\//,
3940
// CouchDB is very relaxed in matching routes
4041
routePrefix = '/+' + environment.db + '/+',
@@ -247,6 +248,16 @@ ONLINE_ONLY_ENDPOINTS.forEach(url =>
247248
app.all(routePrefix + url, authorization.offlineUserFirewall)
248249
);
249250

251+
// allow anyone to access their session
252+
app.all('/_session', function(req, res) {
253+
const given = cookie.get(req, 'userCtx');
254+
if (given) {
255+
// update the expiry date on the cookie to keep it fresh
256+
cookie.setUserCtx(res, decodeURIComponent(given));
257+
}
258+
proxy.web(req, res);
259+
});
260+
250261
var UNAUDITED_ENDPOINTS = [
251262
// This takes arbitrary JSON, not whole documents with `_id`s, so it's not
252263
// auditable in our current framework
@@ -262,8 +273,6 @@ var UNAUDITED_ENDPOINTS = [
262273
// Interacting with mongo filters uses POST
263274
routePrefix + '_find',
264275
routePrefix + '_explain',
265-
// allow anyone to access their _session information
266-
'/_session',
267276
];
268277

269278
UNAUDITED_ENDPOINTS.forEach(function(url) {

api/src/services/cookie.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
const production = process.env.NODE_ENV === 'production';
2+
const ONE_YEAR = 31536000000;
3+
const SESSION_COOKIE_RE = /AuthSession=([^;]*);/;
4+
5+
const getCookieOptions = () => {
6+
return {
7+
sameSite: 'lax', // prevents the browser from sending this cookie along with some cross-site requests
8+
secure: production, // only transmit when requesting via https unless in development mode
9+
};
10+
};
11+
112
module.exports = {
213
get: (req, name) => {
314
const cookies = req.headers && req.headers.cookie;
@@ -7,5 +18,21 @@ module.exports = {
718
const prefix = name + '=';
819
const cookie = cookies.split(';').find(cookie => cookie.trim().startsWith(prefix));
920
return cookie && cookie.trim().substring(prefix.length);
21+
},
22+
setUserCtx: (res, content) => {
23+
const options = getCookieOptions();
24+
options.maxAge = ONE_YEAR;
25+
res.cookie('userCtx', content, options);
26+
},
27+
setSession: (res, cookie) => {
28+
const sessionId = SESSION_COOKIE_RE.exec(cookie)[1];
29+
const options = getCookieOptions();
30+
options.httpOnly = true; // don't allow javascript access to stop xss
31+
res.cookie('AuthSession', sessionId, options);
32+
},
33+
setLocale: (res, locale) => {
34+
const options = getCookieOptions();
35+
options.maxAge = ONE_YEAR;
36+
res.cookie('locale', locale, options);
1037
}
1138
};

tests/e2e/api/routing.js

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const _ = require('underscore'),
2+
moment = require('moment'),
23
utils = require('../../utils'),
34
constants = require('../../constants');
45

@@ -632,10 +633,78 @@ describe('routing', () => {
632633
});
633634
});
634635

636+
describe('_session endpoint', () => {
637+
638+
it('logs the user in', () => {
639+
640+
const username = 'offline';
641+
const now = moment.utc();
642+
const userCtxCookie = {
643+
name: username,
644+
roles: [ 'chw' ]
645+
};
646+
647+
const createSession = () => {
648+
return utils.request({
649+
resolveWithFullResponse: true,
650+
json: true,
651+
path: '/_session',
652+
method: 'POST',
653+
headers: { 'Content-Type': 'application/json' },
654+
body: { name: username, password },
655+
username,
656+
password
657+
});
658+
};
659+
660+
const getSession = sessionCookie => {
661+
return utils.request({
662+
resolveWithFullResponse: true,
663+
path: '/_session',
664+
method: 'GET',
665+
headers: {
666+
Cookie: `locale=en; ${sessionCookie}; userCtx=${JSON.stringify(userCtxCookie)}`,
667+
},
668+
noAuth: true
669+
});
670+
};
671+
672+
return createSession()
673+
.then(res => {
674+
expect(res.statusCode).toEqual(200);
675+
expect(res.headers['set-cookie'].length).toEqual(1);
676+
const sessionCookie = res.headers['set-cookie'][0].split(';')[0];
677+
expect(sessionCookie.split('=')[0]).toEqual('AuthSession');
678+
return sessionCookie;
679+
})
680+
.then(sessionCookie => getSession(sessionCookie))
681+
.then(res => {
682+
expect(res.statusCode).toEqual(200);
683+
expect(res.headers['set-cookie'].length).toEqual(1);
684+
const [ content, age, path, expires, samesite ] = res.headers['set-cookie'][0].split('; ');
685+
686+
// check the cookie content is unchanged
687+
const [ contentKey, contentValue ] = content.split('=');
688+
expect(contentKey).toEqual('userCtx');
689+
expect(decodeURIComponent(contentValue)).toEqual(JSON.stringify(userCtxCookie));
690+
691+
// check the expiry date is around a year away
692+
const expiryValue = expires.split('=')[1];
693+
const expiryDate = moment.utc(expiryValue).add(1, 'hour'); // add a small margin of error
694+
expect(expiryDate.diff(now, 'months')).toEqual(12);
695+
696+
// check the other properties
697+
expect(samesite).toEqual('SameSite=Lax');
698+
expect(age).toEqual('Max-Age=31536000');
699+
expect(path).toEqual('Path=/');
700+
});
701+
});
702+
});
703+
635704
describe('legacy endpoints', () => {
636705
afterEach(done => utils.revertSettings().then(done));
637706

638-
it('should still route to deprecate apiV0 settings endpoints', () => {
707+
it('should still route to deprecated apiV0 settings endpoints', () => {
639708
let settings;
640709
return utils
641710
.updateSettings({}) // this test will update settings that we want successfully reverted afterwards

0 commit comments

Comments
 (0)