mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 18:29:07 +00:00
539 lines
20 KiB
TypeScript
539 lines
20 KiB
TypeScript
import bcrypt from "bcrypt";
|
|
import jwt from "jsonwebtoken";
|
|
|
|
import { OrgMembershipStatus, SecretKeyEncoding, TableName } from "@app/db/schemas";
|
|
import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns";
|
|
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
|
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
|
import { getConfig } from "@app/lib/config/env";
|
|
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
|
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
|
|
import { BadRequestError } from "@app/lib/errors";
|
|
import { isDisposableEmail } from "@app/lib/validator";
|
|
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
|
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
|
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
|
|
|
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
|
import { TokenType } from "../auth-token/auth-token-types";
|
|
import { TOrgDALFactory } from "../org/org-dal";
|
|
import { TOrgServiceFactory } from "../org/org-service";
|
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
|
import { TUserDALFactory } from "../user/user-dal";
|
|
import { UserEncryption } from "../user/user-types";
|
|
import { TAuthDALFactory } from "./auth-dal";
|
|
import { validateProviderAuthToken, validateSignUpAuthorization } from "./auth-fns";
|
|
import { TCompleteAccountInviteDTO, TCompleteAccountSignupDTO } from "./auth-signup-type";
|
|
import { AuthMethod, AuthTokenType } from "./auth-type";
|
|
|
|
type TAuthSignupDep = {
|
|
authDAL: TAuthDALFactory;
|
|
userDAL: TUserDALFactory;
|
|
userGroupMembershipDAL: Pick<
|
|
TUserGroupMembershipDALFactory,
|
|
| "find"
|
|
| "transaction"
|
|
| "insertMany"
|
|
| "deletePendingUserGroupMembershipsByUserIds"
|
|
| "findUserGroupMembershipsInProject"
|
|
>;
|
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
|
|
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById">;
|
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
|
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
|
orgDAL: TOrgDALFactory;
|
|
tokenService: TAuthTokenServiceFactory;
|
|
smtpService: TSmtpService;
|
|
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
|
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "transaction" | "insertMany">;
|
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
|
|
};
|
|
|
|
export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>;
|
|
export const authSignupServiceFactory = ({
|
|
authDAL,
|
|
userDAL,
|
|
userGroupMembershipDAL,
|
|
projectKeyDAL,
|
|
projectDAL,
|
|
projectBotDAL,
|
|
groupProjectDAL,
|
|
tokenService,
|
|
smtpService,
|
|
orgService,
|
|
orgDAL,
|
|
licenseService
|
|
}: TAuthSignupDep) => {
|
|
// first step of signup. create user and send email
|
|
const beginEmailSignupProcess = async (email: string) => {
|
|
const isEmailInvalid = await isDisposableEmail(email);
|
|
if (isEmailInvalid) {
|
|
throw new Error("Provided a disposable email");
|
|
}
|
|
|
|
let user = await userDAL.findUserByUsername(email);
|
|
if (user && user.isAccepted) {
|
|
// TODO(akhilmhdh-pg): copy as old one. this needs to be changed due to security issues
|
|
throw new Error("Failed to send verification code for complete account");
|
|
}
|
|
if (!user) {
|
|
user = await userDAL.create({ authMethods: [AuthMethod.EMAIL], username: email, email, isGhost: false });
|
|
}
|
|
if (!user) throw new Error("Failed to create user");
|
|
|
|
const token = await tokenService.createTokenForUser({
|
|
type: TokenType.TOKEN_EMAIL_CONFIRMATION,
|
|
userId: user.id
|
|
});
|
|
|
|
await smtpService.sendMail({
|
|
template: SmtpTemplates.SignupEmailVerification,
|
|
subjectLine: "Infisical confirmation code",
|
|
recipients: [user.email as string],
|
|
substitutions: {
|
|
code: token
|
|
}
|
|
});
|
|
};
|
|
|
|
const verifyEmailSignup = async (email: string, code: string) => {
|
|
const user = await userDAL.findUserByUsername(email);
|
|
if (!user || (user && user.isAccepted)) {
|
|
// TODO(akhilmhdh): copy as old one. this needs to be changed due to security issues
|
|
throw new Error("Failed to send verification code for complete account");
|
|
}
|
|
const appCfg = getConfig();
|
|
await tokenService.validateTokenForUser({
|
|
type: TokenType.TOKEN_EMAIL_CONFIRMATION,
|
|
userId: user.id,
|
|
code
|
|
});
|
|
|
|
await userDAL.updateById(user.id, { isEmailVerified: true });
|
|
|
|
// generate jwt token this is a temporary token
|
|
const jwtToken = jwt.sign(
|
|
{
|
|
authTokenType: AuthTokenType.SIGNUP_TOKEN,
|
|
userId: user.id.toString()
|
|
},
|
|
appCfg.AUTH_SECRET,
|
|
{ expiresIn: appCfg.JWT_SIGNUP_LIFETIME }
|
|
);
|
|
|
|
return { user, token: jwtToken };
|
|
};
|
|
|
|
const completeEmailAccountSignup = async ({
|
|
email,
|
|
password,
|
|
firstName,
|
|
lastName,
|
|
providerAuthToken,
|
|
salt,
|
|
verifier,
|
|
publicKey,
|
|
protectedKey,
|
|
protectedKeyIV,
|
|
protectedKeyTag,
|
|
organizationName,
|
|
// attributionSource,
|
|
encryptedPrivateKey,
|
|
encryptedPrivateKeyIV,
|
|
encryptedPrivateKeyTag,
|
|
ip,
|
|
userAgent,
|
|
authorization
|
|
}: TCompleteAccountSignupDTO) => {
|
|
const appCfg = getConfig();
|
|
const user = await userDAL.findOne({ username: email });
|
|
if (!user || (user && user.isAccepted)) {
|
|
throw new Error("Failed to complete account for complete user");
|
|
}
|
|
|
|
let organizationId: string | null = null;
|
|
let authMethod: AuthMethod | null = null;
|
|
if (providerAuthToken) {
|
|
const { orgId, authMethod: userAuthMethod } = validateProviderAuthToken(providerAuthToken, user.username);
|
|
authMethod = userAuthMethod;
|
|
organizationId = orgId;
|
|
} else {
|
|
validateSignUpAuthorization(authorization, user.id);
|
|
}
|
|
|
|
const hashedPassword = await bcrypt.hash(password, appCfg.BCRYPT_SALT_ROUND);
|
|
const privateKey = await getUserPrivateKey(password, {
|
|
salt,
|
|
protectedKey,
|
|
protectedKeyIV,
|
|
protectedKeyTag,
|
|
encryptedPrivateKey,
|
|
iv: encryptedPrivateKeyIV,
|
|
tag: encryptedPrivateKeyTag,
|
|
encryptionVersion: UserEncryption.V2
|
|
});
|
|
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey);
|
|
const updateduser = await authDAL.transaction(async (tx) => {
|
|
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
|
|
if (!us) throw new Error("User not found");
|
|
const systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx);
|
|
let userEncKey;
|
|
|
|
// below condition is true means this is system generated credentials
|
|
// the private key is actually system generated password
|
|
// thus we will re-encrypt the system generated private key with the new password
|
|
// akhilmhdh: you may find this like why? The reason is simple we are moving away from e2ee and these are pieces of it
|
|
// without a dummy key in place some things will break and backward compatiability too. 2025 we will be removing all these things
|
|
if (
|
|
systemGeneratedUserEncryptionKey &&
|
|
!systemGeneratedUserEncryptionKey.hashedPassword &&
|
|
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey &&
|
|
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag &&
|
|
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV &&
|
|
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding
|
|
) {
|
|
// get server generated password
|
|
const serverGeneratedPassword = infisicalSymmetricDecrypt({
|
|
iv: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV,
|
|
tag: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag,
|
|
ciphertext: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey,
|
|
keyEncoding: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
|
|
});
|
|
const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, {
|
|
...systemGeneratedUserEncryptionKey
|
|
});
|
|
const encKeys = await generateUserSrpKeys(email, password, {
|
|
publicKey: systemGeneratedUserEncryptionKey.publicKey,
|
|
privateKey: serverGeneratedPrivateKey
|
|
});
|
|
// now reencrypt server generated key with user provided password
|
|
userEncKey = await userDAL.upsertUserEncryptionKey(
|
|
us.id,
|
|
{
|
|
encryptionVersion: UserEncryption.V2,
|
|
protectedKey: encKeys.protectedKey,
|
|
protectedKeyIV: encKeys.protectedKeyIV,
|
|
protectedKeyTag: encKeys.protectedKeyTag,
|
|
publicKey: encKeys.publicKey,
|
|
encryptedPrivateKey: encKeys.encryptedPrivateKey,
|
|
iv: encKeys.encryptedPrivateKeyIV,
|
|
tag: encKeys.encryptedPrivateKeyTag,
|
|
salt: encKeys.salt,
|
|
verifier: encKeys.verifier,
|
|
hashedPassword,
|
|
serverEncryptedPrivateKeyEncoding: encoding,
|
|
serverEncryptedPrivateKeyTag: tag,
|
|
serverEncryptedPrivateKeyIV: iv,
|
|
serverEncryptedPrivateKey: ciphertext
|
|
},
|
|
tx
|
|
);
|
|
} else {
|
|
userEncKey = await userDAL.upsertUserEncryptionKey(
|
|
us.id,
|
|
{
|
|
encryptionVersion: UserEncryption.V2,
|
|
salt,
|
|
verifier,
|
|
publicKey,
|
|
protectedKey,
|
|
protectedKeyIV,
|
|
protectedKeyTag,
|
|
encryptedPrivateKey,
|
|
iv: encryptedPrivateKeyIV,
|
|
tag: encryptedPrivateKeyTag,
|
|
hashedPassword,
|
|
serverEncryptedPrivateKeyEncoding: encoding,
|
|
serverEncryptedPrivateKeyTag: tag,
|
|
serverEncryptedPrivateKeyIV: iv,
|
|
serverEncryptedPrivateKey: ciphertext
|
|
},
|
|
tx
|
|
);
|
|
}
|
|
|
|
// If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it
|
|
if (
|
|
(isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod as AuthMethod)) &&
|
|
organizationId
|
|
) {
|
|
const [pendingOrgMembership] = await orgDAL.findMembership({
|
|
[`${TableName.OrgMembership}.userId` as "userId"]: user.id,
|
|
status: OrgMembershipStatus.Invited,
|
|
[`${TableName.OrgMembership}.orgId` as "orgId"]: organizationId
|
|
});
|
|
|
|
if (pendingOrgMembership) {
|
|
await orgDAL.updateMembershipById(
|
|
pendingOrgMembership.id,
|
|
{
|
|
status: OrgMembershipStatus.Accepted
|
|
},
|
|
tx
|
|
);
|
|
}
|
|
}
|
|
|
|
return { info: us, key: userEncKey };
|
|
});
|
|
|
|
if (!organizationId) {
|
|
const newOrganization = await orgService.createOrganization({
|
|
userId: user.id,
|
|
userEmail: user.email ?? user.username,
|
|
orgName: organizationName
|
|
});
|
|
|
|
if (!newOrganization) throw new Error("Failed to create organization");
|
|
|
|
organizationId = newOrganization.id;
|
|
}
|
|
|
|
const updatedMembersips = await orgDAL.updateMembership(
|
|
{ inviteEmail: email, status: OrgMembershipStatus.Invited },
|
|
{ userId: user.id, status: OrgMembershipStatus.Accepted }
|
|
);
|
|
const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))];
|
|
await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId)));
|
|
|
|
await convertPendingGroupAdditionsToGroupMemberships({
|
|
userIds: [user.id],
|
|
userDAL,
|
|
userGroupMembershipDAL,
|
|
groupProjectDAL,
|
|
projectKeyDAL,
|
|
projectDAL,
|
|
projectBotDAL
|
|
});
|
|
|
|
const tokenSession = await tokenService.getUserTokenSession({
|
|
userAgent,
|
|
ip,
|
|
userId: updateduser.info.id
|
|
});
|
|
if (!tokenSession) throw new Error("Failed to create token");
|
|
|
|
const accessToken = jwt.sign(
|
|
{
|
|
authMethod: authMethod || AuthMethod.EMAIL,
|
|
authTokenType: AuthTokenType.ACCESS_TOKEN,
|
|
userId: updateduser.info.id,
|
|
tokenVersionId: tokenSession.id,
|
|
accessVersion: tokenSession.accessVersion,
|
|
organizationId
|
|
},
|
|
appCfg.AUTH_SECRET,
|
|
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }
|
|
);
|
|
|
|
const refreshToken = jwt.sign(
|
|
{
|
|
authMethod: authMethod || AuthMethod.EMAIL,
|
|
authTokenType: AuthTokenType.REFRESH_TOKEN,
|
|
userId: updateduser.info.id,
|
|
tokenVersionId: tokenSession.id,
|
|
refreshVersion: tokenSession.refreshVersion,
|
|
organizationId
|
|
},
|
|
appCfg.AUTH_SECRET,
|
|
{ expiresIn: appCfg.JWT_REFRESH_LIFETIME }
|
|
);
|
|
|
|
return { user: updateduser.info, accessToken, refreshToken, organizationId };
|
|
};
|
|
|
|
/*
|
|
* User signup flow when they are invited to join the org
|
|
* */
|
|
const completeAccountInvite = async ({
|
|
ip,
|
|
salt,
|
|
email,
|
|
password,
|
|
verifier,
|
|
firstName,
|
|
publicKey,
|
|
userAgent,
|
|
lastName,
|
|
protectedKey,
|
|
protectedKeyIV,
|
|
protectedKeyTag,
|
|
encryptedPrivateKey,
|
|
encryptedPrivateKeyIV,
|
|
encryptedPrivateKeyTag,
|
|
authorization
|
|
}: TCompleteAccountInviteDTO) => {
|
|
const user = await userDAL.findUserByUsername(email);
|
|
if (!user || (user && user.isAccepted)) {
|
|
throw new Error("Failed to complete account for complete user");
|
|
}
|
|
|
|
validateSignUpAuthorization(authorization, user.id);
|
|
|
|
const [orgMembership] = await orgDAL.findMembership({
|
|
inviteEmail: email,
|
|
status: OrgMembershipStatus.Invited
|
|
});
|
|
if (!orgMembership)
|
|
throw new BadRequestError({
|
|
message: "Failed to find invitation for email",
|
|
name: "complete account invite"
|
|
});
|
|
|
|
const appCfg = getConfig();
|
|
const hashedPassword = await bcrypt.hash(password, appCfg.BCRYPT_SALT_ROUND);
|
|
const privateKey = await getUserPrivateKey(password, {
|
|
salt,
|
|
protectedKey,
|
|
protectedKeyIV,
|
|
protectedKeyTag,
|
|
encryptedPrivateKey,
|
|
iv: encryptedPrivateKeyIV,
|
|
tag: encryptedPrivateKeyTag,
|
|
encryptionVersion: 2
|
|
});
|
|
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey);
|
|
const updateduser = await authDAL.transaction(async (tx) => {
|
|
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
|
|
if (!us) throw new Error("User not found");
|
|
const systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx);
|
|
let userEncKey;
|
|
// this means this is system generated credentials
|
|
// now replace the private key
|
|
if (
|
|
systemGeneratedUserEncryptionKey &&
|
|
!systemGeneratedUserEncryptionKey.hashedPassword &&
|
|
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey &&
|
|
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag &&
|
|
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV &&
|
|
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding
|
|
) {
|
|
// get server generated password
|
|
const serverGeneratedPassword = infisicalSymmetricDecrypt({
|
|
iv: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV,
|
|
tag: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag,
|
|
ciphertext: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey,
|
|
keyEncoding: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
|
|
});
|
|
const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, {
|
|
...systemGeneratedUserEncryptionKey
|
|
});
|
|
const encKeys = await generateUserSrpKeys(email, password, {
|
|
publicKey: systemGeneratedUserEncryptionKey.publicKey,
|
|
privateKey: serverGeneratedPrivateKey
|
|
});
|
|
// now reencrypt server generated key with user provided password
|
|
userEncKey = await userDAL.upsertUserEncryptionKey(
|
|
us.id,
|
|
{
|
|
encryptionVersion: 2,
|
|
protectedKey: encKeys.protectedKey,
|
|
protectedKeyIV: encKeys.protectedKeyIV,
|
|
protectedKeyTag: encKeys.protectedKeyTag,
|
|
publicKey: encKeys.publicKey,
|
|
encryptedPrivateKey: encKeys.encryptedPrivateKey,
|
|
iv: encKeys.encryptedPrivateKeyIV,
|
|
tag: encKeys.encryptedPrivateKeyTag,
|
|
salt: encKeys.salt,
|
|
verifier: encKeys.verifier,
|
|
hashedPassword,
|
|
serverEncryptedPrivateKeyEncoding: encoding,
|
|
serverEncryptedPrivateKeyTag: tag,
|
|
serverEncryptedPrivateKeyIV: iv,
|
|
serverEncryptedPrivateKey: ciphertext
|
|
},
|
|
tx
|
|
);
|
|
} else {
|
|
userEncKey = await userDAL.upsertUserEncryptionKey(
|
|
us.id,
|
|
{
|
|
encryptionVersion: UserEncryption.V2,
|
|
salt,
|
|
verifier,
|
|
publicKey,
|
|
protectedKey,
|
|
protectedKeyIV,
|
|
protectedKeyTag,
|
|
encryptedPrivateKey,
|
|
iv: encryptedPrivateKeyIV,
|
|
tag: encryptedPrivateKeyTag,
|
|
hashedPassword,
|
|
serverEncryptedPrivateKeyEncoding: encoding,
|
|
serverEncryptedPrivateKeyTag: tag,
|
|
serverEncryptedPrivateKeyIV: iv,
|
|
serverEncryptedPrivateKey: ciphertext
|
|
},
|
|
tx
|
|
);
|
|
}
|
|
|
|
const updatedMembersips = await orgDAL.updateMembership(
|
|
{ inviteEmail: email, status: OrgMembershipStatus.Invited },
|
|
{ userId: us.id, status: OrgMembershipStatus.Accepted },
|
|
tx
|
|
);
|
|
const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))];
|
|
await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId, tx)));
|
|
|
|
await convertPendingGroupAdditionsToGroupMemberships({
|
|
userIds: [user.id],
|
|
userDAL,
|
|
userGroupMembershipDAL,
|
|
groupProjectDAL,
|
|
projectKeyDAL,
|
|
projectDAL,
|
|
projectBotDAL,
|
|
tx
|
|
});
|
|
|
|
return { info: us, key: userEncKey };
|
|
});
|
|
|
|
const tokenSession = await tokenService.getUserTokenSession({
|
|
userAgent,
|
|
ip,
|
|
userId: updateduser.info.id
|
|
});
|
|
if (!tokenSession) throw new Error("Failed to create token");
|
|
|
|
const accessToken = jwt.sign(
|
|
{
|
|
authMethod: AuthMethod.EMAIL,
|
|
authTokenType: AuthTokenType.ACCESS_TOKEN,
|
|
userId: updateduser.info.id,
|
|
tokenVersionId: tokenSession.id,
|
|
accessVersion: tokenSession.accessVersion
|
|
},
|
|
appCfg.AUTH_SECRET,
|
|
{ expiresIn: appCfg.JWT_SIGNUP_LIFETIME }
|
|
);
|
|
|
|
const refreshToken = jwt.sign(
|
|
{
|
|
authMethod: AuthMethod.EMAIL,
|
|
authTokenType: AuthTokenType.REFRESH_TOKEN,
|
|
userId: updateduser.info.id,
|
|
tokenVersionId: tokenSession.id,
|
|
refreshVersion: tokenSession.refreshVersion
|
|
},
|
|
appCfg.AUTH_SECRET,
|
|
{ expiresIn: appCfg.JWT_SIGNUP_LIFETIME }
|
|
);
|
|
|
|
return { user: updateduser.info, accessToken, refreshToken };
|
|
};
|
|
|
|
return {
|
|
beginEmailSignupProcess,
|
|
verifyEmailSignup,
|
|
completeEmailAccountSignup,
|
|
completeAccountInvite
|
|
};
|
|
};
|