mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-16 03:31:27 +00:00
218 lines
5.2 KiB
TypeScript
218 lines
5.2 KiB
TypeScript
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 { UnauthorizedRequestError } from "../utils/errors";
|
|
import { getSaltRounds } from "../config";
|
|
|
|
/**
|
|
* 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, triesLeft;
|
|
// 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));
|
|
triesLeft = 5;
|
|
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 TokenDataQuery {
|
|
type: string;
|
|
email?: string;
|
|
phoneNumber?: string;
|
|
organization?: Types.ObjectId;
|
|
}
|
|
|
|
interface TokenDataUpdate {
|
|
type: string;
|
|
email?: string;
|
|
phoneNumber?: string;
|
|
organization?: Types.ObjectId;
|
|
tokenHash: string;
|
|
triesLeft?: number;
|
|
expiresAt: Date;
|
|
}
|
|
|
|
const query: TokenDataQuery = { type };
|
|
const update: TokenDataUpdate = {
|
|
type,
|
|
tokenHash: await bcrypt.hash(token, await getSaltRounds()),
|
|
expiresAt,
|
|
};
|
|
|
|
if (email) {
|
|
query.email = email;
|
|
update.email = email;
|
|
}
|
|
if (phoneNumber) {
|
|
query.phoneNumber = phoneNumber;
|
|
update.phoneNumber = phoneNumber;
|
|
}
|
|
if (organizationId) {
|
|
query.organization = organizationId;
|
|
update.organization = organizationId;
|
|
}
|
|
|
|
if (triesLeft) {
|
|
update.triesLeft = triesLeft;
|
|
}
|
|
|
|
await TokenData.findOneAndUpdate(query, update, {
|
|
new: true,
|
|
upsert: true,
|
|
});
|
|
|
|
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;
|
|
}) => {
|
|
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.findOne(query).select("+tokenHash");
|
|
|
|
if (!tokenData) throw new Error("Failed to find token to validate");
|
|
|
|
if (tokenData.expiresAt < new Date()) {
|
|
// case: token expired
|
|
await TokenData.findByIdAndDelete(tokenData._id);
|
|
throw UnauthorizedRequestError({
|
|
message: "MFA session expired. Please log in again",
|
|
context: {
|
|
code: "mfa_expired",
|
|
},
|
|
});
|
|
}
|
|
|
|
const isValid = await bcrypt.compare(token, tokenData.tokenHash);
|
|
if (!isValid) {
|
|
// case: token is not valid
|
|
if (tokenData?.triesLeft !== undefined) {
|
|
// case: token has a try-limit
|
|
if (tokenData.triesLeft === 1) {
|
|
// case: token is out of tries
|
|
await TokenData.findByIdAndDelete(tokenData._id);
|
|
} else {
|
|
// case: token has more than 1 try left
|
|
await TokenData.findByIdAndUpdate(
|
|
tokenData._id,
|
|
{
|
|
triesLeft: tokenData.triesLeft - 1,
|
|
},
|
|
{
|
|
new: true,
|
|
}
|
|
);
|
|
}
|
|
|
|
throw UnauthorizedRequestError({
|
|
message: "MFA code is invalid",
|
|
context: {
|
|
code: "mfa_invalid",
|
|
triesLeft: tokenData.triesLeft - 1,
|
|
},
|
|
});
|
|
}
|
|
|
|
throw UnauthorizedRequestError({
|
|
message: "MFA code is invalid",
|
|
context: {
|
|
code: "mfa_invalid",
|
|
},
|
|
});
|
|
}
|
|
|
|
// case: token is valid
|
|
await TokenData.findByIdAndDelete(tokenData._id);
|
|
};
|
|
|
|
export { createTokenHelper, validateTokenHelper };
|