Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PB-267]: Feat/Add lastPasswordChangeAt to users #382

Merged
merged 14 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions migrations/20231115141235-add-lastPasswordChangedAt-to-users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use strict';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn('users', 'last_password_changed_at', {
type: Sequelize.DATE,
allowNull: true,
defaultValue: null,
});
},

async down(queryInterface) {
await queryInterface.removeColumn('users', 'last_password_changed_at');
},
};
50 changes: 49 additions & 1 deletion src/app/middleware/passport.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,60 @@ const passport = require('passport');
const passportAuth = passport.authenticate('jwt', { session: false });

function Sign(data, secret, expires = false) {
const token = expires ? jwt.sign({ email: data }, secret, { expiresIn: '14d' }) : jwt.sign(data, secret);
const token = expires ?
jwt.sign({ email: data, iat: getDefaultIAT() }, secret, { expiresIn: '14d' }) :
jwt.sign(Object.assign(data, { iat: getDefaultIAT() }), secret);
return token;
}

function SignWithFutureIAT(data, secret) {
return jwt.sign({ email: data, iat: getFutureIAT() }, secret, { expiresIn: '14d' });
}

function SignNewTokenWithFutureIAT(data, secret, expires = false) {
const futureIat = getFutureIAT();
return expires
? jwt.sign(getNewTokenPayload(data, futureIat), secret, { expiresIn: '14d' })
: jwt.sign(getNewTokenPayload(data, futureIat), secret);
}

function SignNewToken(data, secret, expires = false) {
const token = expires ?
jwt.sign(getNewTokenPayload(data), secret, { expiresIn: '14d' }) :
jwt.sign(getNewTokenPayload(data), secret);
return token;
}

function getNewTokenPayload(userData, customIat) {
return {
payload: {
uuid: userData.uuid,
email: userData.email,
name: userData.name,
lastname: userData.lastname,
username: userData.username,
sharedWorkspace: true,
networkCredentials: {
user: userData.bridgeUser,
pass: userData.userId,
},
},
iat: customIat ?? getDefaultIAT(),
};
}

function getDefaultIAT() {
return Math.floor(Date.now() / 1000);
}

function getFutureIAT() {
return Math.floor(Date.now() / 1000) + 60;
}

module.exports = {
passportAuth,
Sign,
SignNewToken,
SignWithFutureIAT,
SignNewTokenWithFutureIAT,
};
6 changes: 6 additions & 0 deletions src/app/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface UserAttributes {
tempKey: string;
avatar: string;
emailVerified: boolean;
lastPasswordChangedAt: Date;
}

