feat: fix session mismatch errors by clearing cookie on logout (#8338)

* feat: fix session mismatch errors by clearing cookie on logout

* feat: remove app.upateHeader

ported from 2.0

* feat: handle if user doesn't click button and just refreshes page
v1.18.x
Barış Soner Uşaklı 5 years ago committed by GitHub
parent 938c232377
commit 5781a2dc65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -37,7 +37,9 @@
}); });
} }
$('[component="logout"]').on('click', app.logout); $('[component="logout"]').on('click', function () {
app.logout();
});
configureSlidemenu(); configureSlidemenu();
setupNProgress(); setupNProgress();

@ -57,7 +57,9 @@ app.cacheBuster = null;
app.newTopic(); app.newTopic();
}); });
$('#header-menu .container').on('click', '[component="user/logout"]', app.logout); $('#header-menu .container').on('click', '[component="user/logout"]', function () {
app.logout();
});
Visibility.change(function (event, state) { Visibility.change(function (event, state) {
if (state === 'visible') { if (state === 'visible') {
@ -105,111 +107,27 @@ app.cacheBuster = null;
}); });
}; };
app.updateHeader = function (data, callback) { app.logout = function (redirect) {
/** redirect = redirect === undefined ? true : redirect;
* data:
* header (obj)
* config (obj)
* next (string)
*/
require([
'benchpress',
'translator',
'forum/unread',
'forum/header/notifications',
'forum/header/chat',
], function (Benchpress, translator, Unread, Notifications, Chat) {
app.user = data.header.user;
data.header.config = data.config;
config = data.config;
Benchpress.setGlobal('config', config);
var htmlEl = $('html');
htmlEl.attr('data-dir', data.header.languageDirection);
htmlEl.css('direction', data.header.languageDirection);
// Manually reconnect socket.io
socket.close();
socket.open();
// Re-render top bar menu
var toRender = {
'slideout-menu': $('.slideout-menu'),
menu: $('#header-menu .container'),
'chats-menu': $('#chats-menu'),
};
Promise.all(Object.keys(toRender).map(function (tpl) {
return Benchpress.render('partials/' + tpl, data.header).then(function (render) {
return translator.Translator.create().translate(render);
});
})).then(function (html) {
Object.keys(toRender)
.map(function (k) { return toRender[k]; })
.forEach(function (element, idx) {
element.html(html[idx]);
});
Unread.initUnreadTopics();
Notifications.prepareDOM();
Chat.prepareDOM();
app.reskin(data.header.bootswatchSkin);
translator.switchTimeagoLanguage(callback);
bootbox.setLocale(config.userLang);
if (config.searchEnabled) {
app.handleSearch();
}
handleStatusChange();
$(window).trigger('action:app.updateHeader');
});
});
};
app.logout = function (e) {
if (e) {
e.preventDefault();
}
$(window).trigger('action:app.logout'); $(window).trigger('action:app.logout');
/*
Set session refresh flag (otherwise the session check will trip and throw invalid session modal)
We know the session is/will be invalid (uid mismatch) because the user is logging out
*/
app.flags = app.flags || {};
app.flags._sessionRefresh = true;
$.ajax(config.relative_path + '/logout', { $.ajax(config.relative_path + '/logout', {
type: 'POST', type: 'POST',
headers: { headers: {
'x-csrf-token': config.csrf_token, 'x-csrf-token': config.csrf_token,
}, },
success: function (data) { success: function (data) {
// ACP logouts go to frontend via page load, not ajaxify
if (ajaxify.data.template.name.startsWith('admin/')) {
$(window).trigger('action:app.loggedOut', data);
window.location.href = config.relative_path + (data.next || '/');
return;
}
app.updateHeader(data, function () {
// Overwrite in hook (below) to redirect elsewhere
data.next = data.next || undefined;
$(window).trigger('action:app.loggedOut', data); $(window).trigger('action:app.loggedOut', data);
if (redirect) {
if (data.next) { if (data.next) {
if (data.next.startsWith('http')) {
window.location.href = data.next; window.location.href = data.next;
return;
}
ajaxify.go(data.next);
} else { } else {
ajaxify.refresh(); window.location.reload();
}
} }
});
}, },
}); });
return false;
}; };
app.alert = function (params) { app.alert = function (params) {
@ -249,27 +167,16 @@ app.cacheBuster = null;
}; };
app.handleInvalidSession = function () { app.handleInvalidSession = function () {
if (app.flags && app.flags._sessionRefresh) {
return;
}
app.flags = app.flags || {};
app.flags._sessionRefresh = true;
socket.disconnect(); socket.disconnect();
app.logout(false);
require(['translator'], function (translator) {
translator.translate('[[error:invalid-session-text]]', function (translated) {
bootbox.alert({ bootbox.alert({
title: '[[error:invalid-session]]', title: '[[error:invalid-session]]',
message: translated, message: '[[error:invalid-session-text]]',
closeButton: false, closeButton: false,
callback: function () { callback: function () {
window.location.reload(); window.location.reload();
}, },
}); });
});
});
}; };
app.enterRoom = function (room, callback) { app.enterRoom = function (room, callback) {

@ -24,26 +24,18 @@ define('forum/login', [], function () {
submitEl.addClass('disabled'); submitEl.addClass('disabled');
/*
Set session refresh flag (otherwise the session check will trip and throw invalid session modal)
We know the session is/will be invalid (uid mismatch) because the user is attempting a login
*/
app.flags = app.flags || {};
app.flags._sessionRefresh = true;
formEl.ajaxSubmit({ formEl.ajaxSubmit({
headers: { headers: {
'x-csrf-token': config.csrf_token, 'x-csrf-token': config.csrf_token,
}, },
success: function (data) { success: function (data) {
var pathname = utils.urlToLocation(data.next).pathname;
var params = utils.params({ url: data.next }); var params = utils.params({ url: data.next });
params.loggedin = true; params.loggedin = true;
var qs = decodeURIComponent($.param(params));
app.updateHeader(data, function () { window.location.href = pathname + '?' + qs;
ajaxify.go(data.next);
app.flags._sessionRefresh = false;
$(window).trigger('action:app.loggedIn', data);
});
}, },
error: function (data) { error: function (data) {
if (data.status === 403 && data.responseText === 'Forbidden') { if (data.status === 403 && data.responseText === 'Forbidden') {
@ -52,7 +44,6 @@ define('forum/login', [], function () {
errorEl.find('p').translateText(data.responseText); errorEl.find('p').translateText(data.responseText);
errorEl.show(); errorEl.show();
submitEl.removeClass('disabled'); submitEl.removeClass('disabled');
app.flags._sessionRefresh = false;
// Select the entire password if that field has focus // Select the entire password if that field has focus
if ($('#password:focus').length) { if ($('#password:focus').length) {

@ -93,7 +93,6 @@ apiController.loadConfig = async function (req) {
config.topicSearchEnabled = settings.topicSearchEnabled || false; config.topicSearchEnabled = settings.topicSearchEnabled || false;
config.bootswatchSkin = (meta.config.disableCustomUserSkins !== 1 && settings.bootswatchSkin && settings.bootswatchSkin !== '') ? settings.bootswatchSkin : ''; config.bootswatchSkin = (meta.config.disableCustomUserSkins !== 1 && settings.bootswatchSkin && settings.bootswatchSkin !== '') ? settings.bootswatchSkin : '';
config = await plugins.fireHook('filter:config.get', config); config = await plugins.fireHook('filter:config.get', config);
req.res.locals.config = config;
return config; return config;
}; };

@ -15,7 +15,6 @@ const plugins = require('../plugins');
const utils = require('../utils'); const utils = require('../utils');
const translator = require('../translator'); const translator = require('../translator');
const helpers = require('./helpers'); const helpers = require('./helpers');
const middleware = require('../middleware');
const privileges = require('../privileges'); const privileges = require('../privileges');
const sockets = require('../socket.io'); const sockets = require('../socket.io');
@ -159,9 +158,7 @@ authenticationController.registerComplete = function (req, res, next) {
async.parallel(callbacks, async function (_blank, err) { async.parallel(callbacks, async function (_blank, err) {
if (err.length) { if (err.length) {
err = err.filter(Boolean).map(function (err) { err = err.filter(Boolean).map(err => err.message);
return err.message;
});
} }
if (err.length) { if (err.length) {
@ -232,7 +229,7 @@ authenticationController.login = function (req, res, next) {
}; };
function continueLogin(req, res, next) { function continueLogin(req, res, next) {
passport.authenticate('local', function (err, userData, info) { passport.authenticate('local', async function (err, userData, info) {
if (err) { if (err) {
return helpers.noScriptErrors(req, res, err.message, 403); return helpers.noScriptErrors(req, res, err.message, 403);
} }
@ -258,39 +255,19 @@ function continueLogin(req, res, next) {
winston.verbose('[auth] Triggering password reset for uid ' + userData.uid + ' due to password policy'); winston.verbose('[auth] Triggering password reset for uid ' + userData.uid + ' due to password policy');
req.session.passwordExpired = true; req.session.passwordExpired = true;
async.series({ const code = await user.reset.generate(userData.uid);
code: async.apply(user.reset.generate, userData.uid),
buildHeader: async.apply(middleware.buildHeader, req, res),
header: async.apply(middleware.generateHeader, req, res, {}),
}, function (err, payload) {
if (err) {
return helpers.noScriptErrors(req, res, err.message, 403);
}
res.status(200).send({ res.status(200).send({
next: nconf.get('relative_path') + '/reset/' + payload.code, next: nconf.get('relative_path') + '/reset/' + code,
header: payload.header,
config: res.locals.config,
});
}); });
} else { } else {
delete req.query.lang; delete req.query.lang;
await authenticationController.doLogin(req, userData.uid);
async.series({
doLogin: async.apply(authenticationController.doLogin, req, userData.uid),
buildHeader: async.apply(middleware.buildHeader, req, res),
header: async.apply(middleware.generateHeader, req, res, {}),
}, function (err, payload) {
if (err) {
return helpers.noScriptErrors(req, res, err.message, 403);
}
var destination; var destination;
if (!req.session.returnTo) { if (req.session.returnTo) {
destination = nconf.get('relative_path') + '/';
} else {
destination = req.session.returnTo; destination = req.session.returnTo;
delete req.session.returnTo; delete req.session.returnTo;
} else {
destination = nconf.get('relative_path') + '/';
} }
if (req.body.noscript === 'true') { if (req.body.noscript === 'true') {
@ -298,11 +275,8 @@ function continueLogin(req, res, next) {
} else { } else {
res.status(200).send({ res.status(200).send({
next: destination, next: destination,
header: payload.header,
config: res.locals.config,
}); });
} }
});
} }
})(req, res, next); })(req, res, next);
} }
@ -416,6 +390,7 @@ const destroyAsync = util.promisify((req, callback) => req.session.destroy(callb
authenticationController.logout = async function (req, res, next) { authenticationController.logout = async function (req, res, next) {
if (!req.loggedIn || !req.sessionID) { if (!req.loggedIn || !req.sessionID) {
res.clearCookie(nconf.get('sessionKey'), meta.configs.cookie.get());
return res.status(200).send('not-logged-in'); return res.status(200).send('not-logged-in');
} }
const uid = req.uid; const uid = req.uid;
@ -434,24 +409,16 @@ authenticationController.logout = async function (req, res, next) {
await db.sortedSetAdd('users:online', Date.now() - (meta.config.onlineCutoff * 60000), uid); await db.sortedSetAdd('users:online', Date.now() - (meta.config.onlineCutoff * 60000), uid);
await plugins.fireHook('static:user.loggedOut', { req: req, res: res, uid: uid, sessionID: sessionID }); await plugins.fireHook('static:user.loggedOut', { req: req, res: res, uid: uid, sessionID: sessionID });
const autoLocaleAsync = util.promisify(middleware.autoLocale);
await autoLocaleAsync(req, res);
// Force session check for all connected socket.io clients with the same session id // Force session check for all connected socket.io clients with the same session id
sockets.in('sess_' + sessionID).emit('checkSession', 0); sockets.in('sess_' + sessionID).emit('checkSession', 0);
if (req.body.noscript === 'true') {
return res.redirect(nconf.get('relative_path') + '/');
}
const buildHeaderAsync = util.promisify(middleware.buildHeader);
const generateHeaderAsync = util.promisify(middleware.generateHeader);
await buildHeaderAsync(req, res);
const header = await generateHeaderAsync(req, res, {});
const payload = { const payload = {
header: header, next: nconf.get('relative_path') + '/',
config: res.locals.config,
}; };
plugins.fireHook('filter:user.logout', payload); plugins.fireHook('filter:user.logout', payload);
if (req.body.noscript === 'true') {
return res.redirect(payload.next);
}
res.status(200).send(payload); res.status(200).send(payload);
} catch (err) { } catch (err) {
next(err); next(err);

@ -45,6 +45,7 @@ module.exports = function (middleware) {
}, next); }, next);
}, },
function (results, next) { function (results, next) {
res.locals.config = results.config;
// Return no arguments // Return no arguments
setImmediate(next); setImmediate(next);
}, },

@ -3,7 +3,6 @@
var os = require('os'); var os = require('os');
var winston = require('winston'); var winston = require('winston');
var _ = require('lodash'); var _ = require('lodash');
const nconf = require('nconf');
var meta = require('../meta'); var meta = require('../meta');
var languages = require('../languages'); var languages = require('../languages');
@ -55,12 +54,6 @@ module.exports = function (middleware) {
headers['X-Upstream-Hostname'] = os.hostname(); headers['X-Upstream-Hostname'] = os.hostname();
} }
// Ensure that the session is valid. This block guards against edge-cases where the server-side session has
// been deleted (but client-side cookie still exists)
if (req.uid > 0 && !req.session.meta && !res.get('Set-Cookie')) {
res.clearCookie(nconf.get('sessionKey'), meta.configs.cookie.get());
}
for (var key in headers) { for (var key in headers) {
if (headers.hasOwnProperty(key) && headers[key]) { if (headers.hasOwnProperty(key) && headers[key]) {
res.setHeader(key, headers[key]); res.setHeader(key, headers[key]);

@ -87,8 +87,7 @@ Auth.reloadRoutes = async function (params) {
// save returnTo for later usage in /register/complete // save returnTo for later usage in /register/complete
// passport seems to remove `req.session.returnTo` after it redirects // passport seems to remove `req.session.returnTo` after it redirects
req.session.registration.returnTo = req.session.returnTo; req.session.registration.returnTo = req.session.returnTo;
next();
}, function (req, res, next) {
passport.authenticate(strategy.name, function (err, user) { passport.authenticate(strategy.name, function (err, user) {
if (err) { if (err) {
delete req.session.registration; delete req.session.registration;

@ -145,7 +145,7 @@ function setupExpressApp(app) {
configureBodyParser(app); configureBodyParser(app);
app.use(cookieParser()); app.use(cookieParser(nconf.get('secret')));
const userAgentMiddleware = useragent.express(); const userAgentMiddleware = useragent.express();
app.use(function userAgent(req, res, next) { app.use(function userAgent(req, res, next) {
userAgentMiddleware(req, res, next); userAgentMiddleware(req, res, next);

Loading…
Cancel
Save