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 JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d';
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_SECRET = process.env.JWT_REFRESH_SECRET!;
const JWT_SERVICE_SECRET = process.env.JWT_SERVICE_SECRET!;
@@ -54,6 +56,8 @@ export {
SALT_ROUNDS,
JWT_AUTH_LIFETIME,
JWT_AUTH_SECRET,
JWT_MFA_LIFETIME,
JWT_MFA_SECRET,
JWT_REFRESH_LIFETIME,
JWT_REFRESH_SECRET,
JWT_SERVICE_SECRET,
+2 -2
View File
@@ -5,7 +5,7 @@ import * as Sentry from '@sentry/node';
import * as bigintConversion from 'bigint-conversion';
const jsrp = require('jsrp');
import { User, LoginSRPDetail } from '../../models';
import { createToken, issueTokens, clearTokens } from '../../helpers/auth';
import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth';
import {
ACTION_LOGIN,
ACTION_LOGOUT
@@ -111,7 +111,7 @@ export const login2 = async (req: Request, res: Response) => {
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
// issue tokens
const tokens = await issueTokens({ userId: user._id.toString() });
const tokens = await issueAuthTokens({ userId: user._id.toString() });
// store (refresh) token in httpOnly cookie
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';
const jsrp = require('jsrp');
import { User } from '../../models';
import { issueTokens } from '../../helpers/auth';
import { issueAuthTokens, createToken } from '../../helpers/auth';
import { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services';
import {
NODE_ENV
NODE_ENV,
JWT_MFA_LIFETIME,
JWT_MFA_SECRET
} from '../../config';
import {
TOKEN_EMAIL_MFA
@@ -102,6 +104,15 @@ export const login2 = async (req: Request, res: Response) => {
if (user.isMfaEnabled) {
// 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({
type: TOKEN_EMAIL_MFA,
@@ -119,12 +130,13 @@ export const login2 = async (req: Request, res: Response) => {
});
return res.status(200).send({
mfaEnabled: true
mfaEnabled: true,
token
});
}
// issue tokens
const tokens = await issueTokens({ userId: user._id.toString() });
const tokens = await issueAuthTokens({ userId: user._id.toString() });
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
@@ -136,18 +148,41 @@ export const login2 = async (req: Request, res: Response) => {
// case: user does not have MFA enabled
// 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,
encryptionVersion: user.encryptionVersion,
protectedKey: user.protectedKey ?? null,
protectedKeyIV: user.protectedKeyIV ?? null,
protectedKeyTag: user.protectedKeyTag ?? null,
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
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({
@@ -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
* 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');
// issue tokens
const tokens = await issueTokens({ userId: user._id.toString() });
const tokens = await issueAuthTokens({ userId: user._id.toString() });
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
@@ -196,7 +268,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
sameSite: 'strict',
secure: NODE_ENV === 'production' ? true : false
});
// case: user does not have MFA enabled
// return (access) token in response
return res.status(200).send({
@@ -217,4 +289,5 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
message: 'Failed to authenticate. Try again?'
});
}
}
}
@@ -5,12 +5,10 @@ import { completeAccount } from '../../helpers/user';
import {
initializeDefaultOrg
} from '../../helpers/signup';
import { issueTokens } from '../../helpers/auth';
import { issueAuthTokens } from '../../helpers/auth';
import { INVITED, ACCEPTED } from '../../variables';
import axios from 'axios';
// TODO: finish
/**
* Complete setting up user by adding their personal and auth information as part of the
* signup flow
@@ -102,7 +100,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
);
// issue tokens
const tokens = await issueTokens({
const tokens = await issueAuthTokens({
userId: user._id.toString()
});
@@ -216,7 +214,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
);
// issue tokens
const tokens = await issueTokens({
const tokens = await issueAuthTokens({
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.refreshToken - issued refresh token
*/
const issueTokens = async ({ userId }: { userId: string }) => {
const issueAuthTokens = async ({ userId }: { userId: string }) => {
let token: string;
let refreshToken: string;
try {
@@ -298,6 +298,6 @@ export {
getAuthSTDPayload,
getAuthAPIKeyPayload,
createToken,
issueTokens,
issueAuthTokens,
clearTokens
};
+1 -15
View File
@@ -2,9 +2,7 @@ import * as Sentry from '@sentry/node';
import { IUser } from '../models';
import { createOrganization } from './organization';
import { addMembershipsOrg } from './membershipOrg';
import { createWorkspace } from './workspace';
import { addMemberships } from './membership';
import { OWNER, ADMIN, ACCEPTED } from '../variables';
import { OWNER, ACCEPTED } from '../variables';
import { sendMail } from '../helpers/nodemailer';
import { TokenService } from '../services';
import { TOKEN_EMAIL_CONFIRMATION } from '../variables';
@@ -95,18 +93,6 @@ const initializeDefaultOrg = async ({
roles: [OWNER],
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) {
throw new Error(`Failed to initialize default organization and workspace [err=${err}]`);
}
+2
View File
@@ -1,4 +1,5 @@
import requireAuth from './requireAuth';
import requireMfaAuth from './requireMfaAuth';
import requireBotAuth from './requireBotAuth';
import requireSignupAuth from './requireSignupAuth';
import requireWorkspaceAuth from './requireWorkspaceAuth';
@@ -15,6 +16,7 @@ import validateRequest from './validateRequest';
export {
requireAuth,
requireMfaAuth,
requireBotAuth,
requireSignupAuth,
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';
const router = express.Router();
import { body } from 'express-validator';
import { validateRequest } from '../../middleware';
import { requireMfaAuth, validateRequest } from '../../middleware';
import { authController } from '../../controllers/v2';
import { authLimiter } from '../../helpers/rateLimiter';
router.post(
'/login1',
authLimiter,
body('email').exists().trim().notEmpty(),
body('clientPublicKey').exists().trim().notEmpty(),
body('email').isString().trim().notEmpty(),
body('clientPublicKey').isString().trim().notEmpty(),
validateRequest,
authController.login1
);
@@ -17,19 +17,28 @@ router.post(
router.post(
'/login2',
authLimiter,
body('email').exists().trim().notEmpty(),
body('clientProof').exists().trim().notEmpty(),
body('email').isString().trim().notEmpty(),
body('clientProof').isString().trim().notEmpty(),
validateRequest,
authController.login2
);
router.post(
'/mfa',
authLimiter,
body('email').exists().trim().notEmpty(),
body('mfaToken').exists().trim().notEmpty(),
validateRequest,
authController.verifyMfaToken
'/mfa/send',
authLimiter,
body('email').isString().trim().notEmpty(),
validateRequest,
authController.sendMfaToken
);
router.post(
'/mfa/verify',
authLimiter,
requireMfaAuth,
body('email').isString().trim().notEmpty(),
body('mfaToken').isString().trim().notEmpty(),
validateRequest,
authController.verifyMfaToken
);
export default router;