export type UserModel = ModelDefined<UserAttributes, UserAttributes>;
Expand Down Expand Up @@ -144,6 +145,11 @@ export default (database: Sequelize): UserModel => {
type: DataTypes.BOOLEAN,
defaultValue: false,
},
lastPasswordChangedAt: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: null,
},
},
{
tableName: 'users',
Expand Down
24 changes: 4 additions & 20 deletions src/app/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Router, Request, Response } from 'express';
import createHttpError from 'http-errors';
import speakeasy from 'speakeasy';
import { UserAttributes } from '../models/user';
import { passportAuth, Sign } from '../middleware/passport';
import { passportAuth, Sign, SignNewToken } from '../middleware/passport';
import Config from '../../config/config';
import { AuthorizedUser } from './types';
import { HttpError } from 'http-errors';
Expand Down Expand Up @@ -155,7 +155,7 @@ export class AuthController {
this.service.User.UpdateAccountActivity(req.body.email);
const userBucket = await this.service.User.GetUserBucket(userData);

const newToken = Sign(this.getNewTokenPayload(userData), this.config.get('secrets').JWT);
const newToken = SignNewToken(userData, this.config.get('secrets').JWT);
const keyExists = await this.service.KeyServer.keysExists(userData);

if (!keyExists && req.body.publicKey) {
Expand Down Expand Up @@ -200,6 +200,7 @@ export class AuthController {
backupsBucket: userData.backupsBucket,
avatar: userData.avatar ? await this.service.User.getSignedAvatarUrl(userData.avatar) : null,
emailVerified: userData.emailVerified,
lastPasswordChangedAt: userData.lastPasswordChangedAt,
};

const userTeam = null;
Expand All @@ -216,28 +217,11 @@ export class AuthController {

async getNewToken(req: Request, res: Response) {
const authRequest = req as Request & { user: UserAttributes };
const newToken = Sign(this.getNewTokenPayload(authRequest.user), this.config.get('secrets').JWT);
const newToken = SignNewToken(authRequest.user, this.config.get('secrets').JWT);

return res.status(200).json({ newToken });
}

private getNewTokenPayload(userData: any) {
return {
payload: {
uuid: userData.uuid,
email: userData.email,
name: userData.name,
lastname: userData.lastname,
username: userData.username,
sharedWorkspace: true,
networkCredentials: {
user: userData.bridgeUser,
pass: userData.userId,
},
},
};
}

async areCredentialsCorrect(req: Request, res: Response) {
if (!req.query.hashedPassword) throw createHttpError(400, 'Query params must contain the hashedPassword property');

Expand Down
1 change: 1 addition & 0 deletions src/app/routes/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export default (router: Router, service: any, App: any): Router => {
backupsBucket: userData.backupsBucket,
avatar: userData.avatar ? await service.User.getSignedAvatarUrl(userData.avatar) : null,
emailVerified: userData.emailVerified,
lastPasswordChangedAt: userData.lastPasswordChangedAt,
};

res.status(200).json({ user, token });
Expand Down
9 changes: 7 additions & 2 deletions src/app/routes/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import {
FileAlreadyExistsError,
FileWithNameAlreadyExistsError
} from '../services/errors/FileWithNameAlreadyExistsError';
import { FolderAlreadyExistsError, FolderWithNameAlreadyExistsError } from '../services/errors/FolderWithNameAlreadyExistsError';
import {
FolderAlreadyExistsError,
FolderWithNameAlreadyExistsError
} from '../services/errors/FolderWithNameAlreadyExistsError';
import * as resourceSharingMiddlewareBuilder from '../middleware/resource-sharing.middleware';
import {validate } from 'uuid';

Expand Down Expand Up @@ -94,7 +97,9 @@ export class StorageController {
return res.status(409).send({ error: err.message });
}
this.logger.error(
`[FILE/CREATE] ERROR: ${(err as Error).message}, BODY ${JSON.stringify(file)}, STACK: ${(err as Error).stack} USER: ${behalfUser.email}`,
`[FILE/CREATE] ERROR: ${(err as Error).message}, BODY ${
JSON.stringify(file)
}, STACK: ${(err as Error).stack} USER: ${behalfUser.email}`,
);
res.status(500).send({ error: 'Internal Server Error' });
}
Expand Down
14 changes: 8 additions & 6 deletions src/app/routes/user.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const openpgp = require('openpgp');
const createHttpError = require('http-errors');
const { passportAuth, Sign } = require('../middleware/passport');
const { passportAuth, Sign, SignWithFutureIAT, SignNewTokenWithFutureIAT } = require('../middleware/passport');
const Logger = require('../../lib/logger').default;
const AnalyticsService = require('../../lib/analytics/AnalyticsService');
const { default: uploadAvatar } = require('../middleware/upload-avatar');
Expand All @@ -16,7 +16,9 @@ module.exports = (Router, Service, App) => {

Service.User.UpdatePasswordMnemonic(req.user, currentPassword, newPassword, newSalt, mnemonic, privateKey)
.then(() => {
res.status(200).send({});
const token = SignWithFutureIAT(req.user.email, App.config.get('secrets').JWT);
const newToken = SignNewTokenWithFutureIAT(req.user, App.config.get('secrets').JWT);
res.status(200).send({ token, newToken });
})
.catch((err) => {
res.status(500).send({ error: err.message });
Expand Down Expand Up @@ -156,10 +158,10 @@ module.exports = (Router, Service, App) => {
res.status(200).send({ token, user });
} catch (err) {
logger.error(
'Update user error %s: %s. STACK %s. BODY %s',
req.user.email,
err.message,
err.stack || 'NO STACK',
'Update user error %s: %s. STACK %s. BODY %s',
req.user.email,
err.message,
err.stack || 'NO STACK',
req.body
);
res.status(500).send({ error: 'Internal Server error' });
Expand Down
2 changes: 2 additions & 0 deletions src/app/services/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ module.exports = (Model, App) => {
password: newPassword,
mnemonic,
hKey: newSalt,
lastPasswordChangedAt: new Date()
},
{
where: { username: { [Op.eq]: user.email } },
Expand All @@ -364,6 +365,7 @@ module.exports = (Model, App) => {
user.hKey = newSalt;
user.mnemonic = oldMnemonic;
user.password = newPassword;
user.lastPasswordChangedAt = new Date();
await user.save();

const keys = await user.getKeyserver();
Expand Down
12 changes: 11 additions & 1 deletion src/config/initializers/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,17 @@ module.exports = (App, Config) => {
// const email = payload.email

App.services.User.FindUserObjByEmail(email)
.then((user) => done(null, user))
.then((user) => {
const userWithoutLastPasswordChangedAt = user.lastPasswordChangedAt === null;
const userWithLastPasswordChangedAtLowerThanToken =
user.lastPasswordChangedAt &&
Math.floor(new Date(user.lastPasswordChangedAt).getTime()) / 1000 < payload.iat;
if (userWithoutLastPasswordChangedAt || userWithLastPasswordChangedAtLowerThanToken) {
done(null, user);
} else {
done(null);
}
})
.catch((err) => {
done(err);
});
Expand Down
Loading