1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-19 05:09:43 +02:00

feat: Improve security of access tokens (#279)

Closes #275
This commit is contained in:
SimonTagne 2022-08-09 18:03:21 +02:00 committed by GitHub
parent 77ac2cf1b1
commit 2b4c2b0f49
40 changed files with 273 additions and 133 deletions

View file

@ -49,7 +49,7 @@ module.exports = {
}
return {
item: sails.helpers.utils.signToken(user.id),
item: sails.helpers.utils.createToken(user.id),
};
},
};

View file

@ -54,6 +54,7 @@ module.exports = {
{
avatarDirname: files[0].extra.dirname,
},
currentUser,
this.req,
);

View file

@ -69,7 +69,7 @@ module.exports = {
const values = _.pick(inputs, ['email']);
user = await sails.helpers.users
.updateOne(user, values, this.req)
.updateOne(user, values, currentUser, this.req)
.intercept('emailAlreadyInUse', () => Errors.EMAIL_ALREADY_IN_USE);
if (!user) {

View file

@ -60,12 +60,21 @@ module.exports = {
}
const values = _.pick(inputs, ['password']);
user = await sails.helpers.users.updateOne(user, values, this.req);
user = await sails.helpers.users.updateOne(user, values, currentUser, this.req);
if (!user) {
throw Errors.USER_NOT_FOUND;
}
if (user.id === currentUser.id) {
const accessToken = sails.helpers.utils.createToken(user.id, user.passwordUpdatedAt);
return {
accessToken,
item: user,
};
}
return {
item: user,
};

View file

@ -71,7 +71,7 @@ module.exports = {
const values = _.pick(inputs, ['username']);
user = await sails.helpers.users
.updateOne(user, values, this.req)
.updateOne(user, values, currentUser, this.req)
.intercept('usernameAlreadyInUse', () => Errors.USERNAME_ALREADY_IN_USE);
if (!user) {

View file

@ -75,7 +75,7 @@ module.exports = {
'subscribeToOwnCards',
]);
user = await sails.helpers.users.updateOne(user, values, this.req);
user = await sails.helpers.users.updateOne(user, values, currentUser, this.req);
if (!user) {
throw Errors.USER_NOT_FOUND;

View file

@ -1,6 +1,7 @@
const path = require('path');
const bcrypt = require('bcrypt');
const rimraf = require('rimraf');
const { v4: uuid } = require('uuid');
module.exports = {
inputs: {
@ -35,6 +36,10 @@ module.exports = {
},
required: true,
},
user: {
type: 'ref',
required: true,
},
request: {
type: 'ref',
},
@ -54,8 +59,10 @@ module.exports = {
let isOnlyPasswordChange = false;
if (!_.isUndefined(inputs.values.password)) {
// eslint-disable-next-line no-param-reassign
inputs.values.password = bcrypt.hashSync(inputs.values.password, 10);
Object.assign(inputs.values, {
password: bcrypt.hashSync(inputs.values.password, 10),
passwordChangedAt: new Date().toUTCString(),
});
if (Object.keys(inputs.values).length === 1) {
isOnlyPasswordChange = true;
@ -103,6 +110,29 @@ module.exports = {
}
}
if (!_.isUndefined(inputs.values.password)) {
sails.sockets.broadcast(
`user:${user.id}`,
'userDelete', // TODO: introduce separate event
{
item: user,
},
inputs.request,
);
if (user.id === inputs.user.id && inputs.request && inputs.request.isSocket) {
const tempRoom = uuid();
sails.sockets.addRoomMembersToRooms(`user:${user.id}`, tempRoom, () => {
sails.sockets.leave(inputs.request, tempRoom, () => {
sails.sockets.leaveAll(tempRoom);
});
});
} else {
sails.sockets.leaveAll(`user:${user.id}`);
}
}
if (!isOnlyPasswordChange) {
/* const projectIds = await sails.helpers.users.getManagerProjectIds(user.id);

View file

@ -0,0 +1,29 @@
const jwt = require('jsonwebtoken');
module.exports = {
sync: true,
inputs: {
subject: {
type: 'json',
required: true,
},
issuedAt: {
type: 'ref',
},
},
fn(inputs) {
const { issuedAt = new Date() } = inputs;
const iat = Math.floor(issuedAt / 1000);
return jwt.sign(
{
iat,
sub: inputs.subject,
exp: iat + sails.config.custom.tokenExpiresIn * 24 * 60 * 60,
},
sails.config.session.secret,
);
},
};

View file

@ -1,16 +0,0 @@
const jwt = require('jsonwebtoken');
module.exports = {
sync: true,
inputs: {
payload: {
type: 'json',
required: true,
},
},
fn(inputs) {
return jwt.sign(inputs.payload, sails.config.session.secret);
},
};

View file

@ -15,10 +15,16 @@ module.exports = {
},
fn(inputs) {
let payload;
try {
return jwt.verify(inputs.token, sails.config.session.secret);
payload = jwt.verify(inputs.token, sails.config.session.secret);
} catch (error) {
throw 'invalidToken';
}
return {
subject: payload.sub,
issuedAt: new Date(payload.iat * 1000),
};
},
};

View file

@ -10,15 +10,20 @@ module.exports = function defineCurrentUserHook(sails) {
const TOKEN_PATTERN = /^Bearer /;
const getUser = async (accessToken) => {
let id;
let payload;
try {
id = sails.helpers.utils.verifyToken(accessToken);
payload = sails.helpers.utils.verifyToken(accessToken);
} catch (error) {
return null;
}
return sails.helpers.users.getOne(id);
const user = await sails.helpers.users.getOne(payload.subject);
if (user && user.passwordChangedAt > payload.issuedAt) {
return null;
}
return user;
};
return {
@ -32,19 +37,23 @@ module.exports = function defineCurrentUserHook(sails) {
routes: {
before: {
'/*': {
'/api/*': {
async fn(req, res, next) {
let accessToken;
if (req.headers.authorization) {
if (TOKEN_PATTERN.test(req.headers.authorization)) {
accessToken = req.headers.authorization.replace(TOKEN_PATTERN, '');
}
} else if (req.cookies.accessToken) {
accessToken = req.cookies.accessToken;
const { authorization: authorizationHeader } = req.headers;
if (authorizationHeader && TOKEN_PATTERN.test(authorizationHeader)) {
const accessToken = authorizationHeader.replace(TOKEN_PATTERN, '');
req.currentUser = await getUser(accessToken);
}
if (accessToken) {
req.currentUser = await getUser(accessToken);
return next();
},
},
'/attachments/*': {
async fn(req, res, next) {
if (req.cookies.accessToken) {
req.currentUser = await getUser(req.cookies.accessToken);
}
return next();

View file

@ -67,6 +67,10 @@ module.exports = {
type: 'ref',
columnName: 'deleted_at',
},
passwordChangedAt: {
type: 'ref',
columnName: 'password_changed_at',
},
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
@ -102,7 +106,7 @@ module.exports = {
customToJSON() {
return {
..._.omit(this, ['password', 'avatarDirname']),
..._.omit(this, ['password', 'avatarDirname', 'passwordChangedAt']),
avatarUrl:
this.avatarDirname &&
`${sails.config.custom.userAvatarsUrl}/${this.avatarDirname}/square-100.jpg`,