mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 06:28:11 +00:00
Merge pull request #114 from Infisical/account-recovery
Begin reset password backend functionality
This commit is contained in:
@@ -217,7 +217,7 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const { email, code } = req.body;
|
const { email, code } = req.body;
|
||||||
|
|
||||||
user = await User.findOne({ email });
|
user = await User.findOne({ email }).select('+publicKey');
|
||||||
if (user && user?.publicKey) {
|
if (user && user?.publicKey) {
|
||||||
// case: user has already completed account
|
// case: user has already completed account
|
||||||
return res.status(403).send({
|
return res.status(403).send({
|
||||||
@@ -257,7 +257,7 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
|||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
error: 'Failed email magic link confirmation'
|
error: 'Failed email magic link verification for organization invitation'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,121 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
|
import crypto from 'crypto';
|
||||||
const jsrp = require('jsrp');
|
const jsrp = require('jsrp');
|
||||||
import * as bigintConversion from 'bigint-conversion';
|
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 = {};
|
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
|
* Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
|
||||||
* @param req
|
* @param req
|
||||||
@@ -43,7 +153,7 @@ export const srp1 = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
error: 'Failed to start change password process'
|
error: 'Failed to start change password process'
|
||||||
@@ -110,7 +220,7 @@ export const changePassword = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
error: 'Failed to change password. Try again?'
|
error: 'Failed to change password. Try again?'
|
||||||
@@ -180,10 +290,73 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to update backup private key'
|
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'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ const sendEmailVerification = async ({ email }: { email: string }) => {
|
|||||||
// send mail
|
// send mail
|
||||||
await sendMail({
|
await sendMail({
|
||||||
template: 'emailVerification.handlebars',
|
template: 'emailVerification.handlebars',
|
||||||
subjectLine: 'Infisical workspace invitation',
|
subjectLine: 'Infisical confirmation code',
|
||||||
recipients: [email],
|
recipients: [email],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
code: token
|
code: token
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import { body } from 'express-validator';
|
import { body } from 'express-validator';
|
||||||
import { requireAuth, validateRequest } from '../middleware';
|
import { requireAuth, requireSignupAuth, validateRequest } from '../middleware';
|
||||||
import { passwordController } from '../controllers';
|
import { passwordController } from '../controllers';
|
||||||
import { passwordLimiter } from '../helpers/rateLimiter';
|
import { passwordLimiter } from '../helpers/rateLimiter';
|
||||||
|
|
||||||
@@ -27,6 +27,33 @@ router.post(
|
|||||||
passwordController.changePassword
|
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(
|
router.post(
|
||||||
'/backup-private-key',
|
'/backup-private-key',
|
||||||
passwordLimiter,
|
passwordLimiter,
|
||||||
@@ -41,4 +68,17 @@ router.post(
|
|||||||
passwordController.createBackupPrivateKey
|
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;
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
<title>Email Verification</title>
|
<title>Organization Invitation</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h2>Infisical</h2>
|
<h2>Infisical</h2>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
<title>Account Recovery</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Infisical</h2>
|
||||||
|
<h2>Reset your password</h2>
|
||||||
|
<p>Someone requested a password reset.</p>
|
||||||
|
<a href="{{callback_url}}?token={{token}}&to={{email}}">Reset password</a>
|
||||||
|
<p>If you didn't initiate this request, please contact us immediately at [email protected]</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
<title>Email Verification</title>
|
<title>Project Invitation</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h2>Infisical</h2>
|
<h2>Infisical</h2>
|
||||||
|
|||||||
Reference in New Issue
Block a user