Finish MFA v1 and refactor all tokens into separate TokenService with modified collection

This commit is contained in:
Tuan Dang
2023-01-23 22:10:15 +07:00
parent 00fee63ff3
commit 5cadb9e2f9
25 changed files with 692 additions and 98 deletions

View File

@ -1,7 +1,7 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { patchRouterParam } = require('./utils/patchAsyncRoutes');
import express, { Request, Response } from 'express';
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import cookieParser from 'cookie-parser';
@ -42,6 +42,7 @@ import {
integrationAuth as v1IntegrationAuthRouter
} from './routes/v1';
import {
auth as v2AuthRouter,
users as v2UsersRouter,
organizations as v2OrganizationsRouter,
workspace as v2WorkspaceRouter,
@ -109,6 +110,7 @@ app.use('/api/v1/integration', v1IntegrationRouter);
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
// v2 routes
app.use('/api/v2/auth', v2AuthRouter);
app.use('/api/v2/users', v2UsersRouter);
app.use('/api/v2/organizations', v2OrganizationsRouter);
app.use('/api/v2/workspace', v2EnvironmentRouter);

View File

@ -1,14 +1,13 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import crypto from 'crypto';
import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
import { MembershipOrg, Organization, User, Token } from '../../models';
import { MembershipOrg, Organization, User } from '../../models';
import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
import { checkEmailVerification } from '../../helpers/signup';
import { createToken } from '../../helpers/auth';
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
import { sendMail } from '../../helpers/nodemailer';
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED } from '../../variables';
import { TokenService } from '../../services';
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables';
/**
* Delete organization membership with id [membershipOrgId] from organization
@ -165,17 +164,11 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
const organization = await Organization.findOne({ _id: organizationId });
if (organization) {
const token = crypto.randomBytes(16).toString('hex');
await Token.findOneAndUpdate(
{ email: inviteeEmail },
{
email: inviteeEmail,
token,
createdAt: new Date()
},
{ upsert: true, new: true }
);
const token = await TokenService.createToken({
type: TOKEN_EMAIL_ORG_INVITATION,
email: inviteeEmail,
organizationId: organization._id
});
await sendMail({
template: 'organizationInvitation.handlebars',
@ -227,10 +220,12 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
if (!membershipOrg)
throw new Error('Failed to find any invitations for email');
await checkEmailVerification({
await TokenService.validateToken({
type: TOKEN_EMAIL_ORG_INVITATION,
email,
code
organizationId: membershipOrg.organization,
token: code
});
if (user && user?.publicKey) {

View File

@ -1,14 +1,14 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import crypto from 'crypto';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require('jsrp');
import * as bigintConversion from 'bigint-conversion';
import { User, Token, BackupPrivateKey } from '../../models';
import { checkEmailVerification } from '../../helpers/signup';
import { User, BackupPrivateKey } from '../../models';
import { createToken } from '../../helpers/auth';
import { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services';
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config';
import { TOKEN_EMAIL_PASSWORD_RESET } from '../../variables';
const clientPublicKeys: any = {};
@ -33,17 +33,10 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
});
}
const token = crypto.randomBytes(16).toString('hex');
await Token.findOneAndUpdate(
{ email },
{
email,
token,
createdAt: new Date()
},
{ upsert: true, new: true }
);
const token = await TokenService.createToken({
type: TOKEN_EMAIL_PASSWORD_RESET,
email
});
await sendMail({
template: 'passwordReset.handlebars',
@ -55,7 +48,6 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
callback_url: SITE_URL + '/password-reset'
}
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
@ -88,10 +80,11 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
error: 'Failed email verification for password reset'
});
}
await checkEmailVerification({
await TokenService.validateToken({
type: TOKEN_EMAIL_PASSWORD_RESET,
email,
code
token: code
});
// generate temporary password-reset token

View File

@ -1,6 +1,6 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
import { User, MembershipOrg } from '../../models';
import { completeAccount } from '../../helpers/user';
import {

View File

@ -0,0 +1,216 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { Request, Response } from 'express';
import jwt from 'jsonwebtoken';
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 { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services';
import {
NODE_ENV
} from '../../config';
import {
TOKEN_EMAIL_MFA
} from '../../variables';
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
userId: string;
}
}
const clientPublicKeys: any = {};
/**
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
* @param req
* @param res
* @returns
*/
export const login1 = async (req: Request, res: Response) => {
try {
const {
email,
clientPublicKey
}: { email: string; clientPublicKey: string } = req.body;
const user = await User.findOne({
email
}).select('+salt +verifier');
if (!user) throw new Error('Failed to find user');
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier
},
() => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
clientPublicKeys[email] = {
clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt)
};
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to start authentication process'
});
}
};
/**
* Log in user step 2: complete step 2 of SRP protocol and return token and their (encrypted)
* private key
* @param req
* @param res
* @returns
*/
export const login2 = async (req: Request, res: Response) => {
// check to see if user has MFA enabled; if yes then issue MFA-token
// TODO: may have to figure out a better token system for tokens with varying expirations
// (e.g. for org-invitations vs. auth etc.)
try {
const { email, clientProof } = req.body;
const user = await User.findOne({
email
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
if (!user) throw new Error('Failed to find user');
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: clientPublicKeys[email].serverBInt
},
async () => {
server.setClientPublicKey(clientPublicKeys[email].clientPublicKey);
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
if (user.isMfaEnabled) {
// case: user has MFA enabled
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
}
});
return res.status(200).send({
mfaEnabled: true
});
}
// issue tokens
const tokens = await issueTokens({ userId: user._id.toString() });
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
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({
mfaEnabled: false,
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
});
}
return res.status(400).send({
message: 'Failed to authenticate. Try again?'
});
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to authenticate. Try again?'
});
}
};
/**
* Verify MFA token [mfaToken] and issue JWT and refresh tokens if the
* MFA token [mfaToken] is valid
* @param req
* @param res
*/
export const verifyMfaToken = async (req: Request, res: Response) => {
try {
const { email, mfaToken } = req.body;
await TokenService.validateToken({
type: TOKEN_EMAIL_MFA,
email,
token: mfaToken
});
const user = await User.findOne({
email
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
if (!user) throw new Error('Failed to find user');
// issue tokens
const tokens = await issueTokens({ userId: user._id.toString() });
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
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({
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to authenticate. Try again?'
});
}
}

View File

@ -1,3 +1,4 @@
import * as authController from './authController';
import * as usersController from './usersController';
import * as organizationsController from './organizationsController';
import * as workspaceController from './workspaceController';
@ -8,6 +9,7 @@ import * as secretsController from './secretsController';
import * as environmentController from './environmentController';
export {
authController,
usersController,
organizationsController,
workspaceController,

View File

@ -55,6 +55,35 @@ export const getMe = async (req: Request, res: Response) => {
});
}
/**
* Update the current user's MFA-enabled status [isMfaEnabled].
* Note: Infisical currently only supports email-based 2FA only; this will expand to
* include SMS and authenticator app modes of authentication in the future.
* @param req
* @param res
* @returns
*/
export const updateMyMfaEnabled = async (req: Request, res: Response) => {
let user;
try {
const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body;
req.user.isMfaEnabled = isMfaEnabled;
await req.user.save();
user = req.user;
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to update current user's MFA status"
});
}
return res.status(200).send({
user
});
}
/**
* Return organizations that the current user is part of.
* @param req

View File

@ -1,12 +1,13 @@
import * as Sentry from '@sentry/node';
import crypto from 'crypto';
import { Token, IToken, IUser } from '../models';
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 { sendMail } from '../helpers/nodemailer';
import { TokenService } from '../services';
import { TOKEN_EMAIL_CONFIRMATION } from '../variables';
/**
* Send magic link to verify email to [email]
@ -14,21 +15,13 @@ import { sendMail } from '../helpers/nodemailer';
* @param {Object} obj
* @param {String} obj.email - email
* @returns {Boolean} success - whether or not operation was successful
*
*/
const sendEmailVerification = async ({ email }: { email: string }) => {
try {
const token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
await Token.findOneAndUpdate(
{ email },
{
email,
token,
createdAt: new Date()
},
{ upsert: true, new: true }
);
const token = await TokenService.createToken({
type: TOKEN_EMAIL_CONFIRMATION,
email
});
// send mail
await sendMail({
@ -62,12 +55,11 @@ const checkEmailVerification = async ({
code: string;
}) => {
try {
const token = await Token.findOneAndDelete({
await TokenService.validateToken({
type: TOKEN_EMAIL_CONFIRMATION,
email,
token: code
});
if (!token) throw new Error('Failed to find email verification token');
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);

View File

@ -0,0 +1,162 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { TokenData } from '../models';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import {
TOKEN_EMAIL_CONFIRMATION,
TOKEN_EMAIL_MFA,
TOKEN_EMAIL_ORG_INVITATION,
TOKEN_EMAIL_PASSWORD_RESET
} from '../variables';
import {
SALT_ROUNDS
} from '../config';
import { UnauthorizedRequestError } from '../utils/errors';
/**
* Create and store a token in the database for purpose [type]
* @param {Object} obj
* @param {String} obj.type
* @param {String} obj.email
* @param {String} obj.phoneNumber
* @param {Types.ObjectId} obj.organizationId
* @returns {String} token - the created token
*/
const createTokenHelper = async ({
type,
email,
phoneNumber,
organizationId
}: {
type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset';
email?: string;
phoneNumber?: string;
organizationId?: Types.ObjectId
}) => {
let token, expiresAt;
try {
// generate random token based on specified token use-case
// type [type]
switch (type) {
case TOKEN_EMAIL_CONFIRMATION:
// generate random 6-digit code
token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
expiresAt = new Date((new Date()).getTime() + 86400000);
break;
case TOKEN_EMAIL_MFA:
// generate random 6-digit code
token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
expiresAt = new Date((new Date()).getTime() + 300000);
break;
case TOKEN_EMAIL_ORG_INVITATION:
// generate random hex
token = crypto.randomBytes(16).toString('hex');
expiresAt = new Date((new Date()).getTime() + 259200000);
break;
case TOKEN_EMAIL_PASSWORD_RESET:
// generate random hex
token = crypto.randomBytes(16).toString('hex');
expiresAt = new Date((new Date()).getTime() + 86400000);
break;
default:
token = crypto.randomBytes(16).toString('hex');
expiresAt = new Date();
break;
}
interface Query {
type: string;
email?: string;
phoneNumber?: string;
organization?: Types.ObjectId;
}
const query: Query = { type };
if (email) { query.email = email; }
if (phoneNumber) { query.phoneNumber = phoneNumber; }
if (organizationId) { query.organization = organizationId }
await TokenData.findOneAndUpdate(
query,
{
type,
email,
phoneNumber,
organization: organizationId,
tokenHash: await bcrypt.hash(token, SALT_ROUNDS),
expiresAt
},
{
new: true,
upsert: true
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error(
"Failed to create token"
);
}
return token;
}
/**
*
* @param {Object} obj
* @param {String} obj.email - email associated with the token
* @param {String} obj.token - value of the token
*/
const validateTokenHelper = async ({
type,
email,
phoneNumber,
organizationId,
token
}: {
type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset';
email?: string;
phoneNumber?: string;
organizationId?: Types.ObjectId;
token: string;
}) => {
try {
interface Query {
type: string;
email?: string;
phoneNumber?: string;
organization?: Types.ObjectId;
}
const query: Query = { type };
if (email) { query.email = email; }
if (phoneNumber) { query.phoneNumber = phoneNumber; }
if (organizationId) { query.organization = organizationId; }
const tokenData = await TokenData.findOneAndDelete(query);
if (!tokenData) throw new Error('Failed to find token to validate');
if (tokenData.expiresAt < new Date()) throw new Error('Token has expired');
const isValid = await bcrypt.compare(token, tokenData.tokenHash);
if (!isValid) throw UnauthorizedRequestError({
message: 'Failed token data validation due to incorrect token'
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error(
"Failed to validate token data"
);
}
}
export {
createTokenHelper,
validateTokenHelper
}

View File

@ -10,7 +10,7 @@ import MembershipOrg, { IMembershipOrg } from './membershipOrg';
import Organization, { IOrganization } from './organization';
import Secret, { ISecret } from './secret';
import ServiceToken, { IServiceToken } from './serviceToken';
import Token, { IToken } from './token';
import TokenData, { ITokenData } from './tokenData';
import User, { IUser } from './user';
import UserAction, { IUserAction } from './userAction';
import Workspace, { IWorkspace } from './workspace';
@ -42,8 +42,8 @@ export {
ISecret,
ServiceToken,
IServiceToken,
Token,
IToken,
TokenData,
ITokenData,
User,
IUser,
UserAction,

View File

@ -1,33 +0,0 @@
import { Schema, model } from 'mongoose';
import { EMAIL_TOKEN_LIFETIME } from '../config';
export interface IToken {
email: string;
token: string;
createdAt: Date;
}
const tokenSchema = new Schema<IToken>({
email: {
type: String,
required: true
},
token: {
type: String,
required: true
},
createdAt: {
type: Date,
default: Date.now
}
});
tokenSchema.index({
createdAt: 1
}, {
expireAfterSeconds: parseInt(EMAIL_TOKEN_LIFETIME)
});
const Token = model<IToken>('Token', tokenSchema);
export default Token;

View File

@ -0,0 +1,57 @@
import { Schema, Types, model } from 'mongoose';
export interface ITokenData {
type: string;
email?: string;
phoneNumber?: string;
organization?: Types.ObjectId;
tokenHash: string;
expiresAt: Date;
createdAt: Date;
updatedAt: Date;
}
const tokenDataSchema = new Schema<ITokenData>({
type: {
type: String,
enum: [
'emailConfirmation',
'emailMfa',
'organizationInvitation',
'passwordReset'
],
required: true
},
email: {
type: String
},
phoneNumber: {
type: String
},
organization: { // organizationInvitation-specific field
type: Schema.Types.ObjectId,
ref: 'Organization'
},
tokenHash: {
type: String,
select: false,
required: true
},
expiresAt: {
type: Date,
expires: 0,
required: true
}
}, {
timestamps: true
});
tokenDataSchema.index({
expiresAt: 1
}, {
expireAfterSeconds: 0
});
const TokenData = model<ITokenData>('TokenData', tokenDataSchema);
export default TokenData;

View File

@ -1,4 +1,5 @@
import { Schema, model, Types } from 'mongoose';
import { MFA_METHOD_EMAIL } from '../variables';
export interface IUser {
_id: Types.ObjectId;
@ -12,6 +13,7 @@ export interface IUser {
salt?: string;
verifier?: string;
refreshVersion?: number;
isMfaEnabled: boolean;
}
const userSchema = new Schema<IUser>(
@ -54,6 +56,10 @@ const userSchema = new Schema<IUser>(
type: Number,
default: 0,
select: false
},
isMfaEnabled: {
type: Boolean,
default: false
}
},
{

View File

@ -7,7 +7,7 @@ import { authLimiter } from '../../helpers/rateLimiter';
router.post('/token', validateRequest, authController.getNewToken);
router.post(
router.post( // deprecated (moved to api/v2/auth/login1)
'/login1',
authLimiter,
body('email').exists().trim().notEmpty(),
@ -16,7 +16,7 @@ router.post(
authController.login1
);
router.post(
router.post( // deprecated (moved to api/v2/auth/login2)
'/login2',
authLimiter,
body('email').exists().trim().notEmpty(),

View File

@ -31,7 +31,7 @@ router.patch(
requireBotAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
body('isActive').isBoolean(),
body('isActive').exists().isBoolean(),
body('botKey'),
validateRequest,
botController.setBotActiveState

View File

@ -0,0 +1,35 @@
import express from 'express';
const router = express.Router();
import { body } from 'express-validator';
import { 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(),
validateRequest,
authController.login1
);
router.post(
'/login2',
authLimiter,
body('email').exists().trim().notEmpty(),
body('clientProof').exists().trim().notEmpty(),
validateRequest,
authController.login2
);
router.post(
'/mfa',
authLimiter,
body('email').exists().trim().notEmpty(),
body('mfaToken').exists().trim().notEmpty(),
validateRequest,
authController.verifyMfaToken
);
export default router;

View File

@ -1,3 +1,4 @@
import auth from './auth';
import users from './users';
import organizations from './organizations';
import workspace from './workspace';
@ -8,6 +9,7 @@ import apiKeyData from './apiKeyData';
import environment from "./environment"
export {
auth,
users,
organizations,
workspace,

View File

@ -1,8 +1,10 @@
import express from 'express';
const router = express.Router();
import {
requireAuth
requireAuth,
validateRequest
} from '../../middleware';
import { body, param } from 'express-validator';
import { usersController } from '../../controllers/v2';
router.get(
@ -13,6 +15,16 @@ router.get(
usersController.getMe
);
router.patch(
'/me/mfa',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
body('isMfaEnabled').exists().isBoolean(),
validateRequest,
usersController.updateMyMfaEnabled
);
router.get(
'/me/organizations',
requireAuth({

View File

@ -0,0 +1,69 @@
import { Types } from 'mongoose';
import { createTokenHelper, validateTokenHelper } from '../helpers/token';
/**
* Class to handle token actions
* TODO: elaborate more on this class
*/
class TokenService {
/**
* Create a token [token] for type [type] with associated details
* @param {Object} obj
* @param {String} obj.type - type or context of token (e.g. emailConfirmation)
* @param {String} obj.email - email associated with the token
* @param {String} obj.phoneNumber - phone number associated with the token
* @param {Types.ObjectId} obj.organizationId - id of organization associated with the token
* @returns {String} token - the token to create
*/
static async createToken({
type,
email,
phoneNumber,
organizationId
}: {
type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset';
email?: string;
phoneNumber?: string;
organizationId?: Types.ObjectId;
}) {
return await createTokenHelper({
type,
email,
phoneNumber,
organizationId
});
}
/**
* Validate whether or not token [token] and its associated details match a token in the DB
* @param {Object} obj
* @param {String} obj.type - type or context of token (e.g. emailConfirmation)
* @param {String} obj.email - email associated with the token
* @param {String} obj.phoneNumber - phone number associated with the token
* @param {Types.ObjectId} obj.organizationId - id of organization associated with the token
* @param {String} obj.token - the token to validate
*/
static async validateToken({
type,
email,
phoneNumber,
organizationId,
token
}: {
type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset';
email?: string;
phoneNumber?: string;
organizationId?: Types.ObjectId;
token: string;
}) {
return await validateTokenHelper({
type,
email,
phoneNumber,
organizationId,
token
});
}
}
export default TokenService;

View File

@ -3,11 +3,13 @@ import postHogClient from './PostHogClient';
import BotService from './BotService';
import EventService from './EventService';
import IntegrationService from './IntegrationService';
import TokenService from './TokenService';
export {
DatabaseService,
postHogClient,
BotService,
EventService,
IntegrationService
IntegrationService,
TokenService
}

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>MFA Code</title>
</head>
<body>
<h2>Infisical</h2>
<h2>Sign in attempt requires further verification</h2>
<p>Your MFA code is below — enter it where you started signing in to Infisical.</p>
<h2>{{code}}</h2>
<p>The MFA code will be valid for 2 minutes.</p>
<p>Not you? Contact Infisical or your administrator immediately.</p>
</body>
</html>

View File

@ -1,15 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Email Verification</title>
<title></title>
</head>
<body>
<h2>Infisical</h2>
<h2>Confirm your email address</h2>
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.
</p>
<h2>{{code}}</h2>
<p>Questions about setting up Infisical? Email us at support@infisical.com</p>
</body>
</html>

View File

@ -42,6 +42,15 @@ import {
} from './action';
import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from './smtp';
import { PLAN_STARTER, PLAN_PRO } from './stripe';
import {
MFA_METHOD_EMAIL
} from './user';
import {
TOKEN_EMAIL_CONFIRMATION,
TOKEN_EMAIL_MFA,
TOKEN_EMAIL_ORG_INVITATION,
TOKEN_EMAIL_PASSWORD_RESET
} from './token';
export {
OWNER,
@ -84,4 +93,9 @@ export {
SMTP_HOST_MAILGUN,
PLAN_STARTER,
PLAN_PRO,
MFA_METHOD_EMAIL,
TOKEN_EMAIL_CONFIRMATION,
TOKEN_EMAIL_MFA,
TOKEN_EMAIL_ORG_INVITATION,
TOKEN_EMAIL_PASSWORD_RESET
};

View File

@ -0,0 +1,11 @@
const TOKEN_EMAIL_CONFIRMATION = 'emailConfirmation';
const TOKEN_EMAIL_MFA = 'emailMfa';
const TOKEN_EMAIL_ORG_INVITATION = 'organizationInvitation';
const TOKEN_EMAIL_PASSWORD_RESET = 'passwordReset';
export {
TOKEN_EMAIL_CONFIRMATION,
TOKEN_EMAIL_MFA,
TOKEN_EMAIL_ORG_INVITATION,
TOKEN_EMAIL_PASSWORD_RESET
}

View File

@ -0,0 +1,5 @@
const MFA_METHOD_EMAIL = 'email';
export {
MFA_METHOD_EMAIL
}