From 6f682250b68ca7123c98c4ed62e3287a192c968c Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sat, 10 Dec 2022 23:06:54 -0500 Subject: [PATCH] Begin reset password backend functionality --- .../controllers/membershipOrgController.ts | 4 +- backend/src/controllers/passwordController.ts | 181 +++++++++++++++++- backend/src/helpers/signup.ts | 2 +- backend/src/routes/password.ts | 44 ++++- .../organizationInvitation.handlebars | 2 +- .../src/templates/passwordReset.handlebars | 15 ++ .../templates/workspaceInvitation.handlebars | 2 +- 7 files changed, 239 insertions(+), 11 deletions(-) create mode 100644 backend/src/templates/passwordReset.handlebars diff --git a/backend/src/controllers/membershipOrgController.ts b/backend/src/controllers/membershipOrgController.ts index bc28049966..a2159bcd05 100644 --- a/backend/src/controllers/membershipOrgController.ts +++ b/backend/src/controllers/membershipOrgController.ts @@ -217,7 +217,7 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => { try { const { email, code } = req.body; - user = await User.findOne({ email }); + user = await User.findOne({ email }).select('+publicKey'); if (user && user?.publicKey) { // case: user has already completed account return res.status(403).send({ @@ -257,7 +257,7 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => { Sentry.setUser(null); Sentry.captureException(err); return res.status(400).send({ - error: 'Failed email magic link confirmation' + error: 'Failed email magic link verification for organization invitation' }); } diff --git a/backend/src/controllers/passwordController.ts b/backend/src/controllers/passwordController.ts index 86b5355dbc..0109dc6a51 100644 --- a/backend/src/controllers/passwordController.ts +++ b/backend/src/controllers/passwordController.ts @@ -1,11 +1,121 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; +import crypto from 'crypto'; const jsrp = require('jsrp'); import * as bigintConversion from 'bigint-conversion'; -import { User, BackupPrivateKey } from '../models'; +import { User, Token, BackupPrivateKey } from '../models'; +import { checkEmailVerification } from '../helpers/signup'; +import { createToken } from '../helpers/auth'; +import { sendMail } from '../helpers/nodemailer'; +import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../config'; const clientPublicKeys: any = {}; +/** + * Password reset step 1: Send email verification link to email [email] + * for account recovery. + * @param req + * @param res + * @returns + */ +export const emailPasswordReset = async (req: Request, res: Response) => { + let email: string; + try { + email = req.body.email; + + const user = await User.findOne({ email }).select('+publicKey'); + if (!user || !user?.publicKey) { + // case: user has already completed account + + return res.status(403).send({ + error: 'Failed to send email verification for password reset' + }); + } + + const token = crypto.randomBytes(16).toString('hex'); + + await Token.findOneAndUpdate( + { email }, + { + email, + token, + createdAt: new Date() + }, + { upsert: true, new: true } + ); + + await sendMail({ + template: 'passwordReset.handlebars', + subjectLine: 'Infisical password reset', + recipients: [email], + substitutions: { + email, + token, + callback_url: SITE_URL + '/password-reset' + } + }); + + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to send email for account recovery' + }); + } + + return res.status(200).send({ + message: `Sent an email for account recovery to ${email}` + }); +} + +/** + * Password reset step 2: Verify email verification link sent to email [email] + * @param req + * @param res + * @returns + */ +export const emailPasswordResetVerify = async (req: Request, res: Response) => { + let user, token; + try { + const { email, code } = req.body; + + user = await User.findOne({ email }).select('+publicKey'); + if (!user || !user?.publicKey) { + // case: user doesn't exist with email [email] or + // hasn't even completed their account + return res.status(403).send({ + error: 'Failed email verification for password reset' + }); + } + + await checkEmailVerification({ + email, + code + }); + + // generate temporary password-reset token + token = createToken({ + payload: { + userId: user._id.toString() + }, + expiresIn: JWT_SIGNUP_LIFETIME, + secret: JWT_SIGNUP_SECRET + }); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed email verification for password reset' + }); + } + + return res.status(200).send({ + message: 'Successfully verified email', + user, + token + }); +} + /** * Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol * @param req @@ -43,7 +153,7 @@ export const srp1 = async (req: Request, res: Response) => { } ); } catch (err) { - Sentry.setUser(null); + Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); return res.status(400).send({ error: 'Failed to start change password process' @@ -110,7 +220,7 @@ export const changePassword = async (req: Request, res: Response) => { } ); } catch (err) { - Sentry.setUser(null); + Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); return res.status(400).send({ error: 'Failed to change password. Try again?' @@ -180,10 +290,73 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => { } ); } catch (err) { - Sentry.setUser(null); + Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); return res.status(400).send({ message: 'Failed to update backup private key' }); } }; + +/** + * Return backup private key for user + * @param req + * @param res + * @returns + */ +export const getBackupPrivateKey = async (req: Request, res: Response) => { + let backupPrivateKey; + try { + backupPrivateKey = await BackupPrivateKey.findOne({ + user: req.user._id + }); + + if (!backupPrivateKey) throw new Error('Failed to find backup private key'); + } catch (err) { + Sentry.setUser({ email: req.user.email}); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get backup private key' + }); + } + + return res.status(200).send({ + backupPrivateKey + }); +} + +export const resetPassword = async (req: Request, res: Response) => { + try { + const { + encryptedPrivateKey, + iv, + tag, + salt, + verifier, + } = req.body; + + await User.findByIdAndUpdate( + req.user._id.toString(), + { + encryptedPrivateKey, + iv, + tag, + salt, + verifier + }, + { + new: true + } + ); + } catch (err) { + Sentry.setUser({ email: req.user.email}); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get backup private key' + }); + } + + return res.status(200).send({ + message: 'Successfully reset password' + }); +} \ No newline at end of file diff --git a/backend/src/helpers/signup.ts b/backend/src/helpers/signup.ts index 141bf455d5..8a201cb110 100644 --- a/backend/src/helpers/signup.ts +++ b/backend/src/helpers/signup.ts @@ -33,7 +33,7 @@ const sendEmailVerification = async ({ email }: { email: string }) => { // send mail await sendMail({ template: 'emailVerification.handlebars', - subjectLine: 'Infisical workspace invitation', + subjectLine: 'Infisical confirmation code', recipients: [email], substitutions: { code: token diff --git a/backend/src/routes/password.ts b/backend/src/routes/password.ts index 5d39eac283..955e532a0b 100644 --- a/backend/src/routes/password.ts +++ b/backend/src/routes/password.ts @@ -1,7 +1,7 @@ import express from 'express'; const router = express.Router(); import { body } from 'express-validator'; -import { requireAuth, validateRequest } from '../middleware'; +import { requireAuth, requireSignupAuth, validateRequest } from '../middleware'; import { passwordController } from '../controllers'; import { passwordLimiter } from '../helpers/rateLimiter'; @@ -27,6 +27,33 @@ router.post( passwordController.changePassword ); +// NEW +router.post( + '/email/password-reset', + passwordLimiter, + body('email').exists().trim().notEmpty(), + validateRequest, + passwordController.emailPasswordReset +); + +// NEW +router.post( + '/email/password-reset-verify', + passwordLimiter, + body('email').exists().trim().notEmpty().isEmail(), + body('code').exists().trim().notEmpty(), + validateRequest, + passwordController.emailPasswordResetVerify +); + +// NEW +router.get( + '/backup-private-key', + passwordLimiter, + requireSignupAuth, + passwordController.getBackupPrivateKey +); + router.post( '/backup-private-key', passwordLimiter, @@ -41,4 +68,17 @@ router.post( passwordController.createBackupPrivateKey ); -export default router; +// NEW +router.post( + '/password-reset', + requireSignupAuth, + body('encryptedPrivateKey').exists().trim().notEmpty(), // private key encrypted under new pwd + body('iv').exists().trim().notEmpty(), // new iv for private key + body('tag').exists().trim().notEmpty(), // new tag for private key + body('salt').exists().trim().notEmpty(), // part of new pwd + body('verifier').exists().trim().notEmpty(), // part of new pwd + validateRequest, + passwordController.resetPassword +); + +export default router; \ No newline at end of file diff --git a/backend/src/templates/organizationInvitation.handlebars b/backend/src/templates/organizationInvitation.handlebars index 6634091887..49cc96f38d 100644 --- a/backend/src/templates/organizationInvitation.handlebars +++ b/backend/src/templates/organizationInvitation.handlebars @@ -4,7 +4,7 @@ - Email Verification + Organization Invitation

Infisical

diff --git a/backend/src/templates/passwordReset.handlebars b/backend/src/templates/passwordReset.handlebars new file mode 100644 index 0000000000..1e629f6644 --- /dev/null +++ b/backend/src/templates/passwordReset.handlebars @@ -0,0 +1,15 @@ + + + + + + Account Recovery + + +

Infisical

+

Reset your password

+

Someone requested a password reset.

+ Reset password +

If you didn't initiate this request, please contact us immediately at team@infisical.com

+ + \ No newline at end of file diff --git a/backend/src/templates/workspaceInvitation.handlebars b/backend/src/templates/workspaceInvitation.handlebars index 22f63efc99..252452ce58 100644 --- a/backend/src/templates/workspaceInvitation.handlebars +++ b/backend/src/templates/workspaceInvitation.handlebars @@ -3,7 +3,7 @@ - Email Verification + Project Invitation

Infisical