diff --git a/app/api/users/generateUnlockCode.ts b/app/api/users/generateUnlockCode.ts new file mode 100644 index 0000000000..ba3b669be2 --- /dev/null +++ b/app/api/users/generateUnlockCode.ts @@ -0,0 +1,3 @@ +import crypto from 'crypto'; + +export const generateUnlockCode = () => crypto.randomBytes(32).toString('hex'); diff --git a/app/api/users/passwordRecoveriesModel.js b/app/api/users/passwordRecoveriesModel.js index 9123aaaa7a..a92ae1f841 100644 --- a/app/api/users/passwordRecoveriesModel.js +++ b/app/api/users/passwordRecoveriesModel.js @@ -2,9 +2,12 @@ import mongoose from 'mongoose'; import { instanceModel } from 'api/odm'; +const ONE_DAY = 60 * 60 * 24; + const schema = new mongoose.Schema({ key: String, user: { type: mongoose.Schema.Types.ObjectId, ref: 'users' }, + expiresAt: { type: Date, expires: ONE_DAY, default: Date.now() }, }); export default instanceModel('passwordrecoveries', schema); diff --git a/app/api/users/specs/users.spec.js b/app/api/users/specs/users.spec.js index ebae63a4d1..15a8e84ee0 100644 --- a/app/api/users/specs/users.spec.js +++ b/app/api/users/specs/users.spec.js @@ -2,7 +2,6 @@ /* eslint-disable max-statements */ import { createError } from 'api/utils'; -import SHA256 from 'crypto-js/sha256'; import crypto from 'crypto'; import mailer from 'api/utils/mailer'; import db from 'api/utils/testing_db'; @@ -25,6 +24,7 @@ import fixtures, { import users from '../users.js'; import passwordRecoveriesModel from '../passwordRecoveriesModel'; import usersModel from '../usersModel'; +import * as unlockCode from '../generateUnlockCode'; describe('Users', () => { beforeEach(async () => { @@ -510,10 +510,11 @@ describe('Users', () => { jest.restoreAllMocks(); jest.spyOn(mailer, 'send').mockImplementation(async () => Promise.resolve('OK')); jest.spyOn(Date, 'now').mockReturnValue(1000); + jest.spyOn(unlockCode, 'generateUnlockCode').mockReturnValue('ABCDEF1234'); }); it('should find the matching email create a recover password doc in the database and send an email', async () => { - const key = SHA256(`test@email.com${1000}`).toString(); + const key = unlockCode.generateUnlockCode(); const settings = await settingsModel.get(); const response = await users.recoverPassword('test@email.com', 'domain'); expect(response).toBe('OK'); @@ -524,13 +525,13 @@ describe('Users', () => { from: emailSender, to: 'test@email.com', subject: 'Password set', - text: `To set your password click on the following link:\ndomain/setpassword/${key}`, + text: `To set your password click on the following link:\ndomain/setpassword/${key}\nThis link will be valid for 24 hours.`, }; expect(mailer.send).toHaveBeenCalledWith(expectedMailOptions); }); it('should personalize the mail if recover password process is part of a newly created user', async () => { - const key = SHA256(`peter@parker.com${1000}`).toString(); + const key = unlockCode.generateUnlockCode(); const settings = await settingsModel.get(); const newUser = await users.newUser( @@ -590,7 +591,7 @@ describe('Users', () => { describe('when the user does not exist with that email', () => { it('should not create the entry in the database, should not send a mail, and return an error.', async () => { jest.spyOn(Date, 'now').mockReturnValue(1000); - const key = SHA256(`false@email.com${1000}`).toString(); + const key = unlockCode.generateUnlockCode(); let response; try { response = await users.recoverPassword('false@email.com'); diff --git a/app/api/users/users.js b/app/api/users/users.js index c0074007ed..2e8dff14a4 100644 --- a/app/api/users/users.js +++ b/app/api/users/users.js @@ -1,5 +1,4 @@ import SHA256 from 'crypto-js/sha256'; -import crypto from 'crypto'; import { createError } from 'api/utils'; import random from 'shared/uniqueID'; @@ -15,16 +14,15 @@ import mailer from '../utils/mailer'; import model from './usersModel'; import passwordRecoveriesModel from './passwordRecoveriesModel'; import settings from '../settings/settings'; +import { generateUnlockCode } from './generateUnlockCode'; const MAX_FAILED_LOGIN_ATTEMPTS = 6; -const generateUnlockCode = () => crypto.randomBytes(32).toString('hex'); - function conformRecoverText(options, _settings, domain, key, user) { const response = {}; if (!options.newUser) { response.subject = 'Password set'; - response.text = `To set your password click on the following link:\n${domain}/setpassword/${key}`; + response.text = `To set your password click on the following link:\n${domain}/setpassword/${key}\nThis link will be valid for 24 hours.`; } if (options.newUser) { @@ -34,7 +32,8 @@ function conformRecoverText(options, _settings, domain, key, user) { `The administrators of ${siteName} have created an account for you under the user name:\n` + `${user.username}\n\n` + 'To complete this process, please create a strong password by clicking on the following link:\n' + - `${domain}/setpassword/${key}?createAccount=true\n\n` + + `${domain}/setpassword/${key}?createAccount=true\n` + + 'This link will be valid for 24 hours.\n\n' + 'For more information about the Uwazi platform, visit https://www.uwazi.io.\n\nThank you!\nUwazi team'; const htmlLink = `${domain}/setpassword/${key}?createAccount=true`; @@ -288,7 +287,7 @@ export default { }, recoverPassword(email, domain, options = {}) { - const key = SHA256(email + Date.now()).toString(); + const key = generateUnlockCode(); return Promise.all([model.get({ email }), settings.get()]).then(([_user, _settings]) => { const user = _user[0]; if (user) { diff --git a/package.json b/package.json index bb86c55b97..65720488a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uwazi", - "version": "1.168.0-rc4", + "version": "1.168.0-rc5", "description": "Uwazi is a free, open-source solution for organising, analysing and publishing your documents.", "keywords": [ "react"