Skip to content

Commit

Permalink
Merge pull request #382 from internxt/feat/add-lastPasswordChangeAt-t…
Browse files Browse the repository at this point in the history
…o-users

[PB-267]: Feat/Add lastPasswordChangeAt to users
  • Loading branch information
larry-internxt authored Jan 29, 2024
2 parents d5deb1e + 99c19a7 commit a2b00c5
Show file tree
Hide file tree
Showing 9 changed files with 104 additions and 30 deletions.
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);

This comment has been minimized.

Copy link
@JoanVicens

JoanVicens Jan 30, 2024

Contributor

Hey, I think this is breaking the /refresh endpoint, the data param is a string so cannot be used in Object.assign.

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 @@ -199,6 +199,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 @@ -215,28 +216,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 @@ -105,6 +105,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: 'File already exists' });
}
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 @@ -357,6 +357,7 @@ module.exports = (Model, App) => {
password: newPassword,
mnemonic,
hKey: newSalt,
lastPasswordChangedAt: new Date()
},
{
where: { username: { [Op.eq]: user.email } },
Expand All @@ -371,6 +372,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 @@ -165,7 +165,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

0 comments on commit a2b00c5

Please sign in to comment.