From 266a7626417acdcdef77b6e9382a4344a73e6a3f Mon Sep 17 00:00:00 2001 From: Simon Tagne <40598597+SimonTagne@users.noreply.github.com> Date: Mon, 8 Aug 2022 14:25:44 +0200 Subject: [PATCH] fix: Invalidate access tokens on password change --- client/src/sagas/core/services/users.js | 8 ++++++- client/src/utils/access-token-storage.js | 3 +++ .../api/controllers/users/update-password.js | 21 +++++++++++++++++++ server/api/helpers/users/update-one.js | 2 ++ server/api/helpers/utils/sign-token.js | 2 +- server/api/hooks/current-user/index.js | 17 +++++++++++++-- server/api/models/User.js | 5 +++++ ...ssword_changed_at_to_user_account_table.js | 11 ++++++++++ 8 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 server/db/migrations/20220803221221_add_password_changed_at_to_user_account_table.js diff --git a/client/src/sagas/core/services/users.js b/client/src/sagas/core/services/users.js index 12f1f96..f4ed448 100644 --- a/client/src/sagas/core/services/users.js +++ b/client/src/sagas/core/services/users.js @@ -5,6 +5,7 @@ import request from '../request'; import selectors from '../../../selectors'; import actions from '../../../actions'; import api from '../../../api'; +import { setAccessToken } from '../../../utils/access-token-storage'; export function* createUser(data) { yield put(actions.createUser(data)); @@ -109,13 +110,18 @@ export function* updateUserPassword(id, data) { yield put(actions.updateUserPassword(id, data)); let user; + let accessToken; try { - ({ item: user } = yield call(request, api.updateUserPassword, id, data)); + ({ item: user, accessToken } = yield call(request, api.updateUserPassword, id, data)); } catch (error) { yield put(actions.updateUserPassword.failure(id, error)); return; } + if (accessToken !== undefined) { + yield call(setAccessToken, accessToken); + } + yield put(actions.updateUserPassword.success(user)); } diff --git a/client/src/utils/access-token-storage.js b/client/src/utils/access-token-storage.js index a4f208c..37f786a 100755 --- a/client/src/utils/access-token-storage.js +++ b/client/src/utils/access-token-storage.js @@ -1,6 +1,7 @@ import Cookies from 'js-cookie'; import Config from '../constants/Config'; +import socket from '../api/socket'; export const setAccessToken = (accessToken) => { Cookies.set(Config.ACCESS_TOKEN_KEY, accessToken, { @@ -11,6 +12,8 @@ export const setAccessToken = (accessToken) => { Cookies.set(Config.ACCESS_TOKEN_VERSION_KEY, Config.ACCESS_TOKEN_VERSION, { expires: Config.ACCESS_TOKEN_EXPIRES, }); + + socket.headers = { Cookie: document.cookie }; }; export const getAccessToken = () => { diff --git a/server/api/controllers/users/update-password.js b/server/api/controllers/users/update-password.js index 699af36..5f90924 100644 --- a/server/api/controllers/users/update-password.js +++ b/server/api/controllers/users/update-password.js @@ -66,6 +66,27 @@ module.exports = { throw Errors.USER_NOT_FOUND; } + // Disconnect all sockets from this user except the current one + const tempRoom = `temp:${user.id}`; + sails.sockets.addRoomMembersToRooms(`user:${user.id}`, tempRoom, () => { + if (currentUser.id === user.id && this.req.isSocket) { + sails.sockets.leave(this.req, tempRoom, () => { + sails.sockets.leaveAll(tempRoom); + }); + } else { + sails.sockets.leaveAll(tempRoom); + } + }); + + if (currentUser.id === user.id) { + const accessToken = sails.helpers.utils.signToken(user.id); + + return { + accessToken, + item: user, + }; + } + return { item: user, }; diff --git a/server/api/helpers/users/update-one.js b/server/api/helpers/users/update-one.js index 8ceb2d1..b9f49a6 100644 --- a/server/api/helpers/users/update-one.js +++ b/server/api/helpers/users/update-one.js @@ -56,6 +56,8 @@ module.exports = { if (!_.isUndefined(inputs.values.password)) { // eslint-disable-next-line no-param-reassign inputs.values.password = bcrypt.hashSync(inputs.values.password, 10); + // eslint-disable-next-line no-param-reassign + inputs.values.passwordChangedAt = new Date(); if (Object.keys(inputs.values).length === 1) { isOnlyPasswordChange = true; diff --git a/server/api/helpers/utils/sign-token.js b/server/api/helpers/utils/sign-token.js index 90fc792..92d22a2 100644 --- a/server/api/helpers/utils/sign-token.js +++ b/server/api/helpers/utils/sign-token.js @@ -11,6 +11,6 @@ module.exports = { }, fn(inputs) { - return jwt.sign(inputs.payload, sails.config.session.secret); + return jwt.sign({ sub: inputs.payload }, sails.config.session.secret); }, }; diff --git a/server/api/hooks/current-user/index.js b/server/api/hooks/current-user/index.js index 3268ca4..138d254 100644 --- a/server/api/hooks/current-user/index.js +++ b/server/api/hooks/current-user/index.js @@ -11,14 +11,27 @@ module.exports = function defineCurrentUserHook(sails) { const getUser = async (accessToken) => { let id; + let iat; + let decodedToken; try { - id = sails.helpers.utils.verifyToken(accessToken); + decodedToken = sails.helpers.utils.verifyToken(accessToken); } catch (error) { return null; } - return sails.helpers.users.getOne(id); + if (_.isString(decodedToken)) { + id = decodedToken; + iat = 1; + } else { + id = decodedToken.sub; + iat = decodedToken.iat; + } + + return sails.helpers.users.getOne({ + id, + passwordChangedAt: { '<=': new Date((iat + 1) * 1000) }, + }); }; return { diff --git a/server/api/models/User.js b/server/api/models/User.js index b545fbb..fea4f83 100755 --- a/server/api/models/User.js +++ b/server/api/models/User.js @@ -67,6 +67,11 @@ module.exports = { type: 'ref', columnName: 'deleted_at', }, + passwordChangedAt: { + type: 'ref', + columnName: 'password_changed_at', + defaultsTo: new Date(0).toUTCString(), + }, // ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗ // ║╣ ║║║╠╩╗║╣ ║║╚═╗ diff --git a/server/db/migrations/20220803221221_add_password_changed_at_to_user_account_table.js b/server/db/migrations/20220803221221_add_password_changed_at_to_user_account_table.js new file mode 100644 index 0000000..50a1ef3 --- /dev/null +++ b/server/db/migrations/20220803221221_add_password_changed_at_to_user_account_table.js @@ -0,0 +1,11 @@ +module.exports.up = async (knex) => + knex.schema.table('user_account', (table) => { + /* Columns */ + + table.timestamp('password_changed_at', true).defaultsTo(new Date(0).toUTCString()); + }); + +module.exports.down = async (knex) => + knex.schema.table('user_account', (table) => { + table.dropColumn('password_changed_at'); + });