feat: changed backend-pg to backend

This commit is contained in:
Akhil Mohan
2024-01-27 19:14:33 +05:30
parent d13eafcef7
commit 66d258f02b
792 changed files with 40298 additions and 40607 deletions
+30
View File
@@ -0,0 +1,30 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TBackupPrivateKey } from "@app/db/schemas";
import { withTransaction } from "@app/lib/knex";
export type TAuthDALFactory = ReturnType<typeof authDALFactory>;
export const authDALFactory = (db: TDbClient) => {
const getBackupPrivateKeyByUserId = async (userId: string) =>
db(TableName.BackupPrivateKey).where({ userId }).first("*");
const upsertBackupKey = async (
userId: string,
data: Partial<TBackupPrivateKey>,
tx?: Knex
): Promise<TBackupPrivateKey | undefined> => {
const [backupKey] = await (tx || db)(TableName.BackupPrivateKey)
.insert({ userId, ...data, updatedAt: new Date() } as TBackupPrivateKey)
.onConflict("userId")
.merge()
.returning("*");
return backupKey;
};
return withTransaction(db, {
getBackupPrivateKeyByUserId,
upsertBackupKey
});
};
+52
View File
@@ -0,0 +1,52 @@
import jwt from "jsonwebtoken";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import {
AuthModeProviderJwtTokenPayload,
AuthModeProviderSignUpTokenPayload,
AuthTokenType
} from "./auth-type";
export const validateProviderAuthToken = (providerToken: string, email: string) => {
if (!providerToken) throw new UnauthorizedError();
const appCfg = getConfig();
const decodedToken = jwt.verify(
providerToken,
appCfg.AUTH_SECRET
) as AuthModeProviderJwtTokenPayload;
if (decodedToken.authTokenType !== AuthTokenType.PROVIDER_TOKEN) throw new UnauthorizedError();
if (decodedToken.email !== email) throw new Error("Invalid auth credentials");
};
export const validateSignUpAuthorization = (token: string, userId: string, validate = true) => {
const appCfg = getConfig();
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>token?.split(" ", 2) ?? [
null,
null
];
if (AUTH_TOKEN_TYPE === null) {
throw new BadRequestError({ message: "Missing Authorization Header in the request header." });
}
if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") {
throw new BadRequestError({
message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`
});
}
if (AUTH_TOKEN_VALUE === null) {
throw new BadRequestError({
message: "Missing Authorization Body in the request header"
});
}
const decodedToken = jwt.verify(
AUTH_TOKEN_VALUE,
appCfg.AUTH_SECRET
) as AuthModeProviderSignUpTokenPayload;
if (!validate) return decodedToken;
if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) throw new UnauthorizedError();
if (decodedToken.userId !== userId) throw new UnauthorizedError();
};
@@ -0,0 +1,283 @@
import jwt from "jsonwebtoken";
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { validateProviderAuthToken } from "./auth-fns";
import {
TLoginClientProofDTO,
TLoginGenServerPublicKeyDTO,
TOauthLoginDTO,
TVerifyMfaTokenDTO
} from "./auth-login-type";
import { AuthMethod, AuthTokenType } from "./auth-type";
type TAuthLoginServiceFactoryDep = {
userDAL: TUserDALFactory;
tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService;
};
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
export const authLoginServiceFactory = ({
userDAL,
tokenService,
smtpService
}: TAuthLoginServiceFactoryDep) => {
/*
* Private
* Not exported. This is to update user device list
* If new device is found. Will be saved and a mail will be send
*/
const updateUserDeviceSession = async (user: TUsers, ip: string, userAgent: string) => {
const devices = await UserDeviceSchema.parseAsync(user.devices || []);
const isDeviceSeen = devices.some(
(device) => device.ip === ip && device.userAgent === userAgent
);
if (!isDeviceSeen) {
const newDeviceList = devices.concat([{ ip, userAgent }]);
await userDAL.updateById(user.id, { devices: JSON.stringify(newDeviceList) });
await smtpService.sendMail({
template: SmtpTemplates.NewDeviceJoin,
subjectLine: "Successful login from new device",
recipients: [user.email],
substitutions: {
email: user.email,
timestamp: new Date().toString(),
ip,
userAgent
}
});
}
};
/*
* Private
* Send mfa code via email
* */
const sendUserMfaCode = async (userId: string, email: string) => {
const code = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_MFA,
userId
});
await smtpService.sendMail({
template: SmtpTemplates.EmailMfa,
subjectLine: "Infisical MFA code",
recipients: [email],
substitutions: {
code
}
});
};
/*
* Check user device and send mail if new device
* generate the auth and refresh token. fn shared by mfa verification and login verification with mfa disabled
*/
const generateUserTokens = async (user: TUsers, ip: string, userAgent: string) => {
const cfg = getConfig();
await updateUserDeviceSession(user, ip, userAgent);
const tokenSession = await tokenService.getUserTokenSession({
userAgent,
ip,
userId: user.id
});
if (!tokenSession) throw new Error("Failed to create token");
const accessToken = jwt.sign(
{
authTokenType: AuthTokenType.ACCESS_TOKEN,
userId: user.id,
tokenVersionId: tokenSession.id,
accessVersion: tokenSession.accessVersion
},
cfg.AUTH_SECRET,
{ expiresIn: cfg.JWT_AUTH_LIFETIME }
);
const refreshToken = jwt.sign(
{
authTokenType: AuthTokenType.REFRESH_TOKEN,
userId: user.id,
tokenVersionId: tokenSession.id,
refreshVersion: tokenSession.refreshVersion
},
cfg.AUTH_SECRET,
{ expiresIn: cfg.JWT_REFRESH_LIFETIME }
);
return { access: accessToken, refresh: refreshToken };
};
/*
* Step 1 of login. To get server public key in exchange of client public key
*/
const loginGenServerPublicKey = async ({
email,
providerAuthToken,
clientPublicKey
}: TLoginGenServerPublicKeyDTO) => {
const userEnc = await userDAL.findUserEncKeyByEmail(email);
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
throw new Error("Failed to find user");
}
if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) {
validateProviderAuthToken(providerAuthToken as string, email);
}
const serverSrpKey = await generateSrpServerKey(userEnc.salt, userEnc.verifier);
const userEncKeys = await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
clientPublicKey,
serverPrivateKey: serverSrpKey.privateKey
});
if (!userEncKeys) throw new Error("Failed to update encryption key");
return { salt: userEncKeys.salt, serverPublicKey: serverSrpKey.pubKey };
};
/*
* Step 2 of login. Pass the client proof and with multi factor setup handle the required steps
*/
const loginExchangeClientProof = async ({
email,
clientProof,
providerAuthToken,
ip,
userAgent
}: TLoginClientProofDTO) => {
const userEnc = await userDAL.findUserEncKeyByEmail(email);
if (!userEnc) throw new Error("Failed to find user");
const cfg = getConfig();
if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) {
validateProviderAuthToken(providerAuthToken as string, email);
}
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey)
throw new Error("Failed to authenticate. Try again?");
const isValidClientProof = await srpCheckClientProof(
userEnc.salt,
userEnc.verifier,
userEnc.serverPrivateKey,
userEnc.clientPublicKey,
clientProof
);
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
serverPrivateKey: null,
clientPublicKey: null
});
// send multi factor auth token if they it enabled
if (userEnc.isMfaEnabled) {
const mfaToken = jwt.sign(
{ authTokenType: AuthTokenType.MFA_TOKEN, userId: userEnc.userId },
cfg.AUTH_SECRET,
{ expiresIn: cfg.JWT_MFA_LIFETIME }
);
await sendUserMfaCode(userEnc.userId, userEnc.email);
return { isMfaEnabled: true, token: mfaToken } as const;
}
const token = await generateUserTokens({ ...userEnc, id: userEnc.userId }, ip, userAgent);
return { token, isMfaEnabled: false, user: userEnc } as const;
};
/*
* Multi factor authentication re-send code, Get user id from token
* saved in frontend
*/
const resendMfaToken = async (userId: string) => {
const user = await userDAL.findById(userId);
if (!user) return;
await sendUserMfaCode(user.id, user.email);
};
/*
* Multi factor authentication verification of code
* Third step of login in which user completes with mfa
* */
const verifyMfaToken = async ({ userId, mfaToken, ip, userAgent }: TVerifyMfaTokenDTO) => {
await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_MFA,
userId,
code: mfaToken
});
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
if (!userEnc) throw new Error("Failed to authenticate user");
const token = await generateUserTokens({ ...userEnc, id: userEnc.userId }, ip, userAgent);
return { token, user: userEnc };
};
/*
* OAuth2 login for google,github, and other oauth2 provider
* */
const oauth2Login = async ({
email,
firstName,
lastName,
authMethod,
callbackPort,
isSignupAllowed
}: TOauthLoginDTO) => {
let user = await userDAL.findUserByEmail(email);
const appCfg = getConfig();
const isOauthSignUpDisabled = !isSignupAllowed && !user;
if (isOauthSignUpDisabled)
throw new BadRequestError({ message: "User signup disabled", name: "Oauth 2 login" });
if (!user) {
user = await userDAL.create({ email, firstName, lastName, authMethods: [authMethod] });
}
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
const isUserCompleted = user.isAccepted;
const providerAuthToken = jwt.sign(
{
authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
authMethod,
isUserCompleted,
isLinkingRequired,
...(callbackPort
? {
callbackPort
}
: {})
},
appCfg.AUTH_SECRET,
{
expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME
}
);
return { isUserCompleted, providerAuthToken };
};
/*
* logout user by incrementing the version by 1 meaning any old session will become invalid
* as there number is behind
* */
const logout = async (userId: string, sessionId: string) => {
await tokenService.clearTokenSessionById(userId, sessionId);
};
return {
loginGenServerPublicKey,
loginExchangeClientProof,
logout,
oauth2Login,
resendMfaToken,
verifyMfaToken,
generateUserTokens
};
};
@@ -0,0 +1,31 @@
import { AuthMethod } from "./auth-type";
export type TLoginGenServerPublicKeyDTO = {
email: string;
clientPublicKey: string;
providerAuthToken?: string;
};
export type TLoginClientProofDTO = {
email: string;
clientProof: string;
providerAuthToken?: string;
ip: string;
userAgent: string;
};
export type TVerifyMfaTokenDTO = {
userId: string;
mfaToken: string;
ip: string;
userAgent: string;
};
export type TOauthLoginDTO = {
email: string;
firstName: string;
lastName?: string;
authMethod: AuthMethod;
callbackPort?: string;
isSignupAllowed?: boolean;
};
@@ -0,0 +1,265 @@
import jwt from "jsonwebtoken";
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { TAuthDALFactory } from "./auth-dal";
import {
TChangePasswordDTO,
TCreateBackupPrivateKeyDTO,
TResetPasswordViaBackupKeyDTO
} from "./auth-password-type";
import { AuthTokenType } from "./auth-type";
type TAuthPasswordServiceFactoryDep = {
authDAL: TAuthDALFactory;
userDAL: TUserDALFactory;
tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService;
};
export type TAuthPasswordFactory = ReturnType<typeof authPaswordServiceFactory>;
export const authPaswordServiceFactory = ({
authDAL,
userDAL,
tokenService,
smtpService
}: TAuthPasswordServiceFactoryDep) => {
/*
* Pre setup for pass change with srp protocol
* Gets srp server user salt and server public key
*/
const generateServerPubKey = async (userId: string, clientPublicKey: string) => {
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
if (!userEnc) throw new Error("Failed to find user");
const serverSrpKey = await generateSrpServerKey(userEnc.salt, userEnc.verifier);
const userEncKeys = await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
clientPublicKey,
serverPrivateKey: serverSrpKey.privateKey
});
if (!userEncKeys) throw new Error("Failed to update encryption key");
return { salt: userEncKeys.salt, serverPublicKey: serverSrpKey.pubKey };
};
/*
* Change password to new pass
* */
const changePassword = async ({
userId,
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
tokenVersionId
}: TChangePasswordDTO) => {
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
if (!userEnc) throw new Error("Failed to find user");
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
serverPrivateKey: null,
clientPublicKey: null
});
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey)
throw new Error("Failed to authenticate. Try again?");
const isValidClientProof = await srpCheckClientProof(
userEnc.salt,
userEnc.verifier,
userEnc.serverPrivateKey,
userEnc.clientPublicKey,
clientProof
);
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
await userDAL.updateUserEncryptionByUserId(userId, {
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier,
serverPrivateKey: null,
clientPublicKey: null
});
if (tokenVersionId) {
await tokenService.clearTokenSessionById(userEnc.userId, tokenVersionId);
}
};
/*
* Email password reset flow via email. Step 1 send email
*/
const sendPasswordResetEmail = async (email: string) => {
const user = await userDAL.findUserByEmail(email);
// ignore as user is not found to avoid an outside entity to identify infisical registered accounts
if (!user || (user && !user.isAccepted)) return;
const cfg = getConfig();
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_PASSWORD_RESET,
userId: user.id
});
await smtpService.sendMail({
template: SmtpTemplates.ResetPassword,
recipients: [email],
subjectLine: "Infisical password reset",
substitutions: {
email,
token,
callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : ""
}
});
};
/*
* Step 2 of reset password. Verify the token and inject a temp token to reset password
* */
const verifyPasswordResetEmail = async (email: string, code: string) => {
const cfg = getConfig();
const user = await userDAL.findUserByEmail(email);
// ignore as user is not found to avoid an outside entity to identify infisical registered accounts
if (!user || (user && !user.isAccepted)) {
throw new Error("Failed email verification for pass reset");
}
await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_PASSWORD_RESET,
userId: user.id,
code
});
const token = jwt.sign(
{
authTokenType: AuthTokenType.SIGNUP_TOKEN,
userId: user.id
},
cfg.AUTH_SECRET,
{ expiresIn: cfg.JWT_SIGNUP_LIFETIME }
);
return { token, user };
};
/*
* Reset password of a user via backup key
* */
const resetPasswordByBackupKey = async ({
encryptedPrivateKey,
protectedKeyTag,
protectedKey,
protectedKeyIV,
salt,
verifier,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
userId
}: TResetPasswordViaBackupKeyDTO) => {
await userDAL.updateUserEncryptionByUserId(userId, {
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier
});
};
/*
* backup key creation to give user's their access back when lost their password
* this also needs to do the generateServerPubKey function to be executed first
* then only client proof can be verified
* */
const createBackupPrivateKey = async ({
clientProof,
encryptedPrivateKey,
salt,
verifier,
iv,
tag,
userId
}: TCreateBackupPrivateKeyDTO) => {
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
throw new Error("Failed to find user");
}
if (!userEnc.clientPublicKey || !userEnc.serverPrivateKey)
throw new Error("failed to create backup key");
const isValidClientProff = await srpCheckClientProof(
userEnc.salt,
userEnc.verifier,
userEnc.serverPrivateKey,
userEnc.clientPublicKey,
clientProof
);
if (!isValidClientProff) throw new Error("failed to create backup key");
const backup = await authDAL.transaction(async (tx) => {
const backupKey = await authDAL.upsertBackupKey(
userEnc.userId,
{
encryptedPrivateKey,
iv,
tag,
salt,
verifier,
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8
},
tx
);
await userDAL.updateUserEncryptionByUserId(
userEnc.userId,
{
serverPrivateKey: null,
clientPublicKey: null
},
tx
);
return backupKey;
});
return backup;
};
/*
* Return user back up
* */
const getBackupPrivateKeyOfUser = async (userId: string) => {
const user = await userDAL.findUserEncKeyByUserId(userId);
if (!user || (user && !user.isAccepted)) {
throw new Error("Failed to find user");
}
const backupKey = await authDAL.getBackupPrivateKeyByUserId(userId);
if (!backupKey) throw new Error("Failed to find user backup key");
return backupKey;
};
return {
generateServerPubKey,
changePassword,
resetPasswordByBackupKey,
sendPasswordResetEmail,
verifyPasswordResetEmail,
createBackupPrivateKey,
getBackupPrivateKeyOfUser
};
};
@@ -0,0 +1,35 @@
export type TChangePasswordDTO = {
userId: string;
clientProof: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
tokenVersionId?: string;
};
export type TResetPasswordViaBackupKeyDTO = {
userId: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
};
export type TCreateBackupPrivateKeyDTO = {
userId: string;
clientProof: string;
encryptedPrivateKey: string;
salt: string;
iv: string;
tag: string;
verifier: string;
};
@@ -0,0 +1,308 @@
import jwt from "jsonwebtoken";
import { OrgMembershipStatus } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { isDisposableEmail } from "@app/lib/validator";
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 { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
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;
orgService: Pick<TOrgServiceFactory, "createOrganization">;
orgDAL: TOrgDALFactory;
tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService;
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
};
export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>;
export const authSignupServiceFactory = ({
authDAL,
userDAL,
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.findUserByEmail(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], email });
}
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.EmailVerification,
subjectLine: "Infisical confirmation code",
recipients: [email],
substitutions: {
code: token
}
});
};
const verifyEmailSignup = async (email: string, code: string) => {
const user = await userDAL.findUserByEmail(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
});
// 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,
firstName,
lastName,
providerAuthToken,
salt,
verifier,
publicKey,
protectedKey,
protectedKeyIV,
protectedKeyTag,
organizationName,
// attributionSource,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
ip,
userAgent,
authorization
}: TCompleteAccountSignupDTO) => {
const user = await userDAL.findUserByEmail(email);
if (!user || (user && user.isAccepted)) {
throw new Error("Failed to complete account for complete user");
}
if (providerAuthToken) {
validateProviderAuthToken(providerAuthToken, user.email);
} else {
validateSignUpAuthorization(authorization, user.id);
}
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 userEncKey = await userDAL.upsertUserEncryptionKey(
us.id,
{
salt,
verifier,
publicKey,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
},
tx
);
return { info: us, key: userEncKey };
});
const hasSamlEnabled = user?.authMethods?.some((authMethod) =>
[AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(
authMethod as AuthMethod
)
);
if (!hasSamlEnabled) {
await orgService.createOrganization(user.id, user.email, organizationName);
}
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))
);
const tokenSession = await tokenService.getUserTokenSession({
userAgent,
ip,
userId: updateduser.info.id
});
if (!tokenSession) throw new Error("Failed to create token");
const appCfg = getConfig();
const accessToken = jwt.sign(
{
authTokenType: AuthTokenType.ACCESS_TOKEN,
userId: updateduser.info.id,
tokenVersionId: tokenSession.id,
accessVersion: tokenSession.accessVersion
},
appCfg.AUTH_SECRET,
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }
);
const refreshToken = jwt.sign(
{
authTokenType: AuthTokenType.REFRESH_TOKEN,
userId: updateduser.info.id,
tokenVersionId: tokenSession.id,
refreshVersion: tokenSession.refreshVersion
},
appCfg.AUTH_SECRET,
{ expiresIn: appCfg.JWT_REFRESH_LIFETIME }
);
return { user: updateduser.info, accessToken, refreshToken };
};
/*
* User signup flow when they are invited to join the org
* */
const completeAccountInvite = async ({
ip,
salt,
email,
verifier,
firstName,
publicKey,
userAgent,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag
}: TCompleteAccountInviteDTO) => {
const user = await userDAL.findUserByEmail(email);
if (!user || (user && user.isAccepted)) {
throw new Error("Failed to complete account for complete user");
}
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 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 userEncKey = await userDAL.upsertUserEncryptionKey(
us.id,
{
salt,
encryptionVersion: 2,
verifier,
publicKey,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
},
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))
);
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 appCfg = getConfig();
const accessToken = jwt.sign(
{
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(
{
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
};
};
@@ -0,0 +1,37 @@
export type TCompleteAccountSignupDTO = {
email: string;
firstName: string;
lastName?: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
organizationName: string;
providerAuthToken?: string | null;
attributionSource?: string | undefined;
ip: string;
userAgent: string;
authorization: string;
};
export type TCompleteAccountInviteDTO = {
email: string;
firstName: string;
lastName?: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
ip: string;
userAgent: string;
};
+59
View File
@@ -0,0 +1,59 @@
export enum AuthMethod {
EMAIL = "email",
GOOGLE = "google",
GITHUB = "github",
GITLAB = "gitlab",
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml"
}
export enum AuthTokenType {
ACCESS_TOKEN = "accessToken",
REFRESH_TOKEN = "refreshToken",
SIGNUP_TOKEN = "signupToken", // TODO: remove in favor of claim
MFA_TOKEN = "mfaToken", // TODO: remove in favor of claim
PROVIDER_TOKEN = "providerToken", // TODO: remove in favor of claim
API_KEY = "apiKey",
SERVICE_ACCESS_TOKEN = "serviceAccessToken",
SERVICE_REFRESH_TOKEN = "serviceRefreshToken",
IDENTITY_ACCESS_TOKEN = "identityAccessToken"
}
export enum AuthMode {
JWT = "jwt",
SERVICE_TOKEN = "serviceToken",
API_KEY = "apiKey",
IDENTITY_ACCESS_TOKEN = "identityAccessToken"
}
export enum ActorType { // would extend to AWS, Azure, ...
USER = "user", // userIdentity
SERVICE = "service",
IDENTITY = "identity",
Machine = "machine"
}
export type AuthModeJwtTokenPayload = {
authTokenType: AuthTokenType.ACCESS_TOKEN;
userId: string;
tokenVersionId: string;
accessVersion: number;
};
export type AuthModeRefreshJwtTokenPayload = {
authTokenType: AuthTokenType.REFRESH_TOKEN;
userId: string;
tokenVersionId: string;
refreshVersion: number;
};
export type AuthModeProviderJwtTokenPayload = {
authTokenType: AuthTokenType.PROVIDER_TOKEN;
email: string;
};
export type AuthModeProviderSignUpTokenPayload = {
authTokenType: AuthTokenType.SIGNUP_TOKEN;
userId: string;
};