feat: api changes for srp handover

This commit is contained in:
=
2024-06-12 19:25:05 +05:30
parent a3a1c9d2e5
commit 777dfd5f58
16 changed files with 344 additions and 34 deletions
@@ -0,0 +1,61 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const doesPasswordFieldExist = await knex.schema.hasColumn(TableName.UserEncryptionKey, "password");
const doesPrivateKeyFieldExist = await knex.schema.hasColumn(
TableName.UserEncryptionKey,
"serverEncryptedPrivateKey"
);
const doesPrivateKeyIVFieldExist = await knex.schema.hasColumn(
TableName.UserEncryptionKey,
"serverEncryptedPrivateKeyIV"
);
const doesPrivateKeyTagFieldExist = await knex.schema.hasColumn(
TableName.UserEncryptionKey,
"serverEncryptedPrivateKeyTag"
);
const doesPrivateKeyEncodingFieldExist = await knex.schema.hasColumn(
TableName.UserEncryptionKey,
"serverEncryptedPrivateKeyEncoding"
);
if (await knex.schema.hasTable(TableName.UserEncryptionKey)) {
await knex.schema.alterTable(TableName.UserEncryptionKey, (t) => {
if (!doesPasswordFieldExist) t.string("password");
if (!doesPrivateKeyFieldExist) t.text("serverEncryptedPrivateKey");
if (!doesPrivateKeyIVFieldExist) t.text("serverEncryptedPrivateKeyIV");
if (!doesPrivateKeyTagFieldExist) t.text("serverEncryptedPrivateKeyTag");
if (!doesPrivateKeyEncodingFieldExist) t.text("serverEncryptedPrivateKeyEncoding");
});
}
}
export async function down(knex: Knex): Promise<void> {
const doesPasswordFieldExist = await knex.schema.hasColumn(TableName.UserEncryptionKey, "password");
const doesPrivateKeyFieldExist = await knex.schema.hasColumn(
TableName.UserEncryptionKey,
"serverEncryptedPrivateKey"
);
const doesPrivateKeyIVFieldExist = await knex.schema.hasColumn(
TableName.UserEncryptionKey,
"serverEncryptedPrivateKeyIV"
);
const doesPrivateKeyTagFieldExist = await knex.schema.hasColumn(
TableName.UserEncryptionKey,
"serverEncryptedPrivateKeyTag"
);
const doesPrivateKeyEncodingFieldExist = await knex.schema.hasColumn(
TableName.UserEncryptionKey,
"serverEncryptedPrivateKeyEncoding"
);
if (await knex.schema.hasTable(TableName.UserEncryptionKey)) {
await knex.schema.alterTable(TableName.UserEncryptionKey, (t) => {
if (doesPasswordFieldExist) t.dropColumn("password");
if (doesPrivateKeyFieldExist) t.dropColumn("serverEncryptedPrivateKey");
if (doesPrivateKeyIVFieldExist) t.dropColumn("serverEncryptedPrivateKeyIV");
if (doesPrivateKeyTagFieldExist) t.dropColumn("serverEncryptedPrivateKeyTag");
if (doesPrivateKeyEncodingFieldExist) t.dropColumn("serverEncryptedPrivateKeyEncoding");
});
}
}
@@ -21,7 +21,12 @@ export const UserEncryptionKeysSchema = z.object({
tag: z.string(), tag: z.string(),
salt: z.string(), salt: z.string(),
verifier: z.string(), verifier: z.string(),
userId: z.string().uuid() userId: z.string().uuid(),
password: z.string().nullable().optional(),
serverEncryptedPrivateKey: z.string().nullable().optional(),
serverEncryptedPrivateKeyIV: z.string().nullable().optional(),
serverEncryptedPrivateKeyTag: z.string().nullable().optional(),
serverEncryptedPrivateKeyEncoding: z.string().nullable().optional()
}); });
export type TUserEncryptionKeys = z.infer<typeof UserEncryptionKeysSchema>; export type TUserEncryptionKeys = z.infer<typeof UserEncryptionKeysSchema>;
@@ -73,7 +73,13 @@ type TLdapConfigServiceFactoryDep = {
>; >;
userDAL: Pick< userDAL: Pick<
TUserDALFactory, TUserDALFactory,
"create" | "findOne" | "transaction" | "updateById" | "findUserEncKeyByUserIdsBatch" | "find" | "create"
| "findOne"
| "transaction"
| "updateById"
| "findUserEncKeyByUserIdsBatch"
| "find"
| "findUserEncKeyByUserId"
>; >;
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">; userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
@@ -592,12 +598,14 @@ export const ldapConfigServiceFactory = ({
}); });
const isUserCompleted = Boolean(user.isAccepted); const isUserCompleted = Boolean(user.isAccepted);
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
const providerAuthToken = jwt.sign( const providerAuthToken = jwt.sign(
{ {
authTokenType: AuthTokenType.PROVIDER_TOKEN, authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user.id, userId: user.id,
username: user.username, username: user.username,
hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }), ...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
firstName, firstName,
lastName, lastName,
@@ -41,7 +41,10 @@ import { TCreateSamlCfgDTO, TGetSamlCfgDTO, TSamlLoginDTO, TUpdateSamlCfgDTO } f
type TSamlConfigServiceFactoryDep = { type TSamlConfigServiceFactoryDep = {
samlConfigDAL: Pick<TSamlConfigDALFactory, "create" | "findOne" | "update" | "findById">; samlConfigDAL: Pick<TSamlConfigDALFactory, "create" | "findOne" | "update" | "findById">;
userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById" | "findById">; userDAL: Pick<
TUserDALFactory,
"create" | "findOne" | "transaction" | "updateById" | "findById" | "findUserEncKeyByUserId"
>;
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">; userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
orgDAL: Pick< orgDAL: Pick<
TOrgDALFactory, TOrgDALFactory,
@@ -452,6 +455,7 @@ export const samlConfigServiceFactory = ({
await licenseService.updateSubscriptionOrgMemberCount(organization.id); await licenseService.updateSubscriptionOrgMemberCount(organization.id);
const isUserCompleted = Boolean(user.isAccepted); const isUserCompleted = Boolean(user.isAccepted);
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
const providerAuthToken = jwt.sign( const providerAuthToken = jwt.sign(
{ {
authTokenType: AuthTokenType.PROVIDER_TOKEN, authTokenType: AuthTokenType.PROVIDER_TOKEN,
@@ -464,6 +468,7 @@ export const samlConfigServiceFactory = ({
organizationId: organization.id, organizationId: organization.id,
organizationSlug: organization.slug, organizationSlug: organization.slug,
authMethod: authProvider, authMethod: authProvider,
hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
authType: UserAliasType.SAML, authType: UserAliasType.SAML,
isUserCompleted, isUserCompleted,
...(relayState ...(relayState
+1 -1
View File
@@ -29,7 +29,7 @@ const envSchema = z
DB_USER: zpStr(z.string().describe("Postgres database username").optional()), DB_USER: zpStr(z.string().describe("Postgres database username").optional()),
DB_PASSWORD: zpStr(z.string().describe("Postgres database password").optional()), DB_PASSWORD: zpStr(z.string().describe("Postgres database password").optional()),
DB_NAME: zpStr(z.string().describe("Postgres database name").optional()), DB_NAME: zpStr(z.string().describe("Postgres database name").optional()),
BCRYPT_SALT_ROUND: z.number().default(12),
NODE_ENV: z.enum(["development", "test", "production"]).default("production"), NODE_ENV: z.enum(["development", "test", "production"]).default("production"),
SALT_ROUNDS: z.coerce.number().default(10), SALT_ROUNDS: z.coerce.number().default(10),
INITIAL_ORGANIZATION_NAME: zpStr(z.string().optional()), INITIAL_ORGANIZATION_NAME: zpStr(z.string().optional()),
+16 -9
View File
@@ -6,7 +6,7 @@ import tweetnacl from "tweetnacl-util";
import { TUserEncryptionKeys } from "@app/db/schemas"; import { TUserEncryptionKeys } from "@app/db/schemas";
import { decryptSymmetric, encryptAsymmetric, encryptSymmetric } from "./encryption"; import { decryptSymmetric128BitHexKeyUTF8, encryptAsymmetric, encryptSymmetric } from "./encryption";
export const generateSrpServerKey = async (salt: string, verifier: string) => { export const generateSrpServerKey = async (salt: string, verifier: string) => {
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
@@ -97,7 +97,13 @@ export const generateUserSrpKeys = async (email: string, password: string) => {
}; };
}; };
export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => { export const getUserPrivateKey = async (
password: string,
user: Pick<
TUserEncryptionKeys,
"protectedKeyTag" | "protectedKey" | "protectedKeyIV" | "encryptedPrivateKey" | "iv" | "salt" | "tag"
>
) => {
const derivedKey = await argon2.hash(password, { const derivedKey = await argon2.hash(password, {
salt: Buffer.from(user.salt), salt: Buffer.from(user.salt),
memoryCost: 65536, memoryCost: 65536,
@@ -108,17 +114,18 @@ export const getUserPrivateKey = async (password: string, user: TUserEncryptionK
raw: true raw: true
}); });
if (!derivedKey) throw new Error("Failed to derive key from password"); if (!derivedKey) throw new Error("Failed to derive key from password");
const key = decryptSymmetric({ const key = decryptSymmetric128BitHexKeyUTF8({
ciphertext: user.protectedKey!, ciphertext: user.protectedKey as string,
iv: user.protectedKeyIV!, iv: user.protectedKeyIV as string,
tag: user.protectedKeyTag!, tag: user.protectedKeyTag as string,
key: derivedKey.toString("base64") key: derivedKey
}); });
const privateKey = decryptSymmetric({
const privateKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: user.encryptedPrivateKey, ciphertext: user.encryptedPrivateKey,
iv: user.iv, iv: user.iv,
tag: user.tag, tag: user.tag,
key key: Buffer.from(key, "hex")
}); });
return privateKey; return privateKey;
}; };
@@ -259,4 +259,50 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
); );
} }
}); });
server.route({
url: "/token-exchange",
method: "POST",
schema: {
body: z.object({
providerAuthToken: z.string(),
email: z.string()
})
},
handler: async (req, res) => {
const userAgent = req.headers["user-agent"];
if (!userAgent) throw new Error("user agent header is required");
const data = await server.services.login.oauth2TokenExchange({
email: req.body.email,
ip: req.realIp,
userAgent,
providerAuthToken: req.body.providerAuthToken
});
if (data.isMfaEnabled) {
return { mfaEnabled: true, token: data.token } as const; // for discriminated union
}
void res.setCookie("jid", data.token.refresh, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
return {
mfaEnabled: false,
encryptionVersion: data.user.encryptionVersion,
token: data.token.access,
publicKey: data.user.publicKey,
encryptedPrivateKey: data.user.encryptedPrivateKey,
iv: data.user.iv,
tag: data.user.tag,
protectedKey: data.user.protectedKey || null,
protectedKeyIV: data.user.protectedKeyIV || null,
protectedKeyTag: data.user.protectedKeyTag || null
} as const;
}
});
}; };
+28 -1
View File
@@ -19,7 +19,14 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
response: { response: {
200: z.object({ 200: z.object({
user: UsersSchema.merge(UserEncryptionKeysSchema.omit({ verifier: true })) user: UsersSchema.merge(
UserEncryptionKeysSchema.omit({
verifier: true,
serverEncryptedPrivateKey: true,
serverEncryptedPrivateKeyIV: true,
serverEncryptedPrivateKeyTag: true
})
)
}) })
} }
}, },
@@ -30,6 +37,26 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "GET",
url: "/private-key",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
privateKey: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req) => {
const privateKey = await server.services.user.getUserPrivateKey(req.permission.id);
return { privateKey };
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/:userId/unlock", url: "/:userId/unlock",
+4 -2
View File
@@ -81,7 +81,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
email: z.string().trim(), email: z.string().trim(),
providerAuthToken: z.string().trim().optional(), providerAuthToken: z.string().trim().optional(),
clientProof: z.string().trim(), clientProof: z.string().trim(),
captchaToken: z.string().trim().optional() captchaToken: z.string().trim().optional(),
password: z.string().optional()
}), }),
response: { response: {
200: z.discriminatedUnion("mfaEnabled", [ 200: z.discriminatedUnion("mfaEnabled", [
@@ -112,7 +113,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
ip: req.realIp, ip: req.realIp,
userAgent, userAgent,
providerAuthToken: req.body.providerAuthToken, providerAuthToken: req.body.providerAuthToken,
clientProof: req.body.clientProof clientProof: req.body.clientProof,
password: req.body.password
}); });
if (data.isMfaEnabled) { if (data.isMfaEnabled) {
@@ -102,7 +102,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
verifier: z.string().trim(), verifier: z.string().trim(),
organizationName: z.string().trim().min(1), organizationName: z.string().trim().min(1),
providerAuthToken: z.string().trim().optional().nullish(), providerAuthToken: z.string().trim().optional().nullish(),
attributionSource: z.string().trim().optional() attributionSource: z.string().trim().optional(),
password: z.string()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -167,6 +168,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
body: z.object({ body: z.object({
email: z.string().email().trim(), email: z.string().email().trim(),
password: z.string(),
firstName: z.string().trim(), firstName: z.string().trim(),
lastName: z.string().trim().optional(), lastName: z.string().trim().optional(),
protectedKey: z.string().trim(), protectedKey: z.string().trim(),
+2 -2
View File
@@ -15,10 +15,10 @@ export const validateProviderAuthToken = (providerToken: string, username?: stri
if (decodedToken.username !== username) throw new Error("Invalid auth credentials"); if (decodedToken.username !== username) throw new Error("Invalid auth credentials");
if (decodedToken.organizationId) { if (decodedToken.organizationId) {
return { orgId: decodedToken.organizationId, authMethod: decodedToken.authMethod }; return { orgId: decodedToken.organizationId, authMethod: decodedToken.authMethod, userName: decodedToken.username };
} }
return { authMethod: decodedToken.authMethod, orgId: null }; return { authMethod: decodedToken.authMethod, orgId: null, userName: decodedToken.username };
}; };
export const validateSignUpAuthorization = (token: string, userId: string, validate = true) => { export const validateSignUpAuthorization = (token: string, userId: string, validate = true) => {
+92 -10
View File
@@ -1,3 +1,4 @@
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { TUsers, UserDeviceSchema } from "@app/db/schemas"; import { TUsers, UserDeviceSchema } from "@app/db/schemas";
@@ -5,6 +6,8 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors";
import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { getServerCfg } from "@app/services/super-admin/super-admin-service";
@@ -19,6 +22,7 @@ import {
TLoginClientProofDTO, TLoginClientProofDTO,
TLoginGenServerPublicKeyDTO, TLoginGenServerPublicKeyDTO,
TOauthLoginDTO, TOauthLoginDTO,
TOauthTokenExchangeDTO,
TVerifyMfaTokenDTO TVerifyMfaTokenDTO
} from "./auth-login-type"; } from "./auth-login-type";
import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType } from "./auth-type"; import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType } from "./auth-type";
@@ -101,7 +105,7 @@ export const authLoginServiceFactory = ({
user: TUsers; user: TUsers;
ip: string; ip: string;
userAgent: string; userAgent: string;
organizationId: string | undefined; organizationId?: string;
authMethod: AuthMethod; authMethod: AuthMethod;
}) => { }) => {
const cfg = getConfig(); const cfg = getConfig();
@@ -178,7 +182,8 @@ export const authLoginServiceFactory = ({
ip, ip,
userAgent, userAgent,
providerAuthToken, providerAuthToken,
captchaToken captchaToken,
password
}: TLoginClientProofDTO) => { }: TLoginClientProofDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
@@ -248,14 +253,29 @@ export const authLoginServiceFactory = ({
throw new Error("Failed to authenticate. Try again?"); throw new Error("Failed to authenticate. Try again?");
} }
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
serverPrivateKey: null,
clientPublicKey: null
});
await userDAL.updateById(userEnc.userId, { await userDAL.updateById(userEnc.userId, {
consecutiveFailedPasswordAttempts: 0 consecutiveFailedPasswordAttempts: 0
}); });
// from password decrypt the private key
if (password) {
const privateKey = await getUserPrivateKey(password, userEnc);
const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND);
const { iv, tag, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey);
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
serverPrivateKey: null,
clientPublicKey: null,
password: hashedPassword,
serverEncryptedPrivateKey: ciphertext,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyEncoding: encoding
});
} else {
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
serverPrivateKey: null,
clientPublicKey: null
});
}
// send multi factor auth token if they it enabled // send multi factor auth token if they it enabled
if (userEnc.isMfaEnabled && userEnc.email) { if (userEnc.isMfaEnabled && userEnc.email) {
@@ -499,8 +519,14 @@ export const authLoginServiceFactory = ({
authMethods: [authMethod], authMethods: [authMethod],
isGhost: false isGhost: false
}); });
} else {
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
if (isLinkingRequired) {
user = await userDAL.updateById(user.id, { authMethods: [...(user.authMethods || []), authMethod] });
}
} }
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
const isUserCompleted = user.isAccepted; const isUserCompleted = user.isAccepted;
const providerAuthToken = jwt.sign( const providerAuthToken = jwt.sign(
{ {
@@ -511,9 +537,9 @@ export const authLoginServiceFactory = ({
isEmailVerified: user.isEmailVerified, isEmailVerified: user.isEmailVerified,
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName, lastName: user.lastName,
hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
authMethod, authMethod,
isUserCompleted, isUserCompleted,
isLinkingRequired,
...(callbackPort ...(callbackPort
? { ? {
callbackPort callbackPort
@@ -525,10 +551,65 @@ export const authLoginServiceFactory = ({
expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME
} }
); );
return { isUserCompleted, providerAuthToken }; return { isUserCompleted, providerAuthToken };
}; };
// to login users with oauth2 token used for private key handoff
// The provider token will be given back to client to send back infisical access token
// why not directly sending access token?
// 1. To keep the logic change easier from SRP oauth to simple oauth
// 2. I don't want to attach access token to url as it may get logged the provider token has very short life span
const oauth2TokenExchange = async ({ userAgent, ip, providerAuthToken, email }: TOauthTokenExchangeDTO) => {
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email);
const appCfg = getConfig();
const { authMethod, userName } = decodedProviderToken;
if (!userName) throw new BadRequestError({ message: "Missing user name" });
const organizationId =
(isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && decodedProviderToken.orgId
? decodedProviderToken.orgId
: undefined;
const user = await userDAL.findUserEncKeyByUsername({
username: email
});
if (!user) throw new BadRequestError({ message: "Invalid token" });
if (!user.serverEncryptedPrivateKey) throw new BadRequestError({ message: "Private key handoff needs to be done" });
// send multi factor auth token if they it enabled
if (user.isMfaEnabled && user.email) {
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
const mfaToken = jwt.sign(
{
authMethod,
authTokenType: AuthTokenType.MFA_TOKEN,
userId: user.userId
},
appCfg.AUTH_SECRET,
{
expiresIn: appCfg.JWT_MFA_LIFETIME
}
);
await sendUserMfaCode({
userId: user.id,
email: user.email
});
return { isMfaEnabled: true, token: mfaToken } as const;
}
const token = await generateUserTokens({
user: { ...user, id: user.userId },
ip,
userAgent,
authMethod,
organizationId
});
return { token, isMfaEnabled: false, user } as const;
};
/* /*
* logout user by incrementing the version by 1 meaning any old session will become invalid * logout user by incrementing the version by 1 meaning any old session will become invalid
* as there number is behind * as there number is behind
@@ -542,6 +623,7 @@ export const authLoginServiceFactory = ({
loginExchangeClientProof, loginExchangeClientProof,
logout, logout,
oauth2Login, oauth2Login,
oauth2TokenExchange,
resendMfaToken, resendMfaToken,
verifyMfaToken, verifyMfaToken,
selectOrganization, selectOrganization,
@@ -13,6 +13,7 @@ export type TLoginClientProofDTO = {
ip: string; ip: string;
userAgent: string; userAgent: string;
captchaToken?: string; captchaToken?: string;
password?: string;
}; };
export type TVerifyMfaTokenDTO = { export type TVerifyMfaTokenDTO = {
@@ -31,3 +32,10 @@ export type TOauthLoginDTO = {
authMethod: AuthMethod; authMethod: AuthMethod;
callbackPort?: string; callbackPort?: string;
}; };
export type TOauthTokenExchangeDTO = {
providerAuthToken: string;
ip: string;
userAgent: string;
email: string;
};
@@ -1,3 +1,4 @@
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { OrgMembershipStatus, TableName } from "@app/db/schemas"; import { OrgMembershipStatus, TableName } from "@app/db/schemas";
@@ -6,6 +7,8 @@ import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-grou
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { isDisposableEmail } from "@app/lib/validator"; import { isDisposableEmail } from "@app/lib/validator";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
@@ -119,6 +122,7 @@ export const authSignupServiceFactory = ({
const completeEmailAccountSignup = async ({ const completeEmailAccountSignup = async ({
email, email,
password,
firstName, firstName,
lastName, lastName,
providerAuthToken, providerAuthToken,
@@ -137,6 +141,7 @@ export const authSignupServiceFactory = ({
userAgent, userAgent,
authorization authorization
}: TCompleteAccountSignupDTO) => { }: TCompleteAccountSignupDTO) => {
const appCfg = getConfig();
const user = await userDAL.findOne({ username: email }); const user = await userDAL.findOne({ username: email });
if (!user || (user && user.isAccepted)) { if (!user || (user && user.isAccepted)) {
throw new Error("Failed to complete account for complete user"); throw new Error("Failed to complete account for complete user");
@@ -152,6 +157,17 @@ export const authSignupServiceFactory = ({
validateSignUpAuthorization(authorization, user.id); 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
});
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey);
const updateduser = await authDAL.transaction(async (tx) => { const updateduser = await authDAL.transaction(async (tx) => {
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx); const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
if (!us) throw new Error("User not found"); if (!us) throw new Error("User not found");
@@ -166,7 +182,12 @@ export const authSignupServiceFactory = ({
protectedKeyTag, protectedKeyTag,
encryptedPrivateKey, encryptedPrivateKey,
iv: encryptedPrivateKeyIV, iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag tag: encryptedPrivateKeyTag,
password: hashedPassword,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
}, },
tx tx
); );
@@ -227,7 +248,6 @@ export const authSignupServiceFactory = ({
userId: updateduser.info.id userId: updateduser.info.id
}); });
if (!tokenSession) throw new Error("Failed to create token"); if (!tokenSession) throw new Error("Failed to create token");
const appCfg = getConfig();
const accessToken = jwt.sign( const accessToken = jwt.sign(
{ {
@@ -265,6 +285,7 @@ export const authSignupServiceFactory = ({
ip, ip,
salt, salt,
email, email,
password,
verifier, verifier,
firstName, firstName,
publicKey, publicKey,
@@ -295,6 +316,18 @@ export const authSignupServiceFactory = ({
name: "complete account invite" 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
});
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey);
const updateduser = await authDAL.transaction(async (tx) => { const updateduser = await authDAL.transaction(async (tx) => {
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx); const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
if (!us) throw new Error("User not found"); if (!us) throw new Error("User not found");
@@ -310,7 +343,12 @@ export const authSignupServiceFactory = ({
protectedKeyTag, protectedKeyTag,
encryptedPrivateKey, encryptedPrivateKey,
iv: encryptedPrivateKeyIV, iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag tag: encryptedPrivateKeyTag,
password: hashedPassword,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
}, },
tx tx
); );
@@ -343,7 +381,6 @@ export const authSignupServiceFactory = ({
userId: updateduser.info.id userId: updateduser.info.id
}); });
if (!tokenSession) throw new Error("Failed to create token"); if (!tokenSession) throw new Error("Failed to create token");
const appCfg = getConfig();
const accessToken = jwt.sign( const accessToken = jwt.sign(
{ {
@@ -1,5 +1,6 @@
export type TCompleteAccountSignupDTO = { export type TCompleteAccountSignupDTO = {
email: string; email: string;
password: string;
firstName: string; firstName: string;
lastName?: string; lastName?: string;
protectedKey: string; protectedKey: string;
@@ -21,6 +22,7 @@ export type TCompleteAccountSignupDTO = {
export type TCompleteAccountInviteDTO = { export type TCompleteAccountInviteDTO = {
email: string; email: string;
password: string;
firstName: string; firstName: string;
lastName?: string; lastName?: string;
protectedKey: string; protectedKey: string;
+19 -1
View File
@@ -1,3 +1,5 @@
import { SecretKeyEncoding } from "@app/db/schemas";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types"; import { TokenType } from "@app/services/auth-token/auth-token-types";
@@ -230,6 +232,21 @@ export const userServiceFactory = ({
); );
}; };
const getUserPrivateKey = async (userId: string) => {
const user = await userDAL.findUserEncKeyByUserId(userId);
if (!user?.serverEncryptedPrivateKey || !user.serverEncryptedPrivateKeyIV || !user.serverEncryptedPrivateKeyTag) {
throw new BadRequestError({ message: "Private key not found. Please login again" });
}
const privateKey = infisicalSymmetricDecrypt({
ciphertext: user.serverEncryptedPrivateKey,
tag: user.serverEncryptedPrivateKeyTag,
iv: user.serverEncryptedPrivateKeyIV,
keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
});
return privateKey;
};
return { return {
sendEmailVerificationCode, sendEmailVerificationCode,
verifyEmailVerificationCode, verifyEmailVerificationCode,
@@ -240,6 +257,7 @@ export const userServiceFactory = ({
getMe, getMe,
createUserAction, createUserAction,
getUserAction, getUserAction,
unlockUser unlockUser,
getUserPrivateKey
}; };
}; };