Checkpoint weaving frontend and backend MFA

This commit is contained in:
Tuan Dang
2023-02-13 12:29:36 +07:00
parent 17406e413d
commit e53fd110f6
30 changed files with 1177 additions and 482 deletions
+4
View File
@@ -4,6 +4,8 @@ const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!;
const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10; const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10;
const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d'; const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d';
const JWT_AUTH_SECRET = process.env.JWT_AUTH_SECRET!; const JWT_AUTH_SECRET = process.env.JWT_AUTH_SECRET!;
const JWT_MFA_LIFETIME = process.env.JWT_MFA_LIFETIME! || '5m';
const JWT_MFA_SECRET = process.env.JWT_MFA_SECRET!;
const JWT_REFRESH_LIFETIME = process.env.JWT_REFRESH_LIFETIME! || '90d'; const JWT_REFRESH_LIFETIME = process.env.JWT_REFRESH_LIFETIME! || '90d';
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET!; const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET!;
const JWT_SERVICE_SECRET = process.env.JWT_SERVICE_SECRET!; const JWT_SERVICE_SECRET = process.env.JWT_SERVICE_SECRET!;
@@ -54,6 +56,8 @@ export {
SALT_ROUNDS, SALT_ROUNDS,
JWT_AUTH_LIFETIME, JWT_AUTH_LIFETIME,
JWT_AUTH_SECRET, JWT_AUTH_SECRET,
JWT_MFA_LIFETIME,
JWT_MFA_SECRET,
JWT_REFRESH_LIFETIME, JWT_REFRESH_LIFETIME,
JWT_REFRESH_SECRET, JWT_REFRESH_SECRET,
JWT_SERVICE_SECRET, JWT_SERVICE_SECRET,
+2 -2
View File
@@ -5,7 +5,7 @@ import * as Sentry from '@sentry/node';
import * as bigintConversion from 'bigint-conversion'; import * as bigintConversion from 'bigint-conversion';
const jsrp = require('jsrp'); const jsrp = require('jsrp');
import { User, LoginSRPDetail } from '../../models'; import { User, LoginSRPDetail } from '../../models';
import { createToken, issueTokens, clearTokens } from '../../helpers/auth'; import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth';
import { import {
ACTION_LOGIN, ACTION_LOGIN,
ACTION_LOGOUT ACTION_LOGOUT
@@ -111,7 +111,7 @@ export const login2 = async (req: Request, res: Response) => {
// compare server and client shared keys // compare server and client shared keys
if (server.checkClientProof(clientProof)) { if (server.checkClientProof(clientProof)) {
// issue tokens // issue tokens
const tokens = await issueTokens({ userId: user._id.toString() }); const tokens = await issueAuthTokens({ userId: user._id.toString() });
// store (refresh) token in httpOnly cookie // store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, { res.cookie('jid', tokens.refreshToken, {
+85 -12
View File
@@ -5,11 +5,13 @@ import * as Sentry from '@sentry/node';
import * as bigintConversion from 'bigint-conversion'; import * as bigintConversion from 'bigint-conversion';
const jsrp = require('jsrp'); const jsrp = require('jsrp');
import { User } from '../../models'; import { User } from '../../models';
import { issueTokens } from '../../helpers/auth'; import { issueAuthTokens, createToken } from '../../helpers/auth';
import { sendMail } from '../../helpers/nodemailer'; import { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services'; import { TokenService } from '../../services';
import { import {
NODE_ENV NODE_ENV,
JWT_MFA_LIFETIME,
JWT_MFA_SECRET
} from '../../config'; } from '../../config';
import { import {
TOKEN_EMAIL_MFA TOKEN_EMAIL_MFA
@@ -102,6 +104,15 @@ export const login2 = async (req: Request, res: Response) => {
if (user.isMfaEnabled) { if (user.isMfaEnabled) {
// case: user has MFA enabled // case: user has MFA enabled
// generate temporary MFA token
const token = createToken({
payload: {
userId: user._id.toString()
},
expiresIn: JWT_MFA_LIFETIME,
secret: JWT_MFA_SECRET
});
const code = await TokenService.createToken({ const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA, type: TOKEN_EMAIL_MFA,
@@ -119,12 +130,13 @@ export const login2 = async (req: Request, res: Response) => {
}); });
return res.status(200).send({ return res.status(200).send({
mfaEnabled: true mfaEnabled: true,
token
}); });
} }
// issue tokens // issue tokens
const tokens = await issueTokens({ userId: user._id.toString() }); const tokens = await issueAuthTokens({ userId: user._id.toString() });
// store (refresh) token in httpOnly cookie // store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, { res.cookie('jid', tokens.refreshToken, {
@@ -136,18 +148,41 @@ export const login2 = async (req: Request, res: Response) => {
// case: user does not have MFA enabled // case: user does not have MFA enabled
// return (access) token in response // return (access) token in response
return res.status(200).send({
interface ResponseData {
mfaEnabled: boolean;
encryptionVersion: any;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
}
const response: ResponseData = {
mfaEnabled: false, mfaEnabled: false,
encryptionVersion: user.encryptionVersion, encryptionVersion: user.encryptionVersion,
protectedKey: user.protectedKey ?? null,
protectedKeyIV: user.protectedKeyIV ?? null,
protectedKeyTag: user.protectedKeyTag ?? null,
token: tokens.token, token: tokens.token,
publicKey: user.publicKey, publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey, encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv, iv: user.iv,
tag: user.tag tag: user.tag
}); }
if (
user?.protectedKey &&
user?.protectedKeyIV &&
user?.protectedKeyTag
) {
response.protectedKey = user.protectedKey;
response.protectedKeyIV = user.protectedKeyIV
response.protectedKeyTag = user.protectedKeyTag;
}
return res.status(200).send(response);
} }
return res.status(400).send({ return res.status(400).send({
@@ -164,6 +199,43 @@ export const login2 = async (req: Request, res: Response) => {
} }
}; };
/**
* Send MFA token to email [email]
* @param req
* @param res
*/
export const sendMfaToken = async (req: Request, res: Response) => {
try {
const { email } = req.body;
const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA,
email
});
// send MFA code [code] to [email]
await sendMail({
template: 'emailMfa.handlebars',
subjectLine: 'Infisical MFA code',
recipients: [email],
substitutions: {
code
}
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to send MFA code'
});
}
return res.status(200).send({
message: 'Successfully sent new MFA code'
});
}
/** /**
* Verify MFA token [mfaToken] and issue JWT and refresh tokens if the * Verify MFA token [mfaToken] and issue JWT and refresh tokens if the
* MFA token [mfaToken] is valid * MFA token [mfaToken] is valid
@@ -187,7 +259,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
if (!user) throw new Error('Failed to find user'); if (!user) throw new Error('Failed to find user');
// issue tokens // issue tokens
const tokens = await issueTokens({ userId: user._id.toString() }); const tokens = await issueAuthTokens({ userId: user._id.toString() });
// store (refresh) token in httpOnly cookie // store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, { res.cookie('jid', tokens.refreshToken, {
@@ -196,7 +268,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
sameSite: 'strict', sameSite: 'strict',
secure: NODE_ENV === 'production' ? true : false secure: NODE_ENV === 'production' ? true : false
}); });
// case: user does not have MFA enabled // case: user does not have MFA enabled
// return (access) token in response // return (access) token in response
return res.status(200).send({ return res.status(200).send({
@@ -217,4 +289,5 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
message: 'Failed to authenticate. Try again?' message: 'Failed to authenticate. Try again?'
}); });
} }
} }
@@ -5,12 +5,10 @@ import { completeAccount } from '../../helpers/user';
import { import {
initializeDefaultOrg initializeDefaultOrg
} from '../../helpers/signup'; } from '../../helpers/signup';
import { issueTokens } from '../../helpers/auth'; import { issueAuthTokens } from '../../helpers/auth';
import { INVITED, ACCEPTED } from '../../variables'; import { INVITED, ACCEPTED } from '../../variables';
import axios from 'axios'; import axios from 'axios';
// TODO: finish
/** /**
* Complete setting up user by adding their personal and auth information as part of the * Complete setting up user by adding their personal and auth information as part of the
* signup flow * signup flow
@@ -102,7 +100,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
); );
// issue tokens // issue tokens
const tokens = await issueTokens({ const tokens = await issueAuthTokens({
userId: user._id.toString() userId: user._id.toString()
}); });
@@ -216,7 +214,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
); );
// issue tokens // issue tokens
const tokens = await issueTokens({ const tokens = await issueAuthTokens({
userId: user._id.toString() userId: user._id.toString()
}); });
+2 -2
View File
@@ -211,7 +211,7 @@ const getAuthAPIKeyPayload = async ({
* @return {String} obj.token - issued JWT token * @return {String} obj.token - issued JWT token
* @return {String} obj.refreshToken - issued refresh token * @return {String} obj.refreshToken - issued refresh token
*/ */
const issueTokens = async ({ userId }: { userId: string }) => { const issueAuthTokens = async ({ userId }: { userId: string }) => {
let token: string; let token: string;
let refreshToken: string; let refreshToken: string;
try { try {
@@ -298,6 +298,6 @@ export {
getAuthSTDPayload, getAuthSTDPayload,
getAuthAPIKeyPayload, getAuthAPIKeyPayload,
createToken, createToken,
issueTokens, issueAuthTokens,
clearTokens clearTokens
}; };
+1 -15
View File
@@ -2,9 +2,7 @@ import * as Sentry from '@sentry/node';
import { IUser } from '../models'; import { IUser } from '../models';
import { createOrganization } from './organization'; import { createOrganization } from './organization';
import { addMembershipsOrg } from './membershipOrg'; import { addMembershipsOrg } from './membershipOrg';
import { createWorkspace } from './workspace'; import { OWNER, ACCEPTED } from '../variables';
import { addMemberships } from './membership';
import { OWNER, ADMIN, ACCEPTED } from '../variables';
import { sendMail } from '../helpers/nodemailer'; import { sendMail } from '../helpers/nodemailer';
import { TokenService } from '../services'; import { TokenService } from '../services';
import { TOKEN_EMAIL_CONFIRMATION } from '../variables'; import { TOKEN_EMAIL_CONFIRMATION } from '../variables';
@@ -95,18 +93,6 @@ const initializeDefaultOrg = async ({
roles: [OWNER], roles: [OWNER],
statuses: [ACCEPTED] statuses: [ACCEPTED]
}); });
// initialize a default workspace inside the new organization
const workspace = await createWorkspace({
name: `Example Project`,
organizationId: organization._id.toString()
});
await addMemberships({
userIds: [user._id.toString()],
workspaceId: workspace._id.toString(),
roles: [ADMIN]
});
} catch (err) { } catch (err) {
throw new Error(`Failed to initialize default organization and workspace [err=${err}]`); throw new Error(`Failed to initialize default organization and workspace [err=${err}]`);
} }
+2
View File
@@ -1,4 +1,5 @@
import requireAuth from './requireAuth'; import requireAuth from './requireAuth';
import requireMfaAuth from './requireMfaAuth';
import requireBotAuth from './requireBotAuth'; import requireBotAuth from './requireBotAuth';
import requireSignupAuth from './requireSignupAuth'; import requireSignupAuth from './requireSignupAuth';
import requireWorkspaceAuth from './requireWorkspaceAuth'; import requireWorkspaceAuth from './requireWorkspaceAuth';
@@ -15,6 +16,7 @@ import validateRequest from './validateRequest';
export { export {
requireAuth, requireAuth,
requireMfaAuth,
requireBotAuth, requireBotAuth,
requireSignupAuth, requireSignupAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
+43
View File
@@ -0,0 +1,43 @@
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
import { User } from '../models';
import { JWT_MFA_SECRET } from '../config';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
userId: string;
}
}
/**
* Validate if (MFA) JWT temporary token on request is valid (e.g. not expired)
* and if there is an associated user.
*/
const requireMfaAuth = async (
req: Request,
res: Response,
next: NextFunction
) => {
// JWT (temporary) authentication middleware for complete signup
const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`}))
if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`}))
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, JWT_MFA_SECRET)
);
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
if (!user)
return next(UnauthorizedRequestError({message: 'Unable to authenticate for User account completion. Try logging in again'}))
req.user = user;
return next();
};
export default requireMfaAuth;
+20 -11
View File
@@ -1,15 +1,15 @@
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 { validateRequest } from '../../middleware'; import { requireMfaAuth, validateRequest } from '../../middleware';
import { authController } from '../../controllers/v2'; import { authController } from '../../controllers/v2';
import { authLimiter } from '../../helpers/rateLimiter'; import { authLimiter } from '../../helpers/rateLimiter';
router.post( router.post(
'/login1', '/login1',
authLimiter, authLimiter,
body('email').exists().trim().notEmpty(), body('email').isString().trim().notEmpty(),
body('clientPublicKey').exists().trim().notEmpty(), body('clientPublicKey').isString().trim().notEmpty(),
validateRequest, validateRequest,
authController.login1 authController.login1
); );
@@ -17,19 +17,28 @@ router.post(
router.post( router.post(
'/login2', '/login2',
authLimiter, authLimiter,
body('email').exists().trim().notEmpty(), body('email').isString().trim().notEmpty(),
body('clientProof').exists().trim().notEmpty(), body('clientProof').isString().trim().notEmpty(),
validateRequest, validateRequest,
authController.login2 authController.login2
); );
router.post( router.post(
'/mfa', '/mfa/send',
authLimiter, authLimiter,
body('email').exists().trim().notEmpty(), body('email').isString().trim().notEmpty(),
body('mfaToken').exists().trim().notEmpty(), validateRequest,
validateRequest, authController.sendMfaToken
authController.verifyMfaToken );
router.post(
'/mfa/verify',
authLimiter,
requireMfaAuth,
body('email').isString().trim().notEmpty(),
body('mfaToken').isString().trim().notEmpty(),
validateRequest,
authController.verifyMfaToken
); );
export default router; export default router;
+148
View File
@@ -0,0 +1,148 @@
import React, { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { faWarning } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from '@app/components/basic/buttons/Button';
import Error from '@app/components/basic/Error';
import InputField from '@app/components/basic/InputField';
import attemptLogin from '@app/components/utilities/attemptLogin';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
/**
* 1st step of login - user enters their username and password
* @param {Object} obj
* @param {String} obj.email - email of user
* @param {Function} obj.setEmail - function to set the email of user
* @param {String} obj.password - password of user
* @param {String} obj.setPassword - function to set the password of user
* @param {Function} obj.setStep - function to set the login flow step
* @returns
*/
export default function LoginStep ({
email,
setEmail,
password,
setPassword,
setStep
}: {
email: string;
setEmail: (email: string) => void;
password: string;
setPassword: (password: string) => void;
setStep: (step: number) => void;
}) {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState(false);
const { t } = useTranslation();
const handleLogin = async () => {
try {
if (!email || !password) {
return;
}
setIsLoading(true);
const isLoginSuccessful = await attemptLogin(email, password);
if (isLoginSuccessful && isLoginSuccessful.success) {
// case: login was successful
if (isLoginSuccessful.mfaEnabled) {
// case: login requires MFA step
setStep(2);
setIsLoading(false);
return;
}
// case: login does not require MFA step
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
}
} catch (err) {
console.error(err);
setLoginError(true);
}
setIsLoading(false);
}
return (
<form onSubmit={(e) => e.preventDefault()}>
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-4 pt-8 px-6 rounded-xl drop-shadow-xl">
<p className="text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-6">
{t('login:login')}
</p>
<div className="flex items-center justify-center w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
<InputField
label={t('common:email')}
onChangeHandler={setEmail}
type="email"
value={email}
placeholder=""
isRequired
autoComplete="username"
/>
</div>
<div className="relative flex items-center justify-center w-full md:p-2 rounded-lg md:mt-2 mt-6 max-h-24 md:max-h-28">
<InputField
label={t('common:password')}
onChangeHandler={setPassword}
type="password"
value={password}
placeholder=""
isRequired
autoComplete="current-password"
id="current-password"
/>
<div className="absolute top-2 right-3 text-primary-700 hover:text-primary duration-200 cursor-pointer text-sm">
<Link href="/verify-email">
<button
type="button"
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
>
{t('login:forgot-password')}
</button>
</Link>
</div>
</div>
{!isLoading && loginError && <Error text={t('login:error-login') ?? ''} />}
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
<Button
type="submit"
text={t('login:login') ?? ''}
onButtonPressed={async () => handleLogin()}
loading={isLoading}
size="lg"
/>
</div>
</div>
</div>
{false && (
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-300 rounded-md max-w-md mx-auto mt-4">
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-6 text-6xl" />
{t('common:maintenance-alert')}
</div>
)}
<div className="flex flex-row items-center justify-center md:pb-4 mt-4">
<p className="text-sm flex justify-center text-gray-400 w-max">
{t('login:need-account')}
</p>
<Link href="/signup">
<button
type="button"
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
>
{t('login:create-account')}
</button>
</Link>
</div>
</form>
);
}
export const getStaticProps = getTranslatedStaticProps(['auth', 'login']);
@@ -1,9 +1,11 @@
/* eslint-disable react/jsx-props-no-spreading */ /* eslint-disable react/jsx-props-no-spreading */
import React, { useState } from 'react'; import React, { useState } from 'react';
import ReactCodeInput from 'react-code-input'; import ReactCodeInput from 'react-code-input';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import sendVerificationEmail from '@app/pages/api/auth/SendVerificationEmail'; import attemptLoginMfa from '@app/components/utilities/attemptLoginMfa';
import resendMfaToken from '@app/pages/api/auth/resendMfaToken';
import Button from '../basic/buttons/Button'; import Button from '../basic/buttons/Button';
import Error from '../basic/Error'; import Error from '../basic/Error';
@@ -27,63 +29,65 @@ const props = {
borderColor: '#2d2f33' borderColor: '#2d2f33'
} }
} as const; } as const;
const propsPhone = {
inputStyle: {
fontFamily: 'monospace',
margin: '4px',
MozAppearance: 'textfield',
width: '40px',
borderRadius: '5px',
fontSize: '24px',
height: '40px',
paddingLeft: '7',
backgroundColor: '#0d1117',
color: 'white',
border: '1px solid #2d2f33',
textAlign: 'center',
outlineColor: '#8ca542',
borderColor: '#2d2f33'
}
} as const;
interface CodeInputStepProps {
email: string;
incrementStep: () => void;
setCode: (value: string) => void;
codeError: boolean;
}
/** /**
* This is the second step of sign up where users need to verify their email * 2nd step of login - users enter their MFA code
* @param {object} obj * @param {Object} obj
* @param {string} obj.email - user's email to which we just sent a verification email * @param {String} obj.email - email of user
* @param {function} obj.incrementStep - goes to the next step of signup * @param {String} obj.password - password of user
* @param {function} obj.setCode - state updating function that set the current value of the emai verification code * @param {Function} obj.setStep - function to set the login flow step
* @param {boolean} obj.codeError - whether the code was inputted wrong or now
* @returns * @returns
*/ */
export default function TwoFAStep({ export default function MFAStep({
email, email,
incrementStep, password
setCode, }: {
codeError email: string;
}: CodeInputStepProps): JSX.Element { password: string;
}): JSX.Element {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isResendingVerificationEmail, setIsResendingVerificationEmail] = useState(false); const [mfaCode, setMfaCode] = useState('');
const [codeError, setCodeError] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const resendVerificationEmail = async () => { const handleLoginMfa = async () => {
setIsResendingVerificationEmail(true); try {
setIsLoading(true); if (mfaCode.length !== 6) {
sendVerificationEmail(email); return;
setTimeout(() => { }
setIsLoading(false);
setIsResendingVerificationEmail(false); setIsLoading(true);
}, 2000); const isLoginSuccessful = await attemptLoginMfa({
}; email,
password,
mfaToken: mfaCode
});
if (isLoginSuccessful) {
setIsLoading(false);
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
}
} catch (err) {
console.error(err);
setCodeError(true);
}
}
const handleResendMfaCode = async () => {
try {
await resendMfaToken({
email
});
} catch (err) {
console.error(err);
}
}
return ( return (
<div className="bg-bunker w-max mx-auto h-7/12 pt-10 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-16"> <form className="bg-bunker w-max mx-auto h-7/12 pt-10 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-16">
<p className="text-l flex justify-center text-bunker-300">{t('signup:step2-message')}</p> <p className="text-l flex justify-center text-bunker-300">{t('signup:step2-message')}</p>
<p className="text-l flex justify-center font-semibold my-2 text-bunker-300">{email} </p> <p className="text-l flex justify-center font-semibold my-2 text-bunker-300">{email} </p>
<div className="hidden md:block"> <div className="hidden md:block">
@@ -92,38 +96,31 @@ export default function TwoFAStep({
inputMode="tel" inputMode="tel"
type="text" type="text"
fields={6} fields={6}
onChange={setCode} onChange={setMfaCode}
{...props} {...props}
className="mt-6 mb-2" className="mt-6 mb-2"
/> />
</div> </div>
<div className="block md:hidden">
<ReactCodeInput
name=""
inputMode="tel"
type="text"
fields={6}
onChange={setCode}
{...propsPhone}
className="mt-2 mb-6"
/>
</div>
{codeError && <Error text={t('signup:step2-code-error')} />} {codeError && <Error text={t('signup:step2-code-error')} />}
<div className="flex max-w-max min-w-28 flex-col items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2"> <div className="flex max-w-max min-w-28 flex-col items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
<Button text={t('signup:verify') ?? ''} onButtonPressed={incrementStep} size="lg" /> <Button
text={t('signup:verify') ?? ''}
onButtonPressed={() => handleLoginMfa()}
size="lg"
/>
</div> </div>
<div className="flex flex-col items-center justify-center w-full max-h-24 max-w-md mx-auto pt-2"> <div className="flex flex-col items-center justify-center w-full max-h-24 max-w-md mx-auto pt-2">
<div className="flex flex-row items-baseline gap-1 text-sm"> <div className="flex flex-row items-baseline gap-1 text-sm">
<span className="text-bunker-400">{t('signup:step2-resend-alert')}</span> <span className="text-bunker-400">{t('signup:step2-resend-alert')}</span>
<u <u
className={`font-normal ${ className={`font-normal ${
isResendingVerificationEmail isLoading
? 'text-bunker-400' ? 'text-bunker-400'
: 'text-primary-700 hover:text-primary duration-200' : 'text-primary-700 hover:text-primary duration-200'
}`} }`}
> >
<button disabled={isLoading} onClick={resendVerificationEmail} type="button"> <button disabled={isLoading} onClick={() => handleResendMfaCode()} type="button">
{isResendingVerificationEmail {isLoading
? t('signup:step2-resend-progress') ? t('signup:step2-resend-progress')
: t('signup:step2-resend-submit')} : t('signup:step2-resend-submit')}
</button> </button>
@@ -131,6 +128,6 @@ export default function TwoFAStep({
</div> </div>
<p className="text-sm text-bunker-400 pb-2">{t('signup:step2-spam-alert')}</p> <p className="text-sm text-bunker-400 pb-2">{t('signup:step2-spam-alert')}</p>
</div> </div>
</div> </form>
); );
} }
+23 -22
View File
@@ -1,7 +1,6 @@
import crypto from 'crypto'; import crypto from 'crypto';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons'; import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -10,20 +9,21 @@ import nacl from 'tweetnacl';
import { encodeBase64 } from 'tweetnacl-util'; import { encodeBase64 } from 'tweetnacl-util';
import completeAccountInformationSignup from '@app/pages/api/auth/CompleteAccountInformationSignup'; import completeAccountInformationSignup from '@app/pages/api/auth/CompleteAccountInformationSignup';
import getOrganizations from '@app/pages/api/organization/getOrgs';
import ProjectService from '@app/services/ProjectService';
import Button from '../basic/buttons/Button'; import Button from '../basic/buttons/Button';
import InputField from '../basic/InputField'; import InputField from '../basic/InputField';
import attemptLogin from '../utilities/attemptLogin';
import passwordCheck from '../utilities/checks/PasswordCheck'; import passwordCheck from '../utilities/checks/PasswordCheck';
import Aes256Gcm from '../utilities/cryptography/aes-256-gcm'; import Aes256Gcm from '../utilities/cryptography/aes-256-gcm';
import { deriveArgonKey } from '../utilities/cryptography/crypto'; import { deriveArgonKey } from '../utilities/cryptography/crypto';
import { saveTokenToLocalStorage } from '../utilities/saveTokenToLocalStorage'; import { saveTokenToLocalStorage } from '../utilities/saveTokenToLocalStorage';
import SecurityClient from '../utilities/SecurityClient';
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
const client = new jsrp.client(); const client = new jsrp.client();
interface UserInfoStepProps { interface UserInfoStepProps {
verificationToken: string;
incrementStep: () => void; incrementStep: () => void;
email: string; email: string;
password: string; password: string;
@@ -48,7 +48,6 @@ interface UserInfoStepProps {
* @param {string} obj.setLastName - function managing the state of user's last name * @param {string} obj.setLastName - function managing the state of user's last name
*/ */
export default function UserInfoStep({ export default function UserInfoStep({
verificationToken,
incrementStep, incrementStep,
email, email,
password, password,
@@ -66,7 +65,6 @@ export default function UserInfoStep({
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter();
// Verifies if the information that the users entered (name, workspace) // Verifies if the information that the users entered (name, workspace)
// is there, and if the password matches the criteria. // is there, and if the password matches the criteria.
@@ -110,6 +108,8 @@ export default function UserInfoStep({
async () => { async () => {
client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => { client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
try { try {
// TODO: moduralize into KeyService
const derivedKey = await deriveArgonKey({ const derivedKey = await deriveArgonKey({
password, password,
salt: result.salt, salt: result.salt,
@@ -158,28 +158,29 @@ export default function UserInfoStep({
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
salt: result.salt, salt: result.salt,
verifier: result.verifier, verifier: result.verifier,
token: verificationToken,
organizationName: `${firstName}'s organization` organizationName: `${firstName}'s organization`
}); });
// if everything works, go the main dashboard page. SecurityClient.setToken(response.token);
if (response.status === 200) {
// response = await response.json();
saveTokenToLocalStorage({ saveTokenToLocalStorage({
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
publicKey, publicKey,
encryptedPrivateKey, encryptedPrivateKey,
iv: encryptedPrivateKeyIV, iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag, tag: encryptedPrivateKeyTag,
privateKey privateKey
}); });
await attemptLogin(email, password, () => {}, router, true, false); incrementStep();
incrementStep();
} const userOrgs = await getOrganizations();
await ProjectService.initProject({
organizationId: userOrgs[0]?._id,
projectName: 'Example Project'
});
} catch (error) { } catch (error) {
setIsLoading(false); setIsLoading(false);
+190 -210
View File
@@ -1,20 +1,12 @@
/* eslint-disable prefer-destructuring */ /* eslint-disable prefer-destructuring */
import crypto from 'crypto';
import jsrp from 'jsrp'; import jsrp from 'jsrp';
import { SecretDataProps } from 'public/data/frequentInterfaces';
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
import login1 from '@app/pages/api/auth/Login1'; import login1 from '@app/pages/api/auth/Login1';
import login2 from '@app/pages/api/auth/Login2'; import login2 from '@app/pages/api/auth/Login2';
import addSecrets from '@app/pages/api/files/AddSecrets';
import getOrganizations from '@app/pages/api/organization/getOrgs'; import getOrganizations from '@app/pages/api/organization/getOrgs';
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects'; import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
import getUser from '@app/pages/api/user/getUser'; import KeyService from '@app/services/KeyService';
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
import { deriveArgonKey, encryptAssymmetric } from './cryptography/crypto';
import encryptSecrets from './secrets/encryptSecrets';
import Telemetry from './telemetry/Telemetry'; import Telemetry from './telemetry/Telemetry';
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage'; import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
import SecurityClient from './SecurityClient'; import SecurityClient from './SecurityClient';
@@ -22,44 +14,39 @@ import SecurityClient from './SecurityClient';
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
const client = new jsrp.client(); const client = new jsrp.client();
interface IsLoginSuccessful {
mfaEnabled: boolean;
success: boolean;
}
/** /**
* This function logs in the user (whether it's right after signup, or a normal login) * Return whether or not login is successful for user with email [email]
* @param {string} email - email of the user logging in * and password [password]
* @param {string} password - password of the user logging in * @param {string} email - email of user to log in
* @param {function} setErrorLogin - function that visually dispay an error is something is wrong * @param {string} password - password of user to log in
* @param {*} router
* @param {boolean} isSignUp - whether this log in is a part of signup
* @param {boolean} isLogin - ?
* @returns
*/ */
const attemptLogin = async ( const attemptLogin = async (
email: string, email: string,
password: string, password: string
setErrorLogin: (value: boolean) => void, ): Promise<IsLoginSuccessful> => {
router: any, const telemetry = new Telemetry().getInstance();
isSignUp: boolean, return new Promise((resolve, reject) => {
isLogin: boolean
) => {
try {
const telemetry = new Telemetry().getInstance();
client.init( client.init(
{ {
username: email, username: email,
password password
}, },
async () => { async () => {
const clientPublicKey = client.getPublicKey();
try { try {
const clientPublicKey = client.getPublicKey();
const { serverPublicKey, salt } = await login1(email, clientPublicKey); const { serverPublicKey, salt } = await login1(email, clientPublicKey);
client.setSalt(salt); client.setSalt(salt);
client.setServerPublicKey(serverPublicKey); client.setServerPublicKey(serverPublicKey);
const clientProof = client.getProof(); // called M1 const clientProof = client.getProof(); // called M1
// if everything works, go the main dashboard page. const {
const { // mfaEnabled mfaEnabled,
encryptionVersion, encryptionVersion,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
@@ -73,52 +60,40 @@ const attemptLogin = async (
email, email,
clientProof clientProof
); );
if (mfaEnabled) {
// case: MFA is enabled
SecurityClient.setToken(token); // set temporary (MFA) JWT token
SecurityClient.setToken(token);
let privateKey; resolve({
if (encryptionVersion === 1) { mfaEnabled,
privateKey = Aes256Gcm.decrypt({ success: true
ciphertext: encryptedPrivateKey,
iv,
tag,
secret: password
.slice(0, 32)
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
}); });
} else if (
saveTokenToLocalStorage({ !mfaEnabled &&
publicKey, encryptionVersion &&
encryptedPrivateKey &&
iv &&
tag &&
token
) {
// case: MFA is not enabled
// set JWT token
SecurityClient.setToken(token);
const privateKey = await KeyService.decryptPrivateKey({
encryptionVersion,
encryptedPrivateKey, encryptedPrivateKey,
iv, iv,
tag, tag,
privateKey
});
} else if (encryptionVersion === 2 && protectedKey && protectedKeyIV && protectedKeyTag) {
const derivedKey = await deriveArgonKey({
password, password,
salt, salt,
mem: 65536, protectedKey,
time: 3, protectedKeyIV,
parallelism: 1, protectedKeyTag
hashLen: 32
});
if (!derivedKey) throw new Error('Failed to derive key');
const key = Aes256Gcm.decrypt({
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag,
secret: Buffer.from(derivedKey.hash)
});
// decrypt back the private key
privateKey = Aes256Gcm.decrypt({
ciphertext: encryptedPrivateKey,
iv,
tag,
secret: Buffer.from(key, 'hex')
}); });
saveTokenToLocalStorage({ saveTokenToLocalStorage({
@@ -131,155 +106,160 @@ const attemptLogin = async (
tag, tag,
privateKey privateKey
}); });
}
// TODO: in the future - move this logic elsewhere
if (!privateKey) throw new Error('Failed to decrypt private key'); // because this function is about logging the user in
// and not initializing the login details
const userOrgs = await getOrganizations();
const orgId = userOrgs[0]._id;
localStorage.setItem('orgData.id', orgId);
const userOrgs = await getOrganizations(); const orgUserProjects = await getOrganizationUserProjects({
const userOrgsData = userOrgs.map((org: { _id: string }) => org._id); orgId
let orgToLogin;
if (userOrgsData.includes(localStorage.getItem('orgData.id'))) {
orgToLogin = localStorage.getItem('orgData.id');
} else {
orgToLogin = userOrgsData[0];
localStorage.setItem('orgData.id', orgToLogin);
}
let orgUserProjects = await getOrganizationUserProjects({
orgId: orgToLogin
});
orgUserProjects = orgUserProjects?.map((project: { _id: string }) => project._id);
let projectToLogin;
if (orgUserProjects.includes(localStorage.getItem('projectData.id'))) {
projectToLogin = localStorage.getItem('projectData.id');
} else {
try {
projectToLogin = orgUserProjects[0];
localStorage.setItem('projectData.id', projectToLogin);
} catch (error) {
console.log('ERROR: User likely has no projects. ', error);
}
}
if (email) {
telemetry.identify(email);
telemetry.capture('User Logged In');
}
if (isSignUp) {
const randomBytes = crypto.randomBytes(16).toString('hex');
const PRIVATE_KEY = String(localStorage.getItem('PRIVATE_KEY'));
const myUser = await getUser();
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes,
publicKey: myUser.publicKey,
privateKey: PRIVATE_KEY
}) as { ciphertext: string; nonce: string };
await uploadKeys(projectToLogin, myUser._id, ciphertext, nonce);
const secretsToBeAdded: SecretDataProps[] = [
{
pos: 0,
key: 'DATABASE_URL',
// eslint-disable-next-line no-template-curly-in-string
value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
valueOverride: undefined,
comment: 'Secret referencing example',
id: '',
tags: []
},
{
pos: 1,
key: 'DB_USERNAME',
value: 'OVERRIDE_THIS',
valueOverride: undefined,
comment:
'Override secrets with personal value',
id: '',
tags: []
},
{
pos: 2,
key: 'DB_PASSWORD',
value: 'OVERRIDE_THIS',
valueOverride: undefined,
comment:
'Another secret override',
id: '',
tags: []
},
{
pos: 3,
key: 'DB_USERNAME',
value: 'user1234',
valueOverride: 'user1234',
comment: '',
id: '',
tags: []
},
{
pos: 4,
key: 'DB_PASSWORD',
value: 'example_password',
valueOverride: 'example_password',
comment: '',
id: '',
tags: []
},
{
pos: 5,
key: 'TWILIO_AUTH_TOKEN',
value: 'example_twillio_token',
valueOverride: undefined,
comment: '',
id: '',
tags: []
},
{
pos: 6,
key: 'WEBSITE_URL',
value: 'http://localhost:3000',
valueOverride: undefined,
comment: '',
id: '',
tags: []
}
];
const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeAdded,
workspaceId: String(localStorage.getItem('projectData.id')),
env: 'dev'
}); });
await addSecrets({ localStorage.setItem('projectData.id', orgUserProjects[0]._id);
secrets: secrets ?? [],
env: 'dev', // // TODO: this part definitely needs to be refactored
workspaceId: String(localStorage.getItem('projectData.id')) // const userOrgs = await getOrganizations();
// const userOrgsData = userOrgs.map((org: { _id: string }) => org._id);
// let orgToLogin;
// if (userOrgsData.includes(localStorage.getItem('orgData.id'))) {
// orgToLogin = localStorage.getItem('orgData.id');
// } else {
// orgToLogin = userOrgsData[0];
// localStorage.setItem('orgData.id', orgToLogin);
// }
// let orgUserProjects = await getOrganizationUserProjects({
// orgId: orgToLogin
// });
// orgUserProjects = orgUserProjects?.map((project: { _id: string }) => project._id);
// let projectToLogin;
// if (orgUserProjects.includes(localStorage.getItem('projectData.id'))) {
// projectToLogin = localStorage.getItem('projectData.id');
// } else {
// try {
// projectToLogin = orgUserProjects[0];
// localStorage.setItem('projectData.id', projectToLogin);
// } catch (error) {
// console.log('ERROR: User likely has no projects. ', error);
// }
// }
if (email) {
telemetry.identify(email);
telemetry.capture('User Logged In');
}
resolve({
mfaEnabled: false,
success: true
}); });
} }
} catch (err) {
if (isLogin) { reject(err);
if (localStorage.getItem('projectData.id') !== "undefined") {
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
} else {
router.push("/noprojects");
}
}
} catch (error) {
console.log(error);
setErrorLogin(true);
console.log('Login response not available');
} }
} }
); );
} catch (error) { });
console.log('Something went wrong during authentication');
}
return true;
}; };
export default attemptLogin; export default attemptLogin;
// should be function: init first project
// if (isSignUp) {
// const randomBytes = crypto.randomBytes(16).toString('hex');
// const PRIVATE_KEY = String(localStorage.getItem('PRIVATE_KEY'));
// const myUser = await getUser();
// const { ciphertext, nonce } = encryptAssymmetric({
// plaintext: randomBytes,
// publicKey: myUser.publicKey,
// privateKey: PRIVATE_KEY
// }) as { ciphertext: string; nonce: string };
// await uploadKeys(projectToLogin, myUser._id, ciphertext, nonce);
// const secretsToBeAdded: SecretDataProps[] = [
// {
// pos: 0,
// key: 'DATABASE_URL',
// // eslint-disable-next-line no-template-curly-in-string
// value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
// valueOverride: undefined,
// comment: 'Secret referencing example',
// id: '',
// tags: []
// },
// {
// pos: 1,
// key: 'DB_USERNAME',
// value: 'OVERRIDE_THIS',
// valueOverride: undefined,
// comment:
// 'Override secrets with personal value',
// id: '',
// tags: []
// },
// {
// pos: 2,
// key: 'DB_PASSWORD',
// value: 'OVERRIDE_THIS',
// valueOverride: undefined,
// comment:
// 'Another secret override',
// id: '',
// tags: []
// },
// {
// pos: 3,
// key: 'DB_USERNAME',
// value: 'user1234',
// valueOverride: 'user1234',
// comment: '',
// id: '',
// tags: []
// },
// {
// pos: 4,
// key: 'DB_PASSWORD',
// value: 'example_password',
// valueOverride: 'example_password',
// comment: '',
// id: '',
// tags: []
// },
// {
// pos: 5,
// key: 'TWILIO_AUTH_TOKEN',
// value: 'example_twillio_token',
// valueOverride: undefined,
// comment: '',
// id: '',
// tags: []
// },
// {
// pos: 6,
// key: 'WEBSITE_URL',
// value: 'http://localhost:3000',
// valueOverride: undefined,
// comment: '',
// id: '',
// tags: []
// }
// ];
// const secrets = await encryptSecrets({
// secretsToEncrypt: secretsToBeAdded,
// workspaceId: String(localStorage.getItem('projectData.id')),
// env: 'dev'
// });
// await addSecrets({
// secrets: secrets ?? [],
// env: 'dev',
// workspaceId: String(localStorage.getItem('projectData.id'))
// });
// }
@@ -0,0 +1,88 @@
/* eslint-disable prefer-destructuring */
import jsrp from 'jsrp';
import login1 from '@app/pages/api/auth/Login1';
import verifyMfaToken from '@app/pages/api/auth/verifyMfaToken';
import KeyService from '@app/services/KeyService';
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
import SecurityClient from './SecurityClient';
// eslint-disable-next-line new-cap
const client = new jsrp.client();
/**
* Return whether or not MFA-login is successful for user with email [email]
* and MFA token [mfaToken]
* @param {Object} obj
* @param {String} obj.email - email of user
* @param {String} obj.mfaToken - MFA code/token
*/
const attemptLoginMfa = async ({
email,
password,
mfaToken
}: {
email: string;
password: string;
mfaToken: string;
}): Promise<Boolean> => {
return new Promise((resolve, reject) => {
client.init({
username: email,
password
}, async () => {
try {
const clientPublicKey = client.getPublicKey();
const { salt } = await login1(email, clientPublicKey);
const {
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
token,
publicKey,
encryptedPrivateKey,
iv,
tag
} = await verifyMfaToken({
email,
mfaToken
});
// set JWT token
SecurityClient.setToken(token);
const privateKey = await KeyService.decryptPrivateKey({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag
});
saveTokenToLocalStorage({
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv,
tag,
privateKey
});
resolve(true);
} catch (err) {
reject(err);
}
});
});
}
export default attemptLoginMfa;
+85
View File
@@ -0,0 +1,85 @@
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
import { deriveArgonKey } from '@app/components/utilities/cryptography/crypto';
/**
* @param {Object} obj
* @param {Number} obj.encryptionVersion
* @param {String} obj.encryptedPrivateKey
* @param {String} obj.iv
* @param {String} obj.tag
* @param {String} obj.password
* @param {String} obj.salt
* @param {String} obj.protectedKey
* @param {String} obj.protectedKeyIV
* @param {String} obj.protectedKeyTag
*/
const decryptPrivateKeyHelper = async ({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag,
}: {
encryptionVersion: number;
encryptedPrivateKey: string;
iv: string;
tag: string;
password: string;
salt: string;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
}) => {
let privateKey;
try {
if (encryptionVersion === 1) {
privateKey = Aes256Gcm.decrypt({
ciphertext: encryptedPrivateKey,
iv,
tag,
secret: password
.slice(0, 32)
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
});
} else if (encryptionVersion === 2 && protectedKey && protectedKeyIV && protectedKeyTag) {
const derivedKey = await deriveArgonKey({
password,
salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
if (!derivedKey) throw new Error('Failed to generate derived key');
const key = Aes256Gcm.decrypt({
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag,
secret: Buffer.from(derivedKey.hash)
});
// decrypt back the private key
privateKey = Aes256Gcm.decrypt({
ciphertext: encryptedPrivateKey,
iv,
tag,
secret: Buffer.from(key, 'hex')
});
} else {
throw new Error('Insufficient details to decrypt private key');
}
} catch (err) {
console.error(err);
throw new Error('Failed to decrypt private key');
}
return privateKey;
}
export { decryptPrivateKeyHelper };
+140
View File
@@ -0,0 +1,140 @@
import crypto from 'crypto';
import { encryptAssymmetric } from '@app/components/utilities/cryptography/crypto';
import encryptSecrets from '@app/components/utilities/secrets/encryptSecrets';
import addSecrets from '@app/pages/api/files/AddSecrets';
import getUser from '@app/pages/api/user/getUser';
import createWorkspace from "@app/pages/api/workspace/createWorkspace";
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
const secretsToBeAdded = [
{
pos: 0,
key: 'DATABASE_URL',
// eslint-disable-next-line no-template-curly-in-string
value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
valueOverride: undefined,
comment: 'Secret referencing example',
id: '',
tags: []
},
{
pos: 1,
key: 'DB_USERNAME',
value: 'OVERRIDE_THIS',
valueOverride: undefined,
comment:
'Override secrets with personal value',
id: '',
tags: []
},
{
pos: 2,
key: 'DB_PASSWORD',
value: 'OVERRIDE_THIS',
valueOverride: undefined,
comment:
'Another secret override',
id: '',
tags: []
},
{
pos: 3,
key: 'DB_USERNAME',
value: 'user1234',
valueOverride: 'user1234',
comment: '',
id: '',
tags: []
},
{
pos: 4,
key: 'DB_PASSWORD',
value: 'example_password',
valueOverride: 'example_password',
comment: '',
id: '',
tags: []
},
{
pos: 5,
key: 'TWILIO_AUTH_TOKEN',
value: 'example_twillio_token',
valueOverride: undefined,
comment: '',
id: '',
tags: []
},
{
pos: 6,
key: 'WEBSITE_URL',
value: 'http://localhost:3000',
valueOverride: undefined,
comment: '',
id: '',
tags: []
}
];
/**
* Create and initialize a new project in organization with id [organizationId]
* Note: current user should be a member of the organization
* @param {Object} obj
* @param {String} obj.organizationId - id of organization
* @param {String} obj.projectName - name of new project
* @returns {Project} project - new project
*/
const initProjectHelper = async ({
organizationId,
projectName
}: {
organizationId: string;
projectName: string;
}) => {
let project;
try {
// create new project
project = await createWorkspace({
workspaceName: projectName,
organizationId
});
// create and upload new (encrypted) project key
const randomBytes = crypto.randomBytes(16).toString('hex');
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
if (!PRIVATE_KEY) throw new Error('Failed to find private key');
const user = await getUser();
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes,
publicKey: user.publicKey,
privateKey: PRIVATE_KEY
});
await uploadKeys(project._id, user._id, ciphertext, nonce);
// encrypt and upload secrets to new project
const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeAdded,
workspaceId: project._id,
env: 'dev'
});
await addSecrets({
secrets: secrets ?? [],
env: 'dev',
workspaceId: project._id
});
} catch (err) {
console.error('Failed to init project in organization', err);
}
return project;
}
export {
initProjectHelper
}
+14 -6
View File
@@ -1,15 +1,23 @@
import { UserWsKeyPair } from '../keys/types'; import { UserWsKeyPair } from '../keys/types';
export type User = { export type User = {
seenIps: string[];
_id: string;
email: string;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
__v: number; email?: string;
firstName: string; firstName?: string;
lastName: string; lastName?: string;
encryptionVersion?: number;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
publicKey: string; publicKey: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
isMfaEnabled: boolean;
seenIps: string[];
_id: string;
__v: number;
}; };
export type OrgUser = { export type OrgUser = {
@@ -1,3 +1,5 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props { interface Props {
email: string; email: string;
firstName: string; firstName: string;
@@ -12,7 +14,6 @@ interface Props {
organizationName: string; organizationName: string;
salt: string; salt: string;
verifier: string; verifier: string;
token: string;
} }
/** /**
@@ -32,7 +33,6 @@ interface Props {
* @param {string} obj.tag * @param {string} obj.tag
* @param {string} obj.salt * @param {string} obj.salt
* @param {string} obj.verifier * @param {string} obj.verifier
* @param {string} obj.token - token that confirms a user's identity
* @returns * @returns
*/ */
const completeAccountInformationSignup = ({ const completeAccountInformationSignup = ({
@@ -48,13 +48,11 @@ const completeAccountInformationSignup = ({
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
salt, salt,
verifier, verifier,
token,
organizationName organizationName
}: Props) => fetch('/api/v2/signup/complete-account/signup', { }: Props) => SecurityClient.fetchCall('/api/v2/signup/complete-account/signup', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
Authorization: `Bearer ${ token}`
}, },
body: JSON.stringify({ body: JSON.stringify({
email, email,
@@ -71,6 +69,12 @@ const completeAccountInformationSignup = ({
verifier, verifier,
organizationName organizationName
}) })
}).then(async (res) => {
if (res && res?.status === 200) {
return res.json();
}
console.log('Failed to verify MFA code');
throw new Error('Something went wrong during MFA code verification');
}); });
export default completeAccountInformationSignup; export default completeAccountInformationSignup;
+1 -1
View File
@@ -25,7 +25,7 @@ const login1 = async (email: string, clientPublicKey: string) => {
const data = (await response.json()) as unknown as Login1; const data = (await response.json()) as unknown as Login1;
return data; return data;
} }
throw new Error("Wrong password"); throw new Error("Wrong password");
}; };
+6 -6
View File
@@ -1,14 +1,14 @@
interface Login2Response { interface Login2Response {
mfaEnabled: boolean; mfaEnabled: boolean;
encryptionVersion: number; token: string;
encryptionVersion?: number;
protectedKey?: string; protectedKey?: string;
protectedKeyIV?: string; protectedKeyIV?: string;
protectedKeyTag?: string; protectedKeyTag?: string;
token: string; publicKey?: string;
publicKey: string; encryptedPrivateKey?: string;
encryptedPrivateKey: string; iv?: string;
iv: string; tag?: string;
tag: string;
} }
/** /**
@@ -0,0 +1,29 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
/**
* Send new MFA token to user with email [email]
* @param {object} obj
* @param {string} obj.email - email of user
* @returns
*/
const resendMfaToken = async ({
email,
}: {
email: string;
}) => SecurityClient.fetchCall('/api/v2/auth/mfa/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email
})
}).then(async (res) => {
if (res && res?.status === 200) {
return res.json();
}
console.log('Failed to send new MFA code');
throw new Error('Something went wrong while sending new MFA code');
});
export default resendMfaToken;
@@ -0,0 +1,35 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
/**
* Verify MFA token [mfaToken] for user with email [email]
* @param {object} obj
* @param {string} obj.email - email of user
* @param {string} obj.mfaToken - MFA cod/token to verify
* @returns
*/
const verifyMfaToken = async ({
email,
mfaToken
}: {
email: string;
mfaToken: string;
}) => SecurityClient.fetchCall('/api/v2/auth/mfa/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
mfaToken
})
}).then(async (res) => {
if (res && res?.status === 200) {
return res.json();
}
console.log('Failed to verify MFA code');
throw new Error('Something went wrong during MFA code verification');
});
export default verifyMfaToken;
@@ -0,0 +1,32 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props {
isMfaEnabled: boolean;
}
/**
* Update the user's MFA-enabled status to [isMfaEnabled]
* @param {Object} obj
* @param {Boolean} obj.isMfaEnabled - whether or not MFA status should be set to enabled or not
* @returns {User} user - user with updated MFA-enabled status
*/
const updateMyMfaEnabled = async ({
isMfaEnabled
}: Props) =>
SecurityClient.fetchCall(`/api/v2/users/me/mfa`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
isMfaEnabled,
})
}).then(async (res) => {
if (res && res.status === 200) {
return (await res.json()).user;
}
console.log('Failed to update MFA status');
return undefined;
});
export default updateMyMfaEnabled;
+31 -101
View File
@@ -4,28 +4,24 @@ import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { faWarning } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from '@app/components/basic/buttons/Button';
import Error from '@app/components/basic/Error';
import InputField from '@app/components/basic/InputField';
import ListBox from '@app/components/basic/Listbox'; import ListBox from '@app/components/basic/Listbox';
import attemptLogin from '@app/components/utilities/attemptLogin'; import LoginStep from '@app/components/login/LoginStep';
import MFAStep from '@app/components/login/MFAStep';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps'; import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import { isLoggedIn } from '@app/reactQuery'; import { isLoggedIn } from '@app/reactQuery';
import getWorkspaces from './api/workspace/getWorkspaces'; import getWorkspaces from './api/workspace/getWorkspaces';
export default function Login() { export default function Login() {
const router = useRouter();
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [errorLogin, setErrorLogin] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isAlreadyLoggedIn, setIsAlreadyLoggedIn] = useState(false); const [isAlreadyLoggedIn, setIsAlreadyLoggedIn] = useState(false);
const router = useRouter(); const [step, setStep] = useState(1);
const { t } = useTranslation(); const { t } = useTranslation();
const lang = router.locale ?? 'en'; const lang = router.locale ?? 'en';
const setLanguage = async (to: string) => { const setLanguage = async (to: string) => {
router.push('/login', '/login', { locale: to }); router.push('/login', '/login', { locale: to });
@@ -49,25 +45,32 @@ export default function Login() {
redirectToDashboard(); redirectToDashboard();
} }
}, []); }, []);
/** const renderStep = (loginStep: number) => {
* This function check if the user entered the correct credentials and should be allowed to log in. // TODO: add MFA step
*/ switch (loginStep) {
const loginCheck = async () => { case 1:
return (
// #TODO: IF 2FA IS ENABLED REDIRECT TO <2FASTEP /> AND 'return;' <LoginStep
email={email}
if (!email || !password) { setEmail={setEmail}
return; password={password}
setPassword={setPassword}
setStep={setStep}
/>
);
case 2:
// TODO: add MFA step
return (
<MFAStep
email={email}
password={password}
/>
);
default:
return <div />
} }
}
setIsLoading(true);
await attemptLogin(email, password, setErrorLogin, router, false, true).then(() => {
setTimeout(() => {
setIsLoading(false);
}, 2000);
});
};
if (isAlreadyLoggedIn) { if (isAlreadyLoggedIn) {
return null return null
@@ -87,80 +90,7 @@ export default function Login() {
<Image src="/images/biglogo.png" height={90} width={120} alt="long logo" /> <Image src="/images/biglogo.png" height={90} width={120} alt="long logo" />
</div> </div>
</Link> </Link>
<form onChange={() => setErrorLogin(false)} onSubmit={(e) => e.preventDefault()}> {renderStep(step)}
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-4 pt-8 px-6 rounded-xl drop-shadow-xl">
<p className="text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-6">
{t('login:login')}
</p>
<div className="flex items-center justify-center w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
<InputField
label={t('common:email')}
onChangeHandler={setEmail}
type="email"
value={email}
placeholder=""
isRequired
autoComplete="username"
/>
</div>
<div className="relative flex items-center justify-center w-full md:p-2 rounded-lg md:mt-2 mt-6 max-h-24 md:max-h-28">
<InputField
label={t('common:password')}
onChangeHandler={setPassword}
type="password"
value={password}
placeholder=""
isRequired
autoComplete="current-password"
id="current-password"
/>
<div className="absolute top-2 right-3 text-primary-700 hover:text-primary duration-200 cursor-pointer text-sm">
<Link href="/verify-email">
<button
type="button"
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
>
{t('login:forgot-password')}
</button>
</Link>
</div>
</div>
{!isLoading && errorLogin && <Error text={t('login:error-login') ?? ''} />}
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
<Button
type="submit"
text={t('login:login') ?? ''}
onButtonPressed={loginCheck}
loading={isLoading}
size="lg"
/>
</div>
</div>
{/* <div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
<p className="text-gray-400">I may have <Link href="/login"><u className="text-sky-500 cursor-pointer">forgotten my password.</u></Link></p>
</div> */}
</div>
{false && (
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-300 rounded-md max-w-md mx-auto mt-4">
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-6 text-6xl" />
{t('common:maintenance-alert')}
</div>
)}
<div className="flex flex-row items-center justify-center md:pb-4 mt-4">
<p className="text-sm flex justify-center text-gray-400 w-max">
{t('login:need-account')}
</p>
<Link href="/signup">
<button
type="button"
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
>
{t('login:create-account')}
</button>
</Link>
</div>
</form>
<div className="absolute right-4 top-0 mt-4 flex items-center justify-center"> <div className="absolute right-4 top-0 mt-4 flex items-center justify-center">
<div className="w-48 mx-auto"> <div className="w-48 mx-auto">
<ListBox <ListBox
@@ -101,10 +101,7 @@ export default function PersonalSettings() {
/> />
</div> </div>
</div> </div>
<SecuritySection <SecuritySection />
isTwoFAEnabled
onIsTwoFAEnabledChange={(value: boolean) => console.log(value)}
/>
<div className="bg-white/5 rounded-md px-6 flex flex-col items-start w-full mt-2 mb-8 pt-2"> <div className="bg-white/5 rounded-md px-6 flex flex-col items-start w-full mt-2 mb-8 pt-2">
<div className="flex flex-row justify-between w-full"> <div className="flex flex-row justify-between w-full">
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
+2 -3
View File
@@ -11,6 +11,7 @@ import DownloadBackupPDF from '@app/components/signup/DonwloadBackupPDFStep';
import EnterEmailStep from '@app/components/signup/EnterEmailStep'; import EnterEmailStep from '@app/components/signup/EnterEmailStep';
import TeamInviteStep from '@app/components/signup/TeamInviteStep'; import TeamInviteStep from '@app/components/signup/TeamInviteStep';
import UserInfoStep from '@app/components/signup/UserInfoStep'; import UserInfoStep from '@app/components/signup/UserInfoStep';
import SecurityClient from '@app/components/utilities/SecurityClient';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps'; import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import checkEmailVerificationCode from './api/auth/CheckEmailVerificationCode'; import checkEmailVerificationCode from './api/auth/CheckEmailVerificationCode';
@@ -28,7 +29,6 @@ export default function SignUp() {
const [codeError, setCodeError] = useState(false); const [codeError, setCodeError] = useState(false);
const [step, setStep] = useState(1); const [step, setStep] = useState(1);
const router = useRouter(); const router = useRouter();
const [verificationToken, setVerificationToken] = useState('');
const { t } = useTranslation(); const { t } = useTranslation();
@@ -59,7 +59,7 @@ export default function SignUp() {
// Checking if the code matches the email. // Checking if the code matches the email.
const response = await checkEmailVerificationCode({ email, code }); const response = await checkEmailVerificationCode({ email, code });
if (response.status === 200) { if (response.status === 200) {
setVerificationToken((await response.json()).token); SecurityClient.setToken((await response.json()).token);
setStep(3); setStep(3);
} else { } else {
setCodeError(true); setCodeError(true);
@@ -94,7 +94,6 @@ export default function SignUp() {
/> />
) : step === 3 ? ( ) : step === 3 ? (
<UserInfoStep <UserInfoStep
verificationToken={verificationToken}
incrementStep={incrementStep} incrementStep={incrementStep}
email={email} email={email}
password={password} password={password}
+5 -1
View File
@@ -29,6 +29,7 @@ import verifySignupInvite from './api/auth/VerifySignupInvite';
const client = new jsrp.client(); const client = new jsrp.client();
export default function SignupInvite() { export default function SignupInvite() {
console.log('SignupInvite');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [firstName, setFirstName] = useState(''); const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState(''); const [lastName, setLastName] = useState('');
@@ -133,6 +134,7 @@ export default function SignupInvite() {
secret: Buffer.from(derivedKey.hash) secret: Buffer.from(derivedKey.hash)
}); });
console.log('SignupInvite A');
let response = await completeAccountInformationSignupInvite({ let response = await completeAccountInformationSignupInvite({
email, email,
firstName, firstName,
@@ -148,17 +150,19 @@ export default function SignupInvite() {
verifier: result.verifier, verifier: result.verifier,
token: verificationToken token: verificationToken
}); });
console.log('SignupInvite B');
// if everything works, go the main dashboard page. // if everything works, go the main dashboard page.
if (!errorCheck && response.status === 200) { if (!errorCheck && response.status === 200) {
response = await response.json(); response = await response.json();
console.log('SignupInvite C');
localStorage.setItem('publicKey', publicKey); localStorage.setItem('publicKey', publicKey);
localStorage.setItem('encryptedPrivateKey', encryptedPrivateKey); localStorage.setItem('encryptedPrivateKey', encryptedPrivateKey);
localStorage.setItem('iv', encryptedPrivateKeyIV); localStorage.setItem('iv', encryptedPrivateKeyIV);
localStorage.setItem('tag', encryptedPrivateKeyTag); localStorage.setItem('tag', encryptedPrivateKeyTag);
console.log('SignupInvite D');
await attemptLogin(email, password, setErrorLogin, router, false, false);
setStep(3); setStep(3);
} }
} catch (error) { } catch (error) {
+56
View File
@@ -0,0 +1,56 @@
import { decryptPrivateKeyHelper } from '@app/helpers/key';
/**
* Class to handle key actions
*/
class KeyService {
/** Return the user's decrypted private key
* @param {Object} obj
* @param {Number} obj.encryptionVersion
* @param {String} obj.encryptedPrivateKey
* @param {String} obj.iv
* @param {String} obj.tag
* @param {String} obj.password
* @param {String} obj.salt
* @param {String} obj.protectedKey
* @param {String} obj.protectedKeyIV
* @param {String} obj.protectedKeyTag
* @returns {String} privateKey - decrypted private key
*/
static async decryptPrivateKey({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag,
}: {
encryptionVersion: number;
encryptedPrivateKey: string;
iv: string;
tag: string;
password: string;
salt: string;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
}) {
return decryptPrivateKeyHelper({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag
});
}
}
export default KeyService;
+26
View File
@@ -0,0 +1,26 @@
import { initProjectHelper } from '@app/helpers/project';
class ProjectService {
/**
* Create and initialize a new project in organization with id [organizationId]
* Note: current user should be a member of the organization
* @param {Object} obj
* @param {String} obj.organizationId - id of organization
* @param {String} obj.projectName - name of new project
* @returns {Project} project - new project
*/
static async initProject({
organizationId,
projectName
}: {
organizationId: string;
projectName: string
}) {
return initProjectHelper({
organizationId,
projectName
});
}
}
export default ProjectService;
@@ -1,14 +1,35 @@
import { useEffect, useState } from 'react';
import { Checkbox } from '@app/components/v2'; import { Checkbox } from '@app/components/v2';
type Props = { import { useGetUser } from '../../../../hooks/api';
isTwoFAEnabled?: boolean; import { User } from '../../../../hooks/api/types';
onIsTwoFAEnabledChange: (state: boolean) => void; import updateMyMfaEnabled from '../../../../pages/api/user/updateMyMfaEnabled';
};
export const SecuritySection = ({ export const SecuritySection = () => {
isTwoFAEnabled, const [isMfaEnabled, setIsMfaEnabled] = useState(false);
onIsTwoFAEnabledChange const { data: user } = useGetUser();
}: Props) => {
useEffect(() => {
if (user && typeof user.isMfaEnabled !== 'undefined') {
setIsMfaEnabled(user.isMfaEnabled);
}
}, [user]);
const toggleMfa = async (state: boolean) => {
try {
const newUser: User = await updateMyMfaEnabled({
isMfaEnabled: state
});
if (newUser) {
setIsMfaEnabled(newUser.isMfaEnabled);
}
} catch (err) {
console.error(err);
}
}
return ( return (
<form> <form>
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2"> <div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
@@ -18,9 +39,9 @@ export const SecuritySection = ({
<Checkbox <Checkbox
className="data-[state=checked]:bg-primary" className="data-[state=checked]:bg-primary"
id="isTwoFAEnabled" id="isTwoFAEnabled"
isChecked={isTwoFAEnabled} isChecked={isMfaEnabled}
onCheckedChange={(state) => { onCheckedChange={(state) => {
onIsTwoFAEnabledChange(state as boolean); toggleMfa(state as boolean);
}} }}
> >
Enable 2-factor authentication via your personal email. Enable 2-factor authentication via your personal email.