From 7383276cda3d8b6c42fce5fd909db5b867e42241 Mon Sep 17 00:00:00 2001 From: Andris Reinman Date: Tue, 27 Aug 2019 15:46:15 +0300 Subject: [PATCH] Reset existing sessions if password is updated --- api.js | 57 ++++++++++++++++++++++++++++++++++++++------- lib/api/users.js | 9 +++++++ lib/user-handler.js | 30 ++++++++++++++++++++++-- package.json | 12 +++++----- 4 files changed, 92 insertions(+), 16 deletions(-) diff --git a/api.js b/api.js index 732cde40..106bd1fe 100644 --- a/api.js +++ b/api.js @@ -17,6 +17,7 @@ const crypto = require('crypto'); const Gelf = require('gelf'); const os = require('os'); const util = require('util'); +const ObjectID = require('mongodb').ObjectID; const usersRoutes = require('./lib/api/users'); const addressesRoutes = require('./lib/api/addresses'); @@ -229,15 +230,27 @@ server.use( } if (tokenData && tokenData.user && tokenData.role && config.api.roles[tokenData.role]) { + let signData; + if ('authVersion' in tokenData) { + // cast value to number + tokenData.authVersion = Number(tokenData.authVersion) || 0; + signData = { + token: accessToken, + user: tokenData.user, + authVersion: tokenData.authVersion, + role: tokenData.role + }; + } else { + signData = { + token: accessToken, + user: tokenData.user, + role: tokenData.role + }; + } + let signature = crypto .createHmac('sha256', config.api.accessControl.secret) - .update( - JSON.stringify({ - token: accessToken, - user: tokenData.user, - role: tokenData.role - }) - ) + .update(JSON.stringify(signData)) .digest('hex'); if (signature !== tokenData.s) { @@ -268,9 +281,14 @@ server.use( req.role = tokenData.role; req.user = tokenData.user; + // make a reference to original method, otherwise might be overrided + let setAuthToken = userHandler.setAuthToken.bind(userHandler); + req.accessToken = { hash: tokenHash, - user: tokenData.user + user: tokenData.user, + // if called then refreshes token data for current hash + update: async () => setAuthToken(tokenData.user, accessToken) }; } else { // expired token, clear it @@ -296,6 +314,29 @@ server.use( return fail(); } + if (/^[0-9a-f]{24}$/i.test(req.user)) { + let tokenAuthVersion = Number(tokenData.authVersion) || 0; + let userData = await db.users.collection('users').findOne( + { + _id: new ObjectID(req.user) + }, + { projection: { authVersion: true } } + ); + let userAuthVersion = Number(userData && userData.authVersion) || 0; + if (!userData || tokenAuthVersion < userAuthVersion) { + // unknown user or expired session + try { + await db.redis + .multi() + .del('tn:token:' + tokenHash) + .exec(); + } catch (err) { + // ignore + } + return fail(); + } + } + return next(); } } diff --git a/lib/api/users.js b/lib/api/users.js index abb4f9b0..dae10048 100644 --- a/lib/api/users.js +++ b/lib/api/users.js @@ -1510,6 +1510,15 @@ module.exports = (db, server, userHandler) => { return next(); } + if (success && result.value.password && req.accessToken && typeof req.accessToken.update === 'function') { + try { + // update access token data for current session after password change + await req.accessToken.update(); + } catch (err) { + // ignore + } + } + res.json({ success }); diff --git a/lib/user-handler.js b/lib/user-handler.js index b25cbd91..69509131 100644 --- a/lib/user-handler.js +++ b/lib/user-handler.js @@ -1266,6 +1266,9 @@ class UserHandler { pendingSeed: '', pendingSeedChanged: false, + // incremented every time password is changed + authVersion: 1, + // default email address address: '', // set this later @@ -2897,6 +2900,13 @@ class UserHandler { updateQuery.$push = $push; } + if (passwordChanged) { + if (!updateQuery.$inc) { + updateQuery.$inc = {}; + } + updateQuery.$inc.authVersion = 1; + } + let result; try { result = await this.users.collection('users').findOneAndUpdate( @@ -2957,6 +2967,8 @@ class UserHandler { sess: data.sess, ip: data.ip }); + + await this.logout(user, 'User password was changed'); } catch (err) { // ignore } @@ -3238,8 +3250,7 @@ class UserHandler { }; } - async generateAuthToken(user) { - let accessToken = crypto.randomBytes(20).toString('hex'); + async setAuthToken(user, accessToken) { let tokenHash = crypto .createHash('sha256') .update(accessToken) @@ -3247,11 +3258,20 @@ class UserHandler { let key = 'tn:token:' + tokenHash; let ttl = config.api.accessControl.tokenTTL || consts.ACCESS_TOKEN_DEFAULT_TTL; + let userData = await this.users.collection('users').findOne( + { + _id: new ObjectID(user) + }, + { projection: { authVersion: true } } + ); + let authVersion = Number(userData && userData.authVersion) || 0; + let tokenData = { user: user.toString(), role: 'user', created: Date.now(), ttl, + authVersion, // signature s: crypto .createHmac('sha256', config.api.accessControl.secret) @@ -3259,6 +3279,7 @@ class UserHandler { JSON.stringify({ token: accessToken, user: user.toString(), + authVersion, role: 'user' }) ) @@ -3273,6 +3294,11 @@ class UserHandler { return accessToken; } + + async generateAuthToken(user) { + let accessToken = crypto.randomBytes(20).toString('hex'); + return await this.setAuthToken(user, accessToken); + } } function rateLimitResponse(res) { diff --git a/package.json b/package.json index 18f4a917..2a77fdcc 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,9 @@ "apidoc": "0.17.7", "browserbox": "0.9.1", "chai": "4.2.0", - "eslint": "6.1.0", + "eslint": "6.2.2", "eslint-config-nodemailer": "1.2.0", - "eslint-config-prettier": "6.0.0", + "eslint-config-prettier": "6.1.0", "grunt": "1.0.4", "grunt-cli": "1.3.2", "grunt-eslint": "22.0.0", @@ -56,12 +56,12 @@ "mailsplit": "4.4.1", "mobileconfig": "2.3.1", "mongo-cursor-pagination": "7.1.0", - "mongodb": "3.2.7", + "mongodb": "3.3.1", "mongodb-extended-json": "1.10.1", "node-forge": "0.8.5", "nodemailer": "6.3.0", "npmlog": "4.1.2", - "openpgp": "4.5.5", + "openpgp": "4.6.0", "pem": "1.14.2", "pwnedpasswords": "1.0.4", "qrcode": "1.4.1", @@ -72,9 +72,9 @@ "speakeasy": "2.0.0", "u2f": "0.1.3", "utf7": "1.0.2", - "uuid": "3.3.2", + "uuid": "3.3.3", "wild-config": "1.4.0", - "yargs": "13.3.0" + "yargs": "14.0.0" }, "repository": { "type": "git",