Reset existing sessions if password is updated

This commit is contained in:
Andris Reinman 2019-08-27 15:46:15 +03:00
parent 7fbf868b33
commit 7383276cda
4 changed files with 92 additions and 16 deletions

57
api.js
View file

@ -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();
}
}

View file

@ -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
});

View file

@ -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) {

View file

@ -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",