Merge remote-tracking branch 'origin' into secrets-mgmt-docs
@@ -0,0 +1,18 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.alterTable(TableName.UserEncryptionKey, (table) => {
|
||||||
|
table.text("encryptedPrivateKey").nullable().alter();
|
||||||
|
table.text("publicKey").nullable().alter();
|
||||||
|
table.text("iv").nullable().alter();
|
||||||
|
table.text("tag").nullable().alter();
|
||||||
|
table.text("salt").nullable().alter();
|
||||||
|
table.text("verifier").nullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(): Promise<void> {
|
||||||
|
// do nothing for now to avoid breaking down migrations
|
||||||
|
}
|
||||||
@@ -15,12 +15,12 @@ export const UserEncryptionKeysSchema = z.object({
|
|||||||
protectedKey: z.string().nullable().optional(),
|
protectedKey: z.string().nullable().optional(),
|
||||||
protectedKeyIV: z.string().nullable().optional(),
|
protectedKeyIV: z.string().nullable().optional(),
|
||||||
protectedKeyTag: z.string().nullable().optional(),
|
protectedKeyTag: z.string().nullable().optional(),
|
||||||
publicKey: z.string(),
|
publicKey: z.string().nullable().optional(),
|
||||||
encryptedPrivateKey: z.string(),
|
encryptedPrivateKey: z.string().nullable().optional(),
|
||||||
iv: z.string(),
|
iv: z.string().nullable().optional(),
|
||||||
tag: z.string(),
|
tag: z.string().nullable().optional(),
|
||||||
salt: z.string(),
|
salt: z.string().nullable().optional(),
|
||||||
verifier: z.string(),
|
verifier: z.string().nullable().optional(),
|
||||||
userId: z.string().uuid(),
|
userId: z.string().uuid(),
|
||||||
hashedPassword: z.string().nullable().optional(),
|
hashedPassword: z.string().nullable().optional(),
|
||||||
serverEncryptedPrivateKey: z.string().nullable().optional(),
|
serverEncryptedPrivateKey: z.string().nullable().optional(),
|
||||||
|
|||||||
@@ -115,6 +115,10 @@ export const generateUserSrpKeys = async (password: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => {
|
export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => {
|
||||||
|
if (!user.encryptedPrivateKey || !user.iv || !user.tag || !user.salt) {
|
||||||
|
throw new Error("User encrypted private key not found");
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { crypto } from "@app/lib/crypto";
|
import { initEnvConfig } from "@app/lib/config/env";
|
||||||
import { initLogger } from "@app/lib/logger";
|
import { initLogger, logger } from "@app/lib/logger";
|
||||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
|
|
||||||
import { AuthMethod } from "../../services/auth/auth-type";
|
import { AuthMethod } from "../../services/auth/auth-type";
|
||||||
@@ -17,7 +17,7 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
initLogger();
|
initLogger();
|
||||||
|
|
||||||
const superAdminDAL = superAdminDALFactory(knex);
|
const superAdminDAL = superAdminDALFactory(knex);
|
||||||
await crypto.initialize(superAdminDAL);
|
await initEnvConfig(superAdminDAL, logger);
|
||||||
|
|
||||||
await knex(TableName.SuperAdmin).insert([
|
await knex(TableName.SuperAdmin).insert([
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
@@ -25,6 +25,7 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
{ id: "00000000-0000-0000-0000-000000000000", initialized: true, allowSignUp: true }
|
{ id: "00000000-0000-0000-0000-000000000000", initialized: true, allowSignUp: true }
|
||||||
]);
|
]);
|
||||||
// Inserts seed entries
|
// Inserts seed entries
|
||||||
|
|
||||||
const [user] = await knex(TableName.Users)
|
const [user] = await knex(TableName.Users)
|
||||||
.insert([
|
.insert([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,28 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { initEnvConfig } from "@app/lib/config/env";
|
||||||
import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography";
|
import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography";
|
||||||
|
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
|
||||||
|
import { initLogger, logger } from "@app/lib/logger";
|
||||||
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
import { AuthMethod } from "@app/services/auth/auth-type";
|
||||||
|
import { assignWorkspaceKeysToMembers, createProjectKey } from "@app/services/project/project-fns";
|
||||||
|
import { projectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||||
|
import { projectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||||
|
import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal";
|
||||||
|
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
|
import { userDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
import { ProjectMembershipRole, ProjectType, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
|
import {
|
||||||
import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data";
|
OrgMembershipRole,
|
||||||
|
OrgMembershipStatus,
|
||||||
|
ProjectMembershipRole,
|
||||||
|
ProjectType,
|
||||||
|
SecretEncryptionAlgo,
|
||||||
|
SecretKeyEncoding,
|
||||||
|
TableName
|
||||||
|
} from "../schemas";
|
||||||
|
import { seedData1 } from "../seed-data";
|
||||||
|
|
||||||
export const DEFAULT_PROJECT_ENVS = [
|
export const DEFAULT_PROJECT_ENVS = [
|
||||||
{ name: "Development", slug: "dev" },
|
{ name: "Development", slug: "dev" },
|
||||||
@@ -11,12 +30,159 @@ export const DEFAULT_PROJECT_ENVS = [
|
|||||||
{ name: "Production", slug: "prod" }
|
{ name: "Production", slug: "prod" }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const createUserWithGhostUser = async (
|
||||||
|
orgId: string,
|
||||||
|
projectId: string,
|
||||||
|
userId: string,
|
||||||
|
userOrgMembershipId: string,
|
||||||
|
knex: Knex
|
||||||
|
) => {
|
||||||
|
const projectKeyDAL = projectKeyDALFactory(knex);
|
||||||
|
const userDAL = userDALFactory(knex);
|
||||||
|
const projectMembershipDAL = projectMembershipDALFactory(knex);
|
||||||
|
const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(knex);
|
||||||
|
|
||||||
|
const email = `sudo-${alphaNumericNanoId(16)}-${orgId}@infisical.com`; // We add a nanoid because the email is unique. And we have to create a new ghost user each time, so we can have access to the private key.
|
||||||
|
|
||||||
|
const password = crypto.randomBytes(128).toString("hex");
|
||||||
|
|
||||||
|
const [ghostUser] = await knex(TableName.Users)
|
||||||
|
.insert({
|
||||||
|
isGhost: true,
|
||||||
|
authMethods: [AuthMethod.EMAIL],
|
||||||
|
username: email,
|
||||||
|
email,
|
||||||
|
isAccepted: true
|
||||||
|
})
|
||||||
|
.returning("*");
|
||||||
|
|
||||||
|
const encKeys = await generateUserSrpKeys(email, password);
|
||||||
|
|
||||||
|
await knex(TableName.UserEncryptionKey)
|
||||||
|
.insert({ userId: ghostUser.id, encryptionVersion: 2, publicKey: encKeys.publicKey })
|
||||||
|
.onConflict("userId")
|
||||||
|
.merge();
|
||||||
|
|
||||||
|
await knex(TableName.OrgMembership)
|
||||||
|
.insert({
|
||||||
|
orgId,
|
||||||
|
userId: ghostUser.id,
|
||||||
|
role: OrgMembershipRole.Admin,
|
||||||
|
status: OrgMembershipStatus.Accepted,
|
||||||
|
isActive: true
|
||||||
|
})
|
||||||
|
.returning("*");
|
||||||
|
|
||||||
|
const [projectMembership] = await knex(TableName.ProjectMembership)
|
||||||
|
.insert({
|
||||||
|
userId: ghostUser.id,
|
||||||
|
projectId
|
||||||
|
})
|
||||||
|
.returning("*");
|
||||||
|
|
||||||
|
await knex(TableName.ProjectUserMembershipRole).insert({
|
||||||
|
projectMembershipId: projectMembership.id,
|
||||||
|
role: ProjectMembershipRole.Admin
|
||||||
|
});
|
||||||
|
|
||||||
|
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({
|
||||||
|
publicKey: encKeys.publicKey,
|
||||||
|
privateKey: encKeys.plainPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex(TableName.ProjectKeys).insert({
|
||||||
|
projectId,
|
||||||
|
receiverId: ghostUser.id,
|
||||||
|
encryptedKey: encryptedProjectKey,
|
||||||
|
nonce: encryptedProjectKeyIv,
|
||||||
|
senderId: ghostUser.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const { iv, tag, ciphertext, encoding, algorithm } = crypto
|
||||||
|
.encryption()
|
||||||
|
.symmetric()
|
||||||
|
.encryptWithRootEncryptionKey(encKeys.plainPrivateKey);
|
||||||
|
|
||||||
|
await knex(TableName.ProjectBot).insert({
|
||||||
|
name: "Infisical Bot (Ghost)",
|
||||||
|
projectId,
|
||||||
|
tag,
|
||||||
|
iv,
|
||||||
|
encryptedProjectKey,
|
||||||
|
encryptedProjectKeyNonce: encryptedProjectKeyIv,
|
||||||
|
encryptedPrivateKey: ciphertext,
|
||||||
|
isActive: true,
|
||||||
|
publicKey: encKeys.publicKey,
|
||||||
|
senderId: ghostUser.id,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding: encoding
|
||||||
|
});
|
||||||
|
|
||||||
|
const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, knex);
|
||||||
|
|
||||||
|
if (!latestKey) {
|
||||||
|
throw new Error("Latest key not found for user");
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await userDAL.findUserEncKeyByUserId(userId, knex);
|
||||||
|
|
||||||
|
if (!user || !user.publicKey) {
|
||||||
|
throw new Error("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [projectAdmin] = assignWorkspaceKeysToMembers({
|
||||||
|
decryptKey: latestKey,
|
||||||
|
userPrivateKey: encKeys.plainPrivateKey,
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
userPublicKey: user.publicKey,
|
||||||
|
orgMembershipId: userOrgMembershipId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a membership for the user
|
||||||
|
const userProjectMembership = await projectMembershipDAL.create(
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
userId: user.id
|
||||||
|
},
|
||||||
|
knex
|
||||||
|
);
|
||||||
|
await projectUserMembershipRoleDAL.create(
|
||||||
|
{ projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin },
|
||||||
|
knex
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a project key for the user
|
||||||
|
await projectKeyDAL.create(
|
||||||
|
{
|
||||||
|
encryptedKey: projectAdmin.workspaceEncryptedKey,
|
||||||
|
nonce: projectAdmin.workspaceEncryptedNonce,
|
||||||
|
senderId: ghostUser.id,
|
||||||
|
receiverId: user.id,
|
||||||
|
projectId
|
||||||
|
},
|
||||||
|
knex
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: ghostUser,
|
||||||
|
keys: encKeys
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export async function seed(knex: Knex): Promise<void> {
|
export async function seed(knex: Knex): Promise<void> {
|
||||||
// Deletes ALL existing entries
|
// Deletes ALL existing entries
|
||||||
await knex(TableName.Project).del();
|
await knex(TableName.Project).del();
|
||||||
await knex(TableName.Environment).del();
|
await knex(TableName.Environment).del();
|
||||||
await knex(TableName.SecretFolder).del();
|
await knex(TableName.SecretFolder).del();
|
||||||
|
|
||||||
|
initLogger();
|
||||||
|
|
||||||
|
const superAdminDAL = superAdminDALFactory(knex);
|
||||||
|
await initEnvConfig(superAdminDAL, logger);
|
||||||
|
|
||||||
const [project] = await knex(TableName.Project)
|
const [project] = await knex(TableName.Project)
|
||||||
.insert({
|
.insert({
|
||||||
name: seedData1.project.name,
|
name: seedData1.project.name,
|
||||||
@@ -29,29 +195,24 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
})
|
})
|
||||||
.returning("*");
|
.returning("*");
|
||||||
|
|
||||||
const projectMembership = await knex(TableName.ProjectMembership)
|
const userOrgMembership = await knex(TableName.OrgMembership)
|
||||||
.insert({
|
.where({
|
||||||
projectId: project.id,
|
orgId: seedData1.organization.id,
|
||||||
userId: seedData1.id
|
userId: seedData1.id
|
||||||
})
|
})
|
||||||
.returning("*");
|
.first();
|
||||||
await knex(TableName.ProjectUserMembershipRole).insert({
|
|
||||||
role: ProjectMembershipRole.Admin,
|
|
||||||
projectMembershipId: projectMembership[0].id
|
|
||||||
});
|
|
||||||
|
|
||||||
|
if (!userOrgMembership) {
|
||||||
|
throw new Error("User org membership not found");
|
||||||
|
}
|
||||||
const user = await knex(TableName.UserEncryptionKey).where({ userId: seedData1.id }).first();
|
const user = await knex(TableName.UserEncryptionKey).where({ userId: seedData1.id }).first();
|
||||||
if (!user) throw new Error("User not found");
|
if (!user) throw new Error("User not found");
|
||||||
|
|
||||||
const userPrivateKey = await getUserPrivateKey(seedData1.password, user);
|
if (!user.publicKey) {
|
||||||
const projectKey = buildUserProjectKey(userPrivateKey, user.publicKey);
|
throw new Error("User public key not found");
|
||||||
await knex(TableName.ProjectKeys).insert({
|
}
|
||||||
projectId: project.id,
|
|
||||||
nonce: projectKey.nonce,
|
await createUserWithGhostUser(seedData1.organization.id, project.id, seedData1.id, userOrgMembership.id, knex);
|
||||||
encryptedKey: projectKey.ciphertext,
|
|
||||||
receiverId: seedData1.id,
|
|
||||||
senderId: seedData1.id
|
|
||||||
});
|
|
||||||
|
|
||||||
// create default environments and default folders
|
// create default environments and default folders
|
||||||
const envs = await knex(TableName.Environment)
|
const envs = await knex(TableName.Environment)
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { initEnvConfig } from "@app/lib/config/env";
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
|
import { initLogger, logger } from "@app/lib/logger";
|
||||||
|
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
|
|
||||||
import { IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas";
|
import { IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas";
|
||||||
import { seedData1 } from "../seed-data";
|
import { seedData1 } from "../seed-data";
|
||||||
@@ -10,6 +13,11 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
await knex(TableName.Identity).del();
|
await knex(TableName.Identity).del();
|
||||||
await knex(TableName.IdentityOrgMembership).del();
|
await knex(TableName.IdentityOrgMembership).del();
|
||||||
|
|
||||||
|
initLogger();
|
||||||
|
|
||||||
|
const superAdminDAL = superAdminDALFactory(knex);
|
||||||
|
await initEnvConfig(superAdminDAL, logger);
|
||||||
|
|
||||||
// Inserts seed entries
|
// Inserts seed entries
|
||||||
await knex(TableName.Identity).insert([
|
await knex(TableName.Identity).insert([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,11 +13,9 @@ const AUTH_REFRESH_INTERVAL = 60 * 1000;
|
|||||||
const HEART_BEAT_INTERVAL = 15 * 1000;
|
const HEART_BEAT_INTERVAL = 15 * 1000;
|
||||||
|
|
||||||
export const sseServiceFactory = (bus: TEventBusService, redis: Redis) => {
|
export const sseServiceFactory = (bus: TEventBusService, redis: Redis) => {
|
||||||
let heartbeatInterval: NodeJS.Timeout | null = null;
|
|
||||||
|
|
||||||
const clients = new Set<EventStreamClient>();
|
const clients = new Set<EventStreamClient>();
|
||||||
|
|
||||||
heartbeatInterval = setInterval(() => {
|
const heartbeatInterval = setInterval(() => {
|
||||||
for (const client of clients) {
|
for (const client of clients) {
|
||||||
if (client.stream.closed) continue;
|
if (client.stream.closed) continue;
|
||||||
void client.ping();
|
void client.ping();
|
||||||
|
|||||||
@@ -66,15 +66,24 @@ export type EventStreamClient = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function createEventStreamClient(redis: Redis, options: IEventStreamClientOpts): EventStreamClient {
|
export function createEventStreamClient(redis: Redis, options: IEventStreamClientOpts): EventStreamClient {
|
||||||
const rules = options.registered.map((r) => ({
|
const rules = options.registered.map((r) => {
|
||||||
subject: options.type,
|
const secretPath = r.conditions?.secretPath;
|
||||||
action: "subscribe",
|
const hasConditions = r.conditions?.environmentSlug || r.conditions?.secretPath;
|
||||||
conditions: {
|
|
||||||
eventType: r.event,
|
return {
|
||||||
secretPath: r.conditions?.secretPath ?? "/",
|
subject: options.type,
|
||||||
environment: r.conditions?.environmentSlug
|
action: "subscribe",
|
||||||
}
|
conditions: {
|
||||||
}));
|
eventType: r.event,
|
||||||
|
...(hasConditions
|
||||||
|
? {
|
||||||
|
environment: r.conditions?.environmentSlug ?? "",
|
||||||
|
secretPath: { $glob: secretPath }
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const id = `sse-${nanoid()}`;
|
const id = `sse-${nanoid()}`;
|
||||||
const control = new AbortController();
|
const control = new AbortController();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
|
import { ProjectVersion, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, ScimRequestError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, ScimRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
@@ -65,6 +65,18 @@ const addAcceptedUsersToGroup = async ({
|
|||||||
const userKeysSet = new Set(keys.map((k) => `${k.projectId}-${k.receiverId}`));
|
const userKeysSet = new Set(keys.map((k) => `${k.projectId}-${k.receiverId}`));
|
||||||
|
|
||||||
for await (const projectId of projectIds) {
|
for await (const projectId of projectIds) {
|
||||||
|
const project = await projectDAL.findById(projectId, tx);
|
||||||
|
if (!project) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Failed to find project with ID '${projectId}'`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project.version !== ProjectVersion.V1 && project.version !== ProjectVersion.V2) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const usersToAddProjectKeyFor = users.filter((u) => !userKeysSet.has(`${projectId}-${u.userId}`));
|
const usersToAddProjectKeyFor = users.filter((u) => !userKeysSet.has(`${projectId}-${u.userId}`));
|
||||||
|
|
||||||
if (usersToAddProjectKeyFor.length) {
|
if (usersToAddProjectKeyFor.length) {
|
||||||
@@ -86,6 +98,12 @@ const addAcceptedUsersToGroup = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!ghostUserLatestKey.sender.publicKey) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Failed to find project owner's public key in project with ID '${projectId}'`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const bot = await projectBotDAL.findOne({ projectId }, tx);
|
const bot = await projectBotDAL.findOne({ projectId }, tx);
|
||||||
|
|
||||||
if (!bot) {
|
if (!bot) {
|
||||||
@@ -112,6 +130,12 @@ const addAcceptedUsersToGroup = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const projectKeysToAdd = usersToAddProjectKeyFor.map((user) => {
|
const projectKeysToAdd = usersToAddProjectKeyFor.map((user) => {
|
||||||
|
if (!user.publicKey) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Failed to find user's public key in project with ID '${projectId}'`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { ciphertext: encryptedKey, nonce } = crypto
|
const { ciphertext: encryptedKey, nonce } = crypto
|
||||||
.encryption()
|
.encryption()
|
||||||
.asymmetric()
|
.asymmetric()
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ type TGroupServiceFactoryDep = {
|
|||||||
TUserGroupMembershipDALFactory,
|
TUserGroupMembershipDALFactory,
|
||||||
"findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find"
|
"findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find"
|
||||||
>;
|
>;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export type TAddUsersToGroup = {
|
|||||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">;
|
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">;
|
||||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
tx: Knex;
|
tx: Knex;
|
||||||
};
|
};
|
||||||
@@ -78,7 +78,7 @@ export type TAddUsersToGroupByUserIds = {
|
|||||||
orgDAL: Pick<TOrgDALFactory, "findMembership">;
|
orgDAL: Pick<TOrgDALFactory, "findMembership">;
|
||||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
tx?: Knex;
|
tx?: Knex;
|
||||||
};
|
};
|
||||||
@@ -102,7 +102,7 @@ export type TConvertPendingGroupAdditionsToGroupMemberships = {
|
|||||||
>;
|
>;
|
||||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
tx?: Knex;
|
tx?: Knex;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ type TLdapConfigServiceFactoryDep = {
|
|||||||
groupDAL: Pick<TGroupDALFactory, "find" | "findOne">;
|
groupDAL: Pick<TGroupDALFactory, "find" | "findOne">;
|
||||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
userGroupMembershipDAL: Pick<
|
userGroupMembershipDAL: Pick<
|
||||||
TUserGroupMembershipDALFactory,
|
TUserGroupMembershipDALFactory,
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ type TOidcConfigServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ type TScimServiceFactoryDep = {
|
|||||||
TOrgMembershipDALFactory,
|
TOrgMembershipDALFactory,
|
||||||
"find" | "findOne" | "create" | "updateById" | "findById" | "update"
|
"find" | "findOne" | "create" | "updateById" | "findById" | "update"
|
||||||
>;
|
>;
|
||||||
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">;
|
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser" | "findById">;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">;
|
||||||
groupDAL: Pick<
|
groupDAL: Pick<
|
||||||
TGroupDALFactory,
|
TGroupDALFactory,
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ export enum ApiDocsTags {
|
|||||||
SecretScanning = "Secret Scanning",
|
SecretScanning = "Secret Scanning",
|
||||||
OidcSso = "OIDC SSO",
|
OidcSso = "OIDC SSO",
|
||||||
SamlSso = "SAML SSO",
|
SamlSso = "SAML SSO",
|
||||||
LdapSso = "LDAP SSO"
|
LdapSso = "LDAP SSO",
|
||||||
|
Events = "Event Subscriptions"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GROUPS = {
|
export const GROUPS = {
|
||||||
@@ -2872,3 +2873,10 @@ export const LdapSso = {
|
|||||||
caCert: "The CA certificate to use when verifying the LDAP server certificate."
|
caCert: "The CA certificate to use when verifying the LDAP server certificate."
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const EventSubscriptions = {
|
||||||
|
SUBSCRIBE_PROJECT_EVENTS: {
|
||||||
|
projectId: "The ID of the project to subscribe to events for.",
|
||||||
|
register: "List of events you want to subscribe to"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ type DecryptedIntegrationAuths = z.infer<typeof DecryptedIntegrationAuthsSchema>
|
|||||||
|
|
||||||
type TLatestKey = TProjectKeys & {
|
type TLatestKey = TProjectKeys & {
|
||||||
sender: {
|
sender: {
|
||||||
publicKey: string;
|
publicKey?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -91,6 +91,10 @@ const getDecryptedValues = (data: Array<{ ciphertext: string; iv: string; tag: s
|
|||||||
return results;
|
return results;
|
||||||
};
|
};
|
||||||
export const decryptSecrets = (encryptedSecrets: TSecrets[], privateKey: string, latestKey: TLatestKey) => {
|
export const decryptSecrets = (encryptedSecrets: TSecrets[], privateKey: string, latestKey: TLatestKey) => {
|
||||||
|
if (!latestKey.sender.publicKey) {
|
||||||
|
throw new Error("Latest key sender public key not found");
|
||||||
|
}
|
||||||
|
|
||||||
const key = crypto.encryption().asymmetric().decrypt({
|
const key = crypto.encryption().asymmetric().decrypt({
|
||||||
ciphertext: latestKey.encryptedKey,
|
ciphertext: latestKey.encryptedKey,
|
||||||
nonce: latestKey.nonce,
|
nonce: latestKey.nonce,
|
||||||
@@ -143,6 +147,10 @@ export const decryptSecretVersions = (
|
|||||||
privateKey: string,
|
privateKey: string,
|
||||||
latestKey: TLatestKey
|
latestKey: TLatestKey
|
||||||
) => {
|
) => {
|
||||||
|
if (!latestKey.sender.publicKey) {
|
||||||
|
throw new Error("Latest key sender public key not found");
|
||||||
|
}
|
||||||
|
|
||||||
const key = crypto.encryption().asymmetric().decrypt({
|
const key = crypto.encryption().asymmetric().decrypt({
|
||||||
ciphertext: latestKey.encryptedKey,
|
ciphertext: latestKey.encryptedKey,
|
||||||
nonce: latestKey.nonce,
|
nonce: latestKey.nonce,
|
||||||
@@ -195,6 +203,10 @@ export const decryptSecretApprovals = (
|
|||||||
privateKey: string,
|
privateKey: string,
|
||||||
latestKey: TLatestKey
|
latestKey: TLatestKey
|
||||||
) => {
|
) => {
|
||||||
|
if (!latestKey.sender.publicKey) {
|
||||||
|
throw new Error("Latest key sender public key not found");
|
||||||
|
}
|
||||||
|
|
||||||
const key = crypto.encryption().asymmetric().decrypt({
|
const key = crypto.encryption().asymmetric().decrypt({
|
||||||
ciphertext: latestKey.encryptedKey,
|
ciphertext: latestKey.encryptedKey,
|
||||||
nonce: latestKey.nonce,
|
nonce: latestKey.nonce,
|
||||||
@@ -247,6 +259,10 @@ export const decryptIntegrationAuths = (
|
|||||||
privateKey: string,
|
privateKey: string,
|
||||||
latestKey: TLatestKey
|
latestKey: TLatestKey
|
||||||
) => {
|
) => {
|
||||||
|
if (!latestKey.sender.publicKey) {
|
||||||
|
throw new Error("Latest key sender public key not found");
|
||||||
|
}
|
||||||
|
|
||||||
const key = crypto.encryption().asymmetric().decrypt({
|
const key = crypto.encryption().asymmetric().decrypt({
|
||||||
ciphertext: latestKey.encryptedKey,
|
ciphertext: latestKey.encryptedKey,
|
||||||
nonce: latestKey.nonce,
|
nonce: latestKey.nonce,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import jsrp from "jsrp";
|
|||||||
import { TUserEncryptionKeys } from "@app/db/schemas";
|
import { TUserEncryptionKeys } from "@app/db/schemas";
|
||||||
import { UserEncryption } from "@app/services/user/user-types";
|
import { UserEncryption } from "@app/services/user/user-types";
|
||||||
|
|
||||||
|
import { BadRequestError } from "../errors";
|
||||||
import { crypto, SymmetricKeySize } from "./cryptography";
|
import { crypto, SymmetricKeySize } from "./cryptography";
|
||||||
|
|
||||||
export const generateSrpServerKey = async (salt: string, verifier: string) => {
|
export const generateSrpServerKey = async (salt: string, verifier: string) => {
|
||||||
@@ -127,6 +128,10 @@ export const getUserPrivateKey = async (
|
|||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
if (user.encryptionVersion === UserEncryption.V1) {
|
if (user.encryptionVersion === UserEncryption.V1) {
|
||||||
|
if (!user.encryptedPrivateKey || !user.iv || !user.tag || !user.salt) {
|
||||||
|
throw new BadRequestError({ message: "User encrypted private key not found" });
|
||||||
|
}
|
||||||
|
|
||||||
return crypto
|
return crypto
|
||||||
.encryption()
|
.encryption()
|
||||||
.symmetric()
|
.symmetric()
|
||||||
@@ -138,12 +143,25 @@ export const getUserPrivateKey = async (
|
|||||||
keySize: SymmetricKeySize.Bits128
|
keySize: SymmetricKeySize.Bits128
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// still used for legacy things
|
||||||
if (
|
if (
|
||||||
user.encryptionVersion === UserEncryption.V2 &&
|
user.encryptionVersion === UserEncryption.V2 &&
|
||||||
user.protectedKey &&
|
user.protectedKey &&
|
||||||
user.protectedKeyIV &&
|
user.protectedKeyIV &&
|
||||||
user.protectedKeyTag
|
user.protectedKeyTag
|
||||||
) {
|
) {
|
||||||
|
if (
|
||||||
|
!user.salt ||
|
||||||
|
!user.protectedKey ||
|
||||||
|
!user.protectedKeyIV ||
|
||||||
|
!user.protectedKeyTag ||
|
||||||
|
!user.encryptedPrivateKey ||
|
||||||
|
!user.iv ||
|
||||||
|
!user.tag
|
||||||
|
) {
|
||||||
|
throw new BadRequestError({ message: "User encrypted private key not found" });
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
@@ -775,7 +775,6 @@ export const registerRoutes = async (
|
|||||||
orgRoleDAL,
|
orgRoleDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
projectBotDAL,
|
|
||||||
incidentContactDAL,
|
incidentContactDAL,
|
||||||
tokenService,
|
tokenService,
|
||||||
projectUserAdditionalPrivilegeDAL,
|
projectUserAdditionalPrivilegeDAL,
|
||||||
@@ -850,7 +849,6 @@ export const registerRoutes = async (
|
|||||||
projectDAL,
|
projectDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
userDAL,
|
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
projectMembershipDAL
|
projectMembershipDAL
|
||||||
@@ -1138,11 +1136,9 @@ export const registerRoutes = async (
|
|||||||
projectBotService,
|
projectBotService,
|
||||||
identityProjectDAL,
|
identityProjectDAL,
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
projectKeyDAL,
|
|
||||||
userDAL,
|
userDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
orgService,
|
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
@@ -1162,7 +1158,6 @@ export const registerRoutes = async (
|
|||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
keyStore,
|
keyStore,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectBotDAL,
|
|
||||||
certificateTemplateDAL,
|
certificateTemplateDAL,
|
||||||
projectSlackConfigDAL,
|
projectSlackConfigDAL,
|
||||||
slackIntegrationDAL,
|
slackIntegrationDAL,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
|||||||
import { getServerSentEventsHeaders } from "@app/ee/services/event/event-sse-stream";
|
import { getServerSentEventsHeaders } from "@app/ee/services/event/event-sse-stream";
|
||||||
import { EventRegisterSchema } from "@app/ee/services/event/types";
|
import { EventRegisterSchema } from "@app/ee/services/event/types";
|
||||||
import { ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { ApiDocsTags, EventSubscriptions } from "@app/lib/api-docs";
|
||||||
import { BadRequestError, ForbiddenRequestError, RateLimitError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, RateLimitError } from "@app/lib/errors";
|
||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
@@ -20,10 +21,14 @@ export const registerEventRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.Events],
|
||||||
|
description: "Subscribe to project events",
|
||||||
body: z.object({
|
body: z.object({
|
||||||
projectId: z.string().trim(),
|
projectId: z.string().trim().describe(EventSubscriptions.SUBSCRIBE_PROJECT_EVENTS.projectId),
|
||||||
register: z.array(EventRegisterSchema).max(10)
|
register: z.array(EventRegisterSchema).min(1).max(10)
|
||||||
})
|
}),
|
||||||
|
produces: ["text/event-stream"]
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req, reply) => {
|
handler: async (req, reply) => {
|
||||||
@@ -75,13 +80,15 @@ export const registerEventRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.body.register.forEach((r) => {
|
req.body.register.forEach((r) => {
|
||||||
|
const fields = {
|
||||||
|
environment: r.conditions?.environmentSlug ?? "",
|
||||||
|
secretPath: r.conditions?.secretPath ?? "/",
|
||||||
|
eventType: r.event
|
||||||
|
};
|
||||||
|
|
||||||
const allowed = info.permission.can(
|
const allowed = info.permission.can(
|
||||||
ProjectPermissionSecretActions.Subscribe,
|
ProjectPermissionSecretActions.Subscribe,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
subject(ProjectPermissionSub.Secrets, fields)
|
||||||
environment: r.conditions?.environmentSlug ?? "",
|
|
||||||
secretPath: r.conditions?.secretPath ?? "/",
|
|
||||||
eventType: r.event
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
@@ -89,9 +96,9 @@ export const registerEventRouter = async (server: FastifyZodProvider) => {
|
|||||||
name: "PermissionDenied",
|
name: "PermissionDenied",
|
||||||
message: `You are not allowed to subscribe on secrets`,
|
message: `You are not allowed to subscribe on secrets`,
|
||||||
details: {
|
details: {
|
||||||
event: r.event,
|
event: fields.eventType,
|
||||||
environmentSlug: r.conditions?.environmentSlug,
|
environmentSlug: fields.environment,
|
||||||
secretPath: r.conditions?.secretPath ?? "/"
|
secretPath: fields.secretPath
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
lastName: true,
|
lastName: true,
|
||||||
id: true,
|
id: true,
|
||||||
superAdmin: true
|
superAdmin: true
|
||||||
}).merge(z.object({ publicKey: z.string().nullable() }))
|
}).merge(z.object({ publicKey: z.string().nullable().optional() }))
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.omit({ createdAt: true, updatedAt: true })
|
.omit({ createdAt: true, updatedAt: true })
|
||||||
|
|||||||
@@ -9,73 +9,6 @@ import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
|||||||
import { UserEncryption } from "@app/services/user/user-types";
|
import { UserEncryption } from "@app/services/user/user-types";
|
||||||
|
|
||||||
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
|
||||||
method: "POST",
|
|
||||||
url: "/srp1",
|
|
||||||
config: {
|
|
||||||
rateLimit: authRateLimit
|
|
||||||
},
|
|
||||||
schema: {
|
|
||||||
body: z.object({
|
|
||||||
clientPublicKey: z.string().trim()
|
|
||||||
}),
|
|
||||||
response: {
|
|
||||||
200: z.object({
|
|
||||||
serverPublicKey: z.string(),
|
|
||||||
salt: z.string()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
|
||||||
handler: async (req) => {
|
|
||||||
const { salt, serverPublicKey } = await server.services.password.generateServerPubKey(
|
|
||||||
req.permission.id,
|
|
||||||
req.body.clientPublicKey
|
|
||||||
);
|
|
||||||
return { salt, serverPublicKey };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.route({
|
|
||||||
method: "POST",
|
|
||||||
url: "/change-password",
|
|
||||||
config: {
|
|
||||||
rateLimit: authRateLimit
|
|
||||||
},
|
|
||||||
schema: {
|
|
||||||
body: z.object({
|
|
||||||
clientProof: z.string().trim(),
|
|
||||||
protectedKey: z.string().trim(),
|
|
||||||
protectedKeyIV: z.string().trim(),
|
|
||||||
protectedKeyTag: z.string().trim(),
|
|
||||||
encryptedPrivateKey: z.string().trim(),
|
|
||||||
encryptedPrivateKeyIV: z.string().trim(),
|
|
||||||
encryptedPrivateKeyTag: z.string().trim(),
|
|
||||||
salt: z.string().trim(),
|
|
||||||
verifier: z.string().trim(),
|
|
||||||
password: z.string().trim()
|
|
||||||
}),
|
|
||||||
response: {
|
|
||||||
200: z.object({
|
|
||||||
message: z.string()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
|
||||||
handler: async (req, res) => {
|
|
||||||
const appCfg = getConfig();
|
|
||||||
await server.services.password.changePassword({ ...req.body, userId: req.permission.id });
|
|
||||||
|
|
||||||
void res.cookie("jid", "", {
|
|
||||||
httpOnly: true,
|
|
||||||
path: "/",
|
|
||||||
sameSite: "strict",
|
|
||||||
secure: appCfg.HTTPS_ENABLED
|
|
||||||
});
|
|
||||||
return { message: "Successfully changed password" };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/email/password-reset",
|
url: "/email/password-reset",
|
||||||
@@ -131,41 +64,6 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
|
||||||
method: "POST",
|
|
||||||
url: "/backup-private-key",
|
|
||||||
config: {
|
|
||||||
rateLimit: authRateLimit
|
|
||||||
},
|
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
|
||||||
schema: {
|
|
||||||
body: z.object({
|
|
||||||
clientProof: z.string().trim(),
|
|
||||||
encryptedPrivateKey: z.string().trim(),
|
|
||||||
iv: z.string().trim(),
|
|
||||||
tag: z.string().trim(),
|
|
||||||
salt: z.string().trim(),
|
|
||||||
verifier: z.string().trim()
|
|
||||||
}),
|
|
||||||
response: {
|
|
||||||
200: z.object({
|
|
||||||
message: z.string(),
|
|
||||||
backupPrivateKey: BackupPrivateKeySchema.omit({ verifier: true })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handler: async (req) => {
|
|
||||||
const token = validateSignUpAuthorization(req.headers.authorization as string, "", false)!;
|
|
||||||
const backupPrivateKey = await server.services.password.createBackupPrivateKey({
|
|
||||||
...req.body,
|
|
||||||
userId: token.userId
|
|
||||||
});
|
|
||||||
if (!backupPrivateKey) throw new Error("Failed to create backup key");
|
|
||||||
|
|
||||||
return { message: "Successfully updated backup private key", backupPrivateKey };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/backup-private-key",
|
url: "/backup-private-key",
|
||||||
@@ -257,14 +155,6 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
protectedKey: z.string().trim(),
|
|
||||||
protectedKeyIV: z.string().trim(),
|
|
||||||
protectedKeyTag: z.string().trim(),
|
|
||||||
encryptedPrivateKey: z.string().trim(),
|
|
||||||
encryptedPrivateKeyIV: z.string().trim(),
|
|
||||||
encryptedPrivateKeyTag: z.string().trim(),
|
|
||||||
salt: z.string().trim(),
|
|
||||||
verifier: z.string().trim(),
|
|
||||||
password: z.string().trim(),
|
password: z.string().trim(),
|
||||||
token: z.string().trim()
|
token: z.string().trim()
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
publicKeys: z
|
publicKeys: z
|
||||||
.object({
|
.object({
|
||||||
publicKey: z.string().optional(),
|
publicKey: z.string().nullable().optional(),
|
||||||
userId: z.string()
|
userId: z.string()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
import { UsersSchema } from "@app/db/schemas";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
@@ -19,23 +19,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
user: UsersSchema.merge(
|
user: UsersSchema.extend({
|
||||||
UserEncryptionKeysSchema.pick({
|
encryptionVersion: z.number()
|
||||||
clientPublicKey: true,
|
})
|
||||||
serverPrivateKey: true,
|
|
||||||
encryptionVersion: true,
|
|
||||||
protectedKey: true,
|
|
||||||
protectedKeyIV: true,
|
|
||||||
protectedKeyTag: true,
|
|
||||||
publicKey: true,
|
|
||||||
encryptedPrivateKey: true,
|
|
||||||
iv: true,
|
|
||||||
tag: true,
|
|
||||||
salt: true,
|
|
||||||
verifier: true,
|
|
||||||
userId: true
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -94,26 +80,6 @@ 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",
|
||||||
|
|||||||
@@ -97,13 +97,13 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
|||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
encryptionVersion: z.number().default(1).nullable().optional(),
|
encryptionVersion: z.number().default(1).nullable().optional(),
|
||||||
protectedKey: z.string().nullable(),
|
protectedKey: z.string().nullish(),
|
||||||
protectedKeyIV: z.string().nullable(),
|
protectedKeyIV: z.string().nullish(),
|
||||||
protectedKeyTag: z.string().nullable(),
|
protectedKeyTag: z.string().nullish(),
|
||||||
publicKey: z.string(),
|
publicKey: z.string().nullish(),
|
||||||
encryptedPrivateKey: z.string(),
|
encryptedPrivateKey: z.string().nullish(),
|
||||||
iv: z.string(),
|
iv: z.string().nullish(),
|
||||||
tag: z.string(),
|
tag: z.string().nullish(),
|
||||||
token: z.string()
|
token: z.string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
firstName: true,
|
firstName: true,
|
||||||
lastName: true,
|
lastName: true,
|
||||||
id: true
|
id: true
|
||||||
}).extend({ publicKey: z.string().nullable() })
|
}).extend({ publicKey: z.string().nullish() })
|
||||||
}).omit({ createdAt: true, updatedAt: true })
|
}).omit({ createdAt: true, updatedAt: true })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { authRateLimit } from "@app/server/config/rateLimiter";
|
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns";
|
import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns";
|
||||||
@@ -41,13 +42,38 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: authRateLimit
|
rateLimit: authRateLimit
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
|
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
|
||||||
handler: async (req) => {
|
handler: async (req, res) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
await server.services.password.resetPasswordV2({
|
await server.services.password.resetPasswordV2({
|
||||||
type: ResetPasswordV2Type.LoggedInReset,
|
type: ResetPasswordV2Type.LoggedInReset,
|
||||||
userId: req.permission.id,
|
userId: req.permission.id,
|
||||||
newPassword: req.body.newPassword,
|
newPassword: req.body.newPassword,
|
||||||
oldPassword: req.body.oldPassword
|
oldPassword: req.body.oldPassword
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void res.cookie("jid", "", {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: appCfg.HTTPS_ENABLED
|
||||||
|
});
|
||||||
|
|
||||||
|
void res.cookie("infisical-project-assume-privileges", "", {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: appCfg.HTTPS_ENABLED,
|
||||||
|
maxAge: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
void res.cookie("aod", "", {
|
||||||
|
httpOnly: false,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "lax",
|
||||||
|
secure: appCfg.HTTPS_ENABLED,
|
||||||
|
maxAge: 0
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
200: ProjectKeysSchema.merge(
|
200: ProjectKeysSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
sender: z.object({
|
sender: z.object({
|
||||||
publicKey: z.string()
|
publicKey: z.string().optional()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
serverPublicKey: z.string(),
|
serverPublicKey: z.string().nullish(),
|
||||||
salt: z.string()
|
salt: z.string().nullish()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -124,14 +124,14 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
encryptionVersion: z.number().default(1).nullable().optional(),
|
encryptionVersion: z.number().default(1).nullish(),
|
||||||
protectedKey: z.string().nullable(),
|
protectedKey: z.string().nullish(),
|
||||||
protectedKeyIV: z.string().nullable(),
|
protectedKeyIV: z.string().nullish(),
|
||||||
protectedKeyTag: z.string().nullable(),
|
protectedKeyTag: z.string().nullish(),
|
||||||
publicKey: z.string(),
|
publicKey: z.string().nullish(),
|
||||||
encryptedPrivateKey: z.string(),
|
encryptedPrivateKey: z.string().nullish(),
|
||||||
iv: z.string(),
|
iv: z.string().nullish(),
|
||||||
tag: z.string(),
|
tag: z.string().nullish(),
|
||||||
token: z.string()
|
token: z.string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -181,4 +181,59 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
} as const;
|
} as const;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// New login route that doesn't use SRP
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/login",
|
||||||
|
config: {
|
||||||
|
rateLimit: authRateLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
email: z.string().trim(),
|
||||||
|
password: z.string().trim(),
|
||||||
|
providerAuthToken: z.string().trim().optional(),
|
||||||
|
captchaToken: z.string().trim().optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
accessToken: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req, res) => {
|
||||||
|
const userAgent = req.headers["user-agent"];
|
||||||
|
if (!userAgent) throw new Error("user agent header is required");
|
||||||
|
|
||||||
|
const { tokens } = await server.services.login.login({
|
||||||
|
email: req.body.email,
|
||||||
|
password: req.body.password,
|
||||||
|
ip: req.realIp,
|
||||||
|
userAgent,
|
||||||
|
providerAuthToken: req.body.providerAuthToken,
|
||||||
|
captchaToken: req.body.captchaToken
|
||||||
|
});
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
void res.setCookie("jid", tokens.refreshToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: appCfg.HTTPS_ENABLED
|
||||||
|
});
|
||||||
|
|
||||||
|
addAuthOriginDomainCookie(res);
|
||||||
|
|
||||||
|
void res.cookie("infisical-project-assume-privileges", "", {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: appCfg.HTTPS_ENABLED,
|
||||||
|
maxAge: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
return { accessToken: tokens.accessToken };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -98,15 +98,6 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
|||||||
email: z.string().trim(),
|
email: z.string().trim(),
|
||||||
firstName: z.string().trim(),
|
firstName: z.string().trim(),
|
||||||
lastName: z.string().trim().optional(),
|
lastName: z.string().trim().optional(),
|
||||||
protectedKey: z.string().trim(),
|
|
||||||
protectedKeyIV: z.string().trim(),
|
|
||||||
protectedKeyTag: z.string().trim(),
|
|
||||||
publicKey: z.string().trim(),
|
|
||||||
encryptedPrivateKey: z.string().trim(),
|
|
||||||
encryptedPrivateKeyIV: z.string().trim(),
|
|
||||||
encryptedPrivateKeyTag: z.string().trim(),
|
|
||||||
salt: z.string().trim(),
|
|
||||||
verifier: z.string().trim(),
|
|
||||||
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()
|
password: z.string()
|
||||||
@@ -189,15 +180,6 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
|||||||
password: z.string(),
|
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(),
|
|
||||||
protectedKeyIV: z.string().trim(),
|
|
||||||
protectedKeyTag: z.string().trim(),
|
|
||||||
publicKey: z.string().trim(),
|
|
||||||
encryptedPrivateKey: z.string().trim(),
|
|
||||||
encryptedPrivateKeyIV: z.string().trim(),
|
|
||||||
encryptedPrivateKeyTag: z.string().trim(),
|
|
||||||
salt: z.string().trim(),
|
|
||||||
verifier: z.string().trim(),
|
|
||||||
tokenMetadata: z.string().optional()
|
tokenMetadata: z.string().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
|
import { TUsers } from "@app/db/schemas";
|
||||||
|
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 { crypto } from "@app/lib/crypto";
|
import { crypto } from "@app/lib/crypto";
|
||||||
import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { AuthModeProviderJwtTokenPayload, AuthModeProviderSignUpTokenPayload, AuthTokenType } from "./auth-type";
|
import {
|
||||||
|
AuthMethod,
|
||||||
|
AuthModeProviderJwtTokenPayload,
|
||||||
|
AuthModeProviderSignUpTokenPayload,
|
||||||
|
AuthTokenType
|
||||||
|
} from "./auth-type";
|
||||||
|
|
||||||
export const validateProviderAuthToken = (providerToken: string, username?: string) => {
|
export const validateProviderAuthToken = (providerToken: string, username?: string) => {
|
||||||
if (!providerToken) throw new UnauthorizedError();
|
if (!providerToken) throw new UnauthorizedError();
|
||||||
@@ -97,3 +105,50 @@ export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const verifyCaptcha = async (user: TUsers, captchaToken?: string) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (
|
||||||
|
user.consecutiveFailedPasswordAttempts &&
|
||||||
|
user.consecutiveFailedPasswordAttempts >= 10 &&
|
||||||
|
Boolean(appCfg.CAPTCHA_SECRET)
|
||||||
|
) {
|
||||||
|
if (!captchaToken) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
name: "Captcha Required",
|
||||||
|
message: "Accomplish the required captcha by logging in via Web"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate captcha token
|
||||||
|
const response = await request.postForm<{ success: boolean }>("https://api.hcaptcha.com/siteverify", {
|
||||||
|
response: captchaToken,
|
||||||
|
secret: appCfg.CAPTCHA_SECRET
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.data.success) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
name: "Invalid Captcha"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAuthMethodAndOrgId = (email: string, providerAuthToken?: string) => {
|
||||||
|
let authMethod = AuthMethod.EMAIL;
|
||||||
|
let organizationId: string | undefined;
|
||||||
|
|
||||||
|
if (providerAuthToken) {
|
||||||
|
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email);
|
||||||
|
|
||||||
|
authMethod = decodedProviderToken.authMethod;
|
||||||
|
if (
|
||||||
|
(isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod)) &&
|
||||||
|
decodedProviderToken.orgId
|
||||||
|
) {
|
||||||
|
organizationId = decodedProviderToken.orgId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { authMethod, organizationId };
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { OrgMembershipRole, OrgMembershipStatus, TableName, TUsers, UserDeviceSc
|
|||||||
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
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 { request } from "@app/lib/config/request";
|
|
||||||
import { crypto, generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
import { crypto, generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||||
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||||
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
@@ -22,7 +21,8 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
|||||||
import { LoginMethod } from "../super-admin/super-admin-types";
|
import { LoginMethod } from "../super-admin/super-admin-types";
|
||||||
import { TTotpServiceFactory } from "../totp/totp-service";
|
import { TTotpServiceFactory } from "../totp/totp-service";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { enforceUserLockStatus, validateProviderAuthToken } from "./auth-fns";
|
import { UserEncryption } from "../user/user-types";
|
||||||
|
import { enforceUserLockStatus, getAuthMethodAndOrgId, validateProviderAuthToken, verifyCaptcha } from "./auth-fns";
|
||||||
import {
|
import {
|
||||||
TLoginClientProofDTO,
|
TLoginClientProofDTO,
|
||||||
TLoginGenServerPublicKeyDTO,
|
TLoginGenServerPublicKeyDTO,
|
||||||
@@ -208,6 +208,10 @@ export const authLoginServiceFactory = ({
|
|||||||
throw new Error("Failed to find user");
|
throw new Error("Failed to find user");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!userEnc.salt || !userEnc.verifier) {
|
||||||
|
throw new BadRequestError({ message: "Salt or verifier not found" });
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
serverCfg.enabledLoginMethods &&
|
serverCfg.enabledLoginMethods &&
|
||||||
!serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) &&
|
!serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) &&
|
||||||
@@ -247,8 +251,6 @@ export const authLoginServiceFactory = ({
|
|||||||
captchaToken,
|
captchaToken,
|
||||||
password
|
password
|
||||||
}: TLoginClientProofDTO) => {
|
}: TLoginClientProofDTO) => {
|
||||||
const appCfg = getConfig();
|
|
||||||
|
|
||||||
// akhilmhdh: case sensitive email resolution
|
// akhilmhdh: case sensitive email resolution
|
||||||
const usersByUsername = await userDAL.findUserEncKeyByUsername({
|
const usersByUsername = await userDAL.findUserEncKeyByUsername({
|
||||||
username: email
|
username: email
|
||||||
@@ -259,44 +261,11 @@ export const authLoginServiceFactory = ({
|
|||||||
const user = await userDAL.findById(userEnc.userId);
|
const user = await userDAL.findById(userEnc.userId);
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
|
|
||||||
let authMethod = AuthMethod.EMAIL;
|
const { authMethod, organizationId } = getAuthMethodAndOrgId(email, providerAuthToken);
|
||||||
let organizationId: string | undefined;
|
await verifyCaptcha(user, captchaToken);
|
||||||
|
|
||||||
if (providerAuthToken) {
|
if (!userEnc.salt || !userEnc.verifier) {
|
||||||
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email);
|
throw new BadRequestError({ message: "Salt or verifier not found" });
|
||||||
|
|
||||||
authMethod = decodedProviderToken.authMethod;
|
|
||||||
if (
|
|
||||||
(isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod)) &&
|
|
||||||
decodedProviderToken.orgId
|
|
||||||
) {
|
|
||||||
organizationId = decodedProviderToken.orgId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
user.consecutiveFailedPasswordAttempts &&
|
|
||||||
user.consecutiveFailedPasswordAttempts >= 10 &&
|
|
||||||
Boolean(appCfg.CAPTCHA_SECRET)
|
|
||||||
) {
|
|
||||||
if (!captchaToken) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
name: "Captcha Required",
|
|
||||||
message: "Accomplish the required captcha by logging in via Web"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate captcha token
|
|
||||||
const response = await request.postForm<{ success: boolean }>("https://api.hcaptcha.com/siteverify", {
|
|
||||||
response: captchaToken,
|
|
||||||
secret: appCfg.CAPTCHA_SECRET
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.data.success) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
name: "Invalid Captcha"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?");
|
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?");
|
||||||
@@ -371,6 +340,80 @@ export const authLoginServiceFactory = ({
|
|||||||
return { token, user: userEnc } as const;
|
return { token, user: userEnc } as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const login = async ({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
ip,
|
||||||
|
userAgent,
|
||||||
|
providerAuthToken,
|
||||||
|
captchaToken
|
||||||
|
}: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
ip: string;
|
||||||
|
userAgent: string;
|
||||||
|
providerAuthToken?: string;
|
||||||
|
captchaToken?: string;
|
||||||
|
}) => {
|
||||||
|
const usersByUsername = await userDAL.findUserEncKeyByUsername({
|
||||||
|
username: email
|
||||||
|
});
|
||||||
|
const userEnc =
|
||||||
|
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
|
||||||
|
|
||||||
|
if (!userEnc) throw new BadRequestError({ message: "User not found" });
|
||||||
|
|
||||||
|
if (userEnc.encryptionVersion !== UserEncryption.V2) {
|
||||||
|
throw new BadRequestError({ message: "Legacy encryption scheme not supported", name: "LegacyEncryptionScheme" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userEnc.hashedPassword) {
|
||||||
|
if (userEnc.authMethods?.includes(AuthMethod.EMAIL)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Legacy encryption scheme not supported",
|
||||||
|
name: "LegacyEncryptionScheme"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestError({ message: "No password found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { authMethod, organizationId } = getAuthMethodAndOrgId(email, providerAuthToken);
|
||||||
|
await verifyCaptcha(userEnc, captchaToken);
|
||||||
|
|
||||||
|
if (!(await crypto.hashing().compareHash(password, userEnc.hashedPassword))) {
|
||||||
|
await userDAL.update(
|
||||||
|
{ id: userEnc.userId },
|
||||||
|
{
|
||||||
|
$incr: {
|
||||||
|
consecutiveFailedPasswordAttempts: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
throw new BadRequestError({ message: "Invalid username or email" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await generateUserTokens({
|
||||||
|
user: {
|
||||||
|
...userEnc,
|
||||||
|
id: userEnc.userId
|
||||||
|
},
|
||||||
|
ip,
|
||||||
|
userAgent,
|
||||||
|
authMethod,
|
||||||
|
organizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
tokens: {
|
||||||
|
accessToken: token.access,
|
||||||
|
refreshToken: token.refresh
|
||||||
|
},
|
||||||
|
user: userEnc
|
||||||
|
} as const;
|
||||||
|
};
|
||||||
|
|
||||||
const selectOrganization = async ({
|
const selectOrganization = async ({
|
||||||
userAgent,
|
userAgent,
|
||||||
authJwtToken,
|
authJwtToken,
|
||||||
@@ -862,6 +905,7 @@ export const authLoginServiceFactory = ({
|
|||||||
resendMfaToken,
|
resendMfaToken,
|
||||||
verifyMfaToken,
|
verifyMfaToken,
|
||||||
selectOrganization,
|
selectOrganization,
|
||||||
generateUserTokens
|
generateUserTokens,
|
||||||
|
login
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { OrgServiceActor } from "@app/lib/types";
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
@@ -16,8 +13,6 @@ import { UserEncryption } from "../user/user-types";
|
|||||||
import { TAuthDALFactory } from "./auth-dal";
|
import { TAuthDALFactory } from "./auth-dal";
|
||||||
import {
|
import {
|
||||||
ResetPasswordV2Type,
|
ResetPasswordV2Type,
|
||||||
TChangePasswordDTO,
|
|
||||||
TCreateBackupPrivateKeyDTO,
|
|
||||||
TResetPasswordV2DTO,
|
TResetPasswordV2DTO,
|
||||||
TResetPasswordViaBackupKeyDTO,
|
TResetPasswordViaBackupKeyDTO,
|
||||||
TSetupPasswordViaBackupKeyDTO
|
TSetupPasswordViaBackupKeyDTO
|
||||||
@@ -40,79 +35,6 @@ export const authPaswordServiceFactory = ({
|
|||||||
smtpService,
|
smtpService,
|
||||||
totpConfigDAL
|
totpConfigDAL
|
||||||
}: TAuthPasswordServiceFactoryDep) => {
|
}: 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,
|
|
||||||
password
|
|
||||||
}: 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?");
|
|
||||||
|
|
||||||
const appCfg = getConfig();
|
|
||||||
const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS);
|
|
||||||
await userDAL.updateUserEncryptionByUserId(userId, {
|
|
||||||
encryptionVersion: 2,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv: encryptedPrivateKeyIV,
|
|
||||||
tag: encryptedPrivateKeyTag,
|
|
||||||
salt,
|
|
||||||
verifier,
|
|
||||||
serverPrivateKey: null,
|
|
||||||
clientPublicKey: null,
|
|
||||||
hashedPassword
|
|
||||||
});
|
|
||||||
|
|
||||||
if (tokenVersionId) {
|
|
||||||
await tokenService.clearTokenSessionById(userEnc.userId, tokenVersionId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Email password reset flow via email. Step 1 send email
|
* Email password reset flow via email. Step 1 send email
|
||||||
*/
|
*/
|
||||||
@@ -215,58 +137,17 @@ export const authPaswordServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newHashedPassword = await crypto.hashing().createHash(newPassword, cfg.SALT_ROUNDS);
|
if (user.encryptionVersion !== UserEncryption.V2) {
|
||||||
|
|
||||||
// we need to get the original private key first for v2
|
|
||||||
let privateKey: string;
|
|
||||||
if (
|
|
||||||
user.serverEncryptedPrivateKey &&
|
|
||||||
user.serverEncryptedPrivateKeyTag &&
|
|
||||||
user.serverEncryptedPrivateKeyIV &&
|
|
||||||
user.serverEncryptedPrivateKeyEncoding &&
|
|
||||||
user.encryptionVersion === UserEncryption.V2
|
|
||||||
) {
|
|
||||||
privateKey = crypto
|
|
||||||
.encryption()
|
|
||||||
.symmetric()
|
|
||||||
.decryptWithRootEncryptionKey({
|
|
||||||
iv: user.serverEncryptedPrivateKeyIV,
|
|
||||||
tag: user.serverEncryptedPrivateKeyTag,
|
|
||||||
ciphertext: user.serverEncryptedPrivateKey,
|
|
||||||
keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Cannot reset password without current credentials or recovery method",
|
message: "Cannot reset password without current credentials or recovery method",
|
||||||
name: "Reset password"
|
name: "Reset password"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const encKeys = await generateUserSrpKeys(user.username, newPassword, {
|
const newHashedPassword = await crypto.hashing().createHash(newPassword, cfg.SALT_ROUNDS);
|
||||||
publicKey: user.publicKey,
|
|
||||||
privateKey
|
|
||||||
});
|
|
||||||
|
|
||||||
const { tag, iv, ciphertext, encoding } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(privateKey);
|
|
||||||
|
|
||||||
await userDAL.updateUserEncryptionByUserId(userId, {
|
await userDAL.updateUserEncryptionByUserId(userId, {
|
||||||
hashedPassword: newHashedPassword,
|
hashedPassword: newHashedPassword
|
||||||
|
|
||||||
// srp params
|
|
||||||
salt: encKeys.salt,
|
|
||||||
verifier: encKeys.verifier,
|
|
||||||
|
|
||||||
protectedKey: encKeys.protectedKey,
|
|
||||||
protectedKeyIV: encKeys.protectedKeyIV,
|
|
||||||
protectedKeyTag: encKeys.protectedKeyTag,
|
|
||||||
encryptedPrivateKey: encKeys.encryptedPrivateKey,
|
|
||||||
iv: encKeys.encryptedPrivateKeyIV,
|
|
||||||
tag: encKeys.encryptedPrivateKeyTag,
|
|
||||||
|
|
||||||
serverEncryptedPrivateKey: ciphertext,
|
|
||||||
serverEncryptedPrivateKeyIV: iv,
|
|
||||||
serverEncryptedPrivateKeyTag: tag,
|
|
||||||
serverEncryptedPrivateKeyEncoding: encoding
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await tokenService.revokeAllMySessions(userId);
|
await tokenService.revokeAllMySessions(userId);
|
||||||
@@ -317,66 +198,6 @@ export const authPaswordServiceFactory = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
* 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 getBackupPrivateKeyOfUser = async (userId: string) => {
|
||||||
const user = await userDAL.findUserEncKeyByUserId(userId);
|
const user = await userDAL.findUserEncKeyByUserId(userId);
|
||||||
if (!user || (user && !user.isAccepted)) {
|
if (!user || (user && !user.isAccepted)) {
|
||||||
@@ -420,21 +241,7 @@ export const authPaswordServiceFactory = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const setupPassword = async (
|
const setupPassword = async ({ password, token }: TSetupPasswordViaBackupKeyDTO, actor: OrgServiceActor) => {
|
||||||
{
|
|
||||||
encryptedPrivateKey,
|
|
||||||
protectedKeyTag,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
salt,
|
|
||||||
verifier,
|
|
||||||
encryptedPrivateKeyIV,
|
|
||||||
encryptedPrivateKeyTag,
|
|
||||||
password,
|
|
||||||
token
|
|
||||||
}: TSetupPasswordViaBackupKeyDTO,
|
|
||||||
actor: OrgServiceActor
|
|
||||||
) => {
|
|
||||||
try {
|
try {
|
||||||
await tokenService.validateTokenForUser({
|
await tokenService.validateTokenForUser({
|
||||||
type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP,
|
type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP,
|
||||||
@@ -470,15 +277,7 @@ export const authPaswordServiceFactory = ({
|
|||||||
await userDAL.updateUserEncryptionByUserId(
|
await userDAL.updateUserEncryptionByUserId(
|
||||||
actor.id,
|
actor.id,
|
||||||
{
|
{
|
||||||
encryptionVersion: 2,
|
encryptionVersion: UserEncryption.V2,
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv: encryptedPrivateKeyIV,
|
|
||||||
tag: encryptedPrivateKeyTag,
|
|
||||||
salt,
|
|
||||||
verifier,
|
|
||||||
hashedPassword,
|
hashedPassword,
|
||||||
serverPrivateKey: null,
|
serverPrivateKey: null,
|
||||||
clientPublicKey: null
|
clientPublicKey: null
|
||||||
@@ -491,12 +290,9 @@ export const authPaswordServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
generateServerPubKey,
|
|
||||||
changePassword,
|
|
||||||
resetPasswordByBackupKey,
|
resetPasswordByBackupKey,
|
||||||
sendPasswordResetEmail,
|
sendPasswordResetEmail,
|
||||||
verifyPasswordResetEmail,
|
verifyPasswordResetEmail,
|
||||||
createBackupPrivateKey,
|
|
||||||
getBackupPrivateKeyOfUser,
|
getBackupPrivateKeyOfUser,
|
||||||
sendPasswordSetupEmail,
|
sendPasswordSetupEmail,
|
||||||
setupPassword,
|
setupPassword,
|
||||||
|
|||||||
@@ -1,18 +1,3 @@
|
|||||||
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;
|
|
||||||
password: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum ResetPasswordV2Type {
|
export enum ResetPasswordV2Type {
|
||||||
Recovery = "recovery",
|
Recovery = "recovery",
|
||||||
LoggedInReset = "logged-in-reset"
|
LoggedInReset = "logged-in-reset"
|
||||||
@@ -39,14 +24,6 @@ export type TResetPasswordViaBackupKeyDTO = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TSetupPasswordViaBackupKeyDTO = {
|
export type TSetupPasswordViaBackupKeyDTO = {
|
||||||
protectedKey: string;
|
|
||||||
protectedKeyIV: string;
|
|
||||||
protectedKeyTag: string;
|
|
||||||
encryptedPrivateKey: string;
|
|
||||||
encryptedPrivateKeyIV: string;
|
|
||||||
encryptedPrivateKeyTag: string;
|
|
||||||
salt: string;
|
|
||||||
verifier: string;
|
|
||||||
password: string;
|
password: string;
|
||||||
token: string;
|
token: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { OrgMembershipStatus, SecretKeyEncoding, TableName } from "@app/db/schemas";
|
import { OrgMembershipStatus, TableName } from "@app/db/schemas";
|
||||||
import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns";
|
import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns";
|
||||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||||
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 { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
|
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { getMinExpiresIn } from "@app/lib/fn";
|
import { getMinExpiresIn } from "@app/lib/fn";
|
||||||
import { isDisposableEmail } from "@app/lib/validator";
|
import { isDisposableEmail } from "@app/lib/validator";
|
||||||
@@ -41,7 +40,7 @@ type TAuthSignupDep = {
|
|||||||
| "findUserGroupMembershipsInProject"
|
| "findUserGroupMembershipsInProject"
|
||||||
>;
|
>;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById" | "findById">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||||
orgService: Pick<TOrgServiceFactory, "createOrganization" | "findOrganizationById">;
|
orgService: Pick<TOrgServiceFactory, "createOrganization" | "findOrganizationById">;
|
||||||
@@ -147,17 +146,8 @@ export const authSignupServiceFactory = ({
|
|||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
providerAuthToken,
|
providerAuthToken,
|
||||||
salt,
|
|
||||||
verifier,
|
|
||||||
publicKey,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
organizationName,
|
organizationName,
|
||||||
// attributionSource,
|
// attributionSource,
|
||||||
encryptedPrivateKey,
|
|
||||||
encryptedPrivateKeyIV,
|
|
||||||
encryptedPrivateKeyTag,
|
|
||||||
ip,
|
ip,
|
||||||
userAgent,
|
userAgent,
|
||||||
authorization,
|
authorization,
|
||||||
@@ -191,98 +181,18 @@ export const authSignupServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS);
|
const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS);
|
||||||
const privateKey = await getUserPrivateKey(password, {
|
|
||||||
salt,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv: encryptedPrivateKeyIV,
|
|
||||||
tag: encryptedPrivateKeyTag,
|
|
||||||
encryptionVersion: UserEncryption.V2
|
|
||||||
});
|
|
||||||
const { tag, encoding, ciphertext, iv } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(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");
|
||||||
const systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx);
|
|
||||||
let userEncKey;
|
|
||||||
|
|
||||||
// below condition is true means this is system generated credentials
|
const userEncKey = await userDAL.upsertUserEncryptionKey(
|
||||||
// the private key is actually system generated password
|
us.id,
|
||||||
// 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
|
encryptionVersion: UserEncryption.V2,
|
||||||
// without a dummy key in place some things will break and backward compatiability too. 2025 we will be removing all these things
|
hashedPassword
|
||||||
if (
|
},
|
||||||
systemGeneratedUserEncryptionKey &&
|
tx
|
||||||
!systemGeneratedUserEncryptionKey.hashedPassword &&
|
);
|
||||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey &&
|
|
||||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag &&
|
|
||||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV &&
|
|
||||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding
|
|
||||||
) {
|
|
||||||
// get server generated password
|
|
||||||
const serverGeneratedPassword = crypto
|
|
||||||
.encryption()
|
|
||||||
.symmetric()
|
|
||||||
.decryptWithRootEncryptionKey({
|
|
||||||
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 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 (
|
if (
|
||||||
@@ -400,19 +310,10 @@ export const authSignupServiceFactory = ({
|
|||||||
const completeAccountInvite = async ({
|
const completeAccountInvite = async ({
|
||||||
email,
|
email,
|
||||||
ip,
|
ip,
|
||||||
salt,
|
|
||||||
password,
|
password,
|
||||||
verifier,
|
|
||||||
firstName,
|
firstName,
|
||||||
publicKey,
|
|
||||||
userAgent,
|
userAgent,
|
||||||
lastName,
|
lastName,
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
encryptedPrivateKeyIV,
|
|
||||||
encryptedPrivateKeyTag,
|
|
||||||
authorization
|
authorization
|
||||||
}: TCompleteAccountInviteDTO) => {
|
}: TCompleteAccountInviteDTO) => {
|
||||||
const sanitizedEmail = email.trim().toLowerCase();
|
const sanitizedEmail = email.trim().toLowerCase();
|
||||||
@@ -437,94 +338,17 @@ export const authSignupServiceFactory = ({
|
|||||||
|
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS);
|
const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS);
|
||||||
const privateKey = await getUserPrivateKey(password, {
|
|
||||||
salt,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv: encryptedPrivateKeyIV,
|
|
||||||
tag: encryptedPrivateKeyTag,
|
|
||||||
encryptionVersion: 2
|
|
||||||
});
|
|
||||||
const { tag, encoding, ciphertext, iv } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(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");
|
||||||
const systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx);
|
const userEncKey = await userDAL.upsertUserEncryptionKey(
|
||||||
let userEncKey;
|
us.id,
|
||||||
// this means this is system generated credentials
|
{
|
||||||
// now replace the private key
|
encryptionVersion: 2,
|
||||||
if (
|
hashedPassword
|
||||||
systemGeneratedUserEncryptionKey &&
|
},
|
||||||
!systemGeneratedUserEncryptionKey.hashedPassword &&
|
tx
|
||||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey &&
|
);
|
||||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag &&
|
|
||||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV &&
|
|
||||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding
|
|
||||||
) {
|
|
||||||
// get server generated password
|
|
||||||
const serverGeneratedPassword = crypto
|
|
||||||
.encryption()
|
|
||||||
.symmetric()
|
|
||||||
.decryptWithRootEncryptionKey({
|
|
||||||
iv: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV,
|
|
||||||
tag: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag,
|
|
||||||
ciphertext: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey,
|
|
||||||
keyEncoding: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
|
|
||||||
});
|
|
||||||
const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, {
|
|
||||||
...systemGeneratedUserEncryptionKey
|
|
||||||
});
|
|
||||||
const encKeys = await generateUserSrpKeys(sanitizedEmail, 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(
|
const updatedMembersips = await orgDAL.updateMembership(
|
||||||
{ inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited },
|
{ inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited },
|
||||||
|
|||||||
@@ -3,15 +3,6 @@ export type TCompleteAccountSignupDTO = {
|
|||||||
password: string;
|
password: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
protectedKey: string;
|
|
||||||
protectedKeyIV: string;
|
|
||||||
protectedKeyTag: string;
|
|
||||||
publicKey: string;
|
|
||||||
encryptedPrivateKey: string;
|
|
||||||
encryptedPrivateKeyIV: string;
|
|
||||||
encryptedPrivateKeyTag: string;
|
|
||||||
salt: string;
|
|
||||||
verifier: string;
|
|
||||||
organizationName?: string;
|
organizationName?: string;
|
||||||
providerAuthToken?: string | null;
|
providerAuthToken?: string | null;
|
||||||
attributionSource?: string | undefined;
|
attributionSource?: string | undefined;
|
||||||
@@ -26,15 +17,6 @@ export type TCompleteAccountInviteDTO = {
|
|||||||
password: string;
|
password: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
protectedKey: string;
|
|
||||||
protectedKeyIV: string;
|
|
||||||
protectedKeyTag: string;
|
|
||||||
publicKey: string;
|
|
||||||
encryptedPrivateKey: string;
|
|
||||||
encryptedPrivateKeyIV: string;
|
|
||||||
encryptedPrivateKeyTag: string;
|
|
||||||
salt: string;
|
|
||||||
verifier: string;
|
|
||||||
ip: string;
|
ip: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
authorization: string;
|
authorization: string;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { isValidIp } from "@app/lib/ip";
|
import { isValidIp } from "@app/lib/ip";
|
||||||
import { isFQDN } from "@app/lib/validator/validate-url";
|
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||||
|
import { TAltNameMapping, TAltNameType } from "@app/services/certificate/certificate-types";
|
||||||
|
|
||||||
const isValidDate = (dateString: string) => {
|
const isValidDate = (dateString: string) => {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
@@ -56,3 +57,19 @@ export const validateAltNamesField = z
|
|||||||
message: "Each alt name must be a valid hostname, email address, IP address or URL"
|
message: "Each alt name must be a valid hostname, email address, IP address or URL"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const validateAndMapAltNameType = (name: string): TAltNameMapping | null => {
|
||||||
|
if (isFQDN(name, { allow_wildcard: true, require_tld: false })) {
|
||||||
|
return { type: TAltNameType.DNS, value: name };
|
||||||
|
}
|
||||||
|
if (z.string().url().safeParse(name).success) {
|
||||||
|
return { type: TAltNameType.URL, value: name };
|
||||||
|
}
|
||||||
|
if (z.string().email().safeParse(name).success) {
|
||||||
|
return { type: TAltNameType.EMAIL, value: name };
|
||||||
|
}
|
||||||
|
if (isValidIp(name)) {
|
||||||
|
return { type: TAltNameType.IP, value: name };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/* eslint-disable no-bitwise */
|
/* eslint-disable no-bitwise */
|
||||||
import * as x509 from "@peculiar/x509";
|
import * as x509 from "@peculiar/x509";
|
||||||
import RE2 from "re2";
|
import RE2 from "re2";
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { TCertificateTemplates, TPkiSubscribers } from "@app/db/schemas";
|
import { TCertificateTemplates, TPkiSubscribers } from "@app/db/schemas";
|
||||||
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
@@ -9,7 +8,6 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { isFQDN } from "@app/lib/validator/validate-url";
|
|
||||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||||
@@ -17,7 +15,8 @@ import {
|
|||||||
CertExtendedKeyUsage,
|
CertExtendedKeyUsage,
|
||||||
CertKeyAlgorithm,
|
CertKeyAlgorithm,
|
||||||
CertKeyUsage,
|
CertKeyUsage,
|
||||||
CertStatus
|
CertStatus,
|
||||||
|
TAltNameMapping
|
||||||
} from "@app/services/certificate/certificate-types";
|
} from "@app/services/certificate/certificate-types";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
@@ -34,6 +33,7 @@ import {
|
|||||||
} from "../certificate-authority-fns";
|
} from "../certificate-authority-fns";
|
||||||
import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal";
|
import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal";
|
||||||
import { TIssueCertWithTemplateDTO } from "./internal-certificate-authority-types";
|
import { TIssueCertWithTemplateDTO } from "./internal-certificate-authority-types";
|
||||||
|
import { validateAndMapAltNameType } from "../certificate-authority-validators";
|
||||||
|
|
||||||
type TInternalCertificateAuthorityFnsDeps = {
|
type TInternalCertificateAuthorityFnsDeps = {
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa" | "findById">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa" | "findById">;
|
||||||
@@ -152,27 +152,15 @@ export const InternalCertificateAuthorityFns = ({
|
|||||||
extensions.push(extendedKeyUsagesExtension);
|
extensions.push(extendedKeyUsagesExtension);
|
||||||
}
|
}
|
||||||
|
|
||||||
let altNamesArray: { type: "email" | "dns" | "ip" | "url"; value: string }[] = [];
|
let altNamesArray: TAltNameMapping[] = [];
|
||||||
|
|
||||||
if (subscriber.subjectAlternativeNames?.length) {
|
if (subscriber.subjectAlternativeNames?.length) {
|
||||||
altNamesArray = subscriber.subjectAlternativeNames.map((altName) => {
|
altNamesArray = subscriber.subjectAlternativeNames.map((altName) => {
|
||||||
if (z.string().email().safeParse(altName).success) {
|
const altNameType = validateAndMapAltNameType(altName);
|
||||||
return { type: "email", value: altName };
|
if (!altNameType) {
|
||||||
|
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
|
||||||
}
|
}
|
||||||
|
return altNameType;
|
||||||
if (isFQDN(altName, { allow_wildcard: true, require_tld: false })) {
|
|
||||||
return { type: "dns", value: altName };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (z.string().url().safeParse(altName).success) {
|
|
||||||
return { type: "url", value: altName };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (z.string().ip().safeParse(altName).success) {
|
|
||||||
return { type: "ip", value: altName };
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
||||||
@@ -426,27 +414,15 @@ export const InternalCertificateAuthorityFns = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let altNamesArray: { type: "email" | "dns" | "ip" | "url"; value: string }[] = [];
|
let altNamesArray: TAltNameMapping[] = [];
|
||||||
|
|
||||||
if (altNames) {
|
if (altNames) {
|
||||||
altNamesArray = altNames.split(",").map((altName) => {
|
altNamesArray = altNames.split(",").map((altName) => {
|
||||||
if (z.string().email().safeParse(altName).success) {
|
const altNameType = validateAndMapAltNameType(altName);
|
||||||
return { type: "email", value: altName };
|
if (!altNameType) {
|
||||||
|
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
|
||||||
}
|
}
|
||||||
|
return altNameType;
|
||||||
if (isFQDN(altName, { allow_wildcard: true, require_tld: false })) {
|
|
||||||
return { type: "dns", value: altName };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (z.string().url().safeParse(altName).success) {
|
|
||||||
return { type: "url", value: altName };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (z.string().ip().safeParse(altName).success) {
|
|
||||||
return { type: "ip", value: altName };
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
import * as x509 from "@peculiar/x509";
|
import * as x509 from "@peculiar/x509";
|
||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { ActionProjectType, TableName, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
import { ActionProjectType, TableName, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
@@ -18,7 +17,6 @@ import { crypto } from "@app/lib/crypto/cryptography";
|
|||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { isFQDN } from "@app/lib/validator/validate-url";
|
|
||||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
@@ -34,7 +32,8 @@ import {
|
|||||||
CertExtendedKeyUsageOIDToName,
|
CertExtendedKeyUsageOIDToName,
|
||||||
CertKeyAlgorithm,
|
CertKeyAlgorithm,
|
||||||
CertKeyUsage,
|
CertKeyUsage,
|
||||||
CertStatus
|
CertStatus,
|
||||||
|
TAltNameMapping
|
||||||
} from "../../certificate/certificate-types";
|
} from "../../certificate/certificate-types";
|
||||||
import { TCertificateTemplateDALFactory } from "../../certificate-template/certificate-template-dal";
|
import { TCertificateTemplateDALFactory } from "../../certificate-template/certificate-template-dal";
|
||||||
import { validateCertificateDetailsAgainstTemplate } from "../../certificate-template/certificate-template-fns";
|
import { validateCertificateDetailsAgainstTemplate } from "../../certificate-template/certificate-template-fns";
|
||||||
@@ -69,6 +68,7 @@ import {
|
|||||||
TSignIntermediateDTO,
|
TSignIntermediateDTO,
|
||||||
TUpdateCaDTO
|
TUpdateCaDTO
|
||||||
} from "./internal-certificate-authority-types";
|
} from "./internal-certificate-authority-types";
|
||||||
|
import { validateAndMapAltNameType } from "../certificate-authority-validators";
|
||||||
|
|
||||||
type TInternalCertificateAuthorityServiceFactoryDep = {
|
type TInternalCertificateAuthorityServiceFactoryDep = {
|
||||||
certificateAuthorityDAL: Pick<
|
certificateAuthorityDAL: Pick<
|
||||||
@@ -1364,34 +1364,18 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let altNamesArray: {
|
let altNamesArray: TAltNameMapping[] = [];
|
||||||
type: "email" | "dns";
|
|
||||||
value: string;
|
|
||||||
}[] = [];
|
|
||||||
|
|
||||||
if (altNames) {
|
if (altNames) {
|
||||||
altNamesArray = altNames
|
altNamesArray = altNames
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((name) => name.trim())
|
.map((name) => name.trim())
|
||||||
.map((altName) => {
|
.map((altName): TAltNameMapping => {
|
||||||
// check if the altName is a valid email
|
const altNameType = validateAndMapAltNameType(altName);
|
||||||
if (z.string().email().safeParse(altName).success) {
|
if (!altNameType) {
|
||||||
return {
|
throw new Error(`Invalid altName: ${altName}`);
|
||||||
type: "email",
|
|
||||||
value: altName
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
return altNameType;
|
||||||
// check if the altName is a valid hostname
|
|
||||||
if (isFQDN(altName, { allow_wildcard: true })) {
|
|
||||||
return {
|
|
||||||
type: "dns",
|
|
||||||
value: altName
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
|
|
||||||
throw new Error(`Invalid altName: ${altName}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
||||||
@@ -1766,34 +1750,22 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
let altNamesFromCsr: string = "";
|
let altNamesFromCsr: string = "";
|
||||||
let altNamesArray: {
|
let altNamesArray: TAltNameMapping[] = [];
|
||||||
type: "email" | "dns";
|
|
||||||
value: string;
|
|
||||||
}[] = [];
|
|
||||||
if (altNames) {
|
if (altNames) {
|
||||||
altNamesArray = altNames
|
altNamesArray = altNames
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((name) => name.trim())
|
.map((name) => name.trim())
|
||||||
.map((altName) => {
|
.map((altName): TAltNameMapping => {
|
||||||
// check if the altName is a valid email
|
const altNameType = validateAndMapAltNameType(altName);
|
||||||
if (z.string().email().safeParse(altName).success) {
|
if (!altNameType) {
|
||||||
return {
|
throw new Error(`Invalid altName: ${altName}`);
|
||||||
type: "email",
|
|
||||||
value: altName
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
return altNameType;
|
||||||
// check if the altName is a valid hostname
|
|
||||||
if (isFQDN(altName, { allow_wildcard: true })) {
|
|
||||||
return {
|
|
||||||
type: "dns",
|
|
||||||
value: altName
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
|
|
||||||
throw new Error(`Invalid altName: ${altName}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
||||||
|
extensions.push(altNamesExtension);
|
||||||
} else {
|
} else {
|
||||||
// attempt to read from CSR if altNames is not explicitly provided
|
// attempt to read from CSR if altNames is not explicitly provided
|
||||||
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
|
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
|
||||||
@@ -1801,11 +1773,16 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
const sanNames = new x509.GeneralNames(sanExtension.value);
|
const sanNames = new x509.GeneralNames(sanExtension.value);
|
||||||
|
|
||||||
altNamesArray = sanNames.items
|
altNamesArray = sanNames.items
|
||||||
.filter((value) => value.type === "email" || value.type === "dns")
|
.filter(
|
||||||
.map((name) => ({
|
(value) => value.type === "email" || value.type === "dns" || value.type === "url" || value.type === "ip"
|
||||||
type: name.type as "email" | "dns",
|
)
|
||||||
value: name.value
|
.map((name): TAltNameMapping => {
|
||||||
}));
|
const altNameType = validateAndMapAltNameType(name.value);
|
||||||
|
if (!altNameType) {
|
||||||
|
throw new Error(`Invalid altName from CSR: ${name.value}`);
|
||||||
|
}
|
||||||
|
return altNameType;
|
||||||
|
});
|
||||||
|
|
||||||
altNamesFromCsr = sanNames.items.map((item) => item.value).join(",");
|
altNamesFromCsr = sanNames.items.map((item) => item.value).join(",");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,3 +104,14 @@ export type TGetCertificateCredentialsDTO = {
|
|||||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum TAltNameType {
|
||||||
|
EMAIL = "email",
|
||||||
|
DNS = "dns",
|
||||||
|
IP = "ip",
|
||||||
|
URL = "url"
|
||||||
|
}
|
||||||
|
export type TAltNameMapping = {
|
||||||
|
type: TAltNameType;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas";
|
import { ActionProjectType, ProjectMembershipRole, ProjectVersion, SecretKeyEncoding, TGroups } from "@app/db/schemas";
|
||||||
import { TListProjectGroupUsersDTO } from "@app/ee/services/group/group-types";
|
import { TListProjectGroupUsersDTO } from "@app/ee/services/group/group-types";
|
||||||
import {
|
import {
|
||||||
constructPermissionErrorMessage,
|
constructPermissionErrorMessage,
|
||||||
@@ -188,7 +188,7 @@ export const groupProjectServiceFactory = ({
|
|||||||
// other groups that are in the project
|
// other groups that are in the project
|
||||||
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group!.id, project.id, tx);
|
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group!.id, project.id, tx);
|
||||||
|
|
||||||
if (groupMembers.length) {
|
if (groupMembers.length && (project.version === ProjectVersion.V1 || project.version === ProjectVersion.V2)) {
|
||||||
const ghostUser = await projectDAL.findProjectGhostUser(project.id, tx);
|
const ghostUser = await projectDAL.findProjectGhostUser(project.id, tx);
|
||||||
|
|
||||||
if (!ghostUser) {
|
if (!ghostUser) {
|
||||||
@@ -205,6 +205,12 @@ export const groupProjectServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!ghostUserLatestKey.sender.publicKey) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Failed to find project owner's latest key in project with name ${project.name}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const bot = await projectBotDAL.findOne({ projectId: project.id }, tx);
|
const bot = await projectBotDAL.findOne({ projectId: project.id }, tx);
|
||||||
|
|
||||||
if (!bot) {
|
if (!bot) {
|
||||||
@@ -231,6 +237,12 @@ export const groupProjectServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const projectKeyData = groupMembers.map(({ user: { publicKey, id } }) => {
|
const projectKeyData = groupMembers.map(({ user: { publicKey, id } }) => {
|
||||||
|
if (!publicKey) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Failed to find user's public key in project with name ${project.name}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { ciphertext: encryptedKey, nonce } = crypto
|
const { ciphertext: encryptedKey, nonce } = crypto
|
||||||
.encryption()
|
.encryption()
|
||||||
.asymmetric()
|
.asymmetric()
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
import { ProjectMembershipRole, ProjectVersion, SecretKeyEncoding } from "@app/db/schemas";
|
import { ProjectMembershipRole, ProjectVersion } from "@app/db/schemas";
|
||||||
import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
|
||||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
|
||||||
import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types";
|
import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types";
|
||||||
|
|
||||||
type TOrgAdminServiceFactoryDep = {
|
type TOrgAdminServiceFactoryDep = {
|
||||||
@@ -25,7 +22,6 @@ type TOrgAdminServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "create">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "create">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserId">;
|
|
||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
};
|
};
|
||||||
@@ -38,7 +34,6 @@ export const orgAdminServiceFactory = ({
|
|||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
userDAL,
|
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
smtpService
|
smtpService
|
||||||
}: TOrgAdminServiceFactoryDep) => {
|
}: TOrgAdminServiceFactoryDep) => {
|
||||||
@@ -83,7 +78,7 @@ export const orgAdminServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
projectId
|
projectId
|
||||||
}: TAccessProjectDTO) => {
|
}: TAccessProjectDTO) => {
|
||||||
const { permission, membership } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
@@ -98,8 +93,10 @@ export const orgAdminServiceFactory = ({
|
|||||||
const project = await projectDAL.findOne({ id: projectId, orgId: actorOrgId });
|
const project = await projectDAL.findOne({ id: projectId, orgId: actorOrgId });
|
||||||
if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` });
|
if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` });
|
||||||
|
|
||||||
if (project.version === ProjectVersion.V1) {
|
if (project.version === ProjectVersion.V1 || project.version === ProjectVersion.V2) {
|
||||||
throw new BadRequestError({ message: "Please upgrade your project on your dashboard" });
|
throw new BadRequestError({
|
||||||
|
message: `Project '${project.name}' is a legacy project and must be upgraded before accessing it through the admin console.`
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// check already there exist a membership if there return it
|
// check already there exist a membership if there return it
|
||||||
@@ -144,30 +141,6 @@ export const orgAdminServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const botPrivateKey = crypto
|
|
||||||
.encryption()
|
|
||||||
.symmetric()
|
|
||||||
.decryptWithRootEncryptionKey({
|
|
||||||
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
|
||||||
iv: bot.iv,
|
|
||||||
tag: bot.tag,
|
|
||||||
ciphertext: bot.encryptedPrivateKey
|
|
||||||
});
|
|
||||||
|
|
||||||
const userEncryptionKey = await userDAL.findUserEncKeyByUserId(actorId);
|
|
||||||
if (!userEncryptionKey)
|
|
||||||
throw new NotFoundError({ message: `User encryption key for user with ID '${actorId}' not found` });
|
|
||||||
const [newWsMember] = assignWorkspaceKeysToMembers({
|
|
||||||
decryptKey: ghostUserLatestKey,
|
|
||||||
userPrivateKey: botPrivateKey,
|
|
||||||
members: [
|
|
||||||
{
|
|
||||||
orgMembershipId: membership.id,
|
|
||||||
userPublicKey: userEncryptionKey.publicKey
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedMembership = await projectMembershipDAL.transaction(async (tx) => {
|
const updatedMembership = await projectMembershipDAL.transaction(async (tx) => {
|
||||||
const newProjectMembership = await projectMembershipDAL.create(
|
const newProjectMembership = await projectMembershipDAL.create(
|
||||||
{
|
{
|
||||||
@@ -181,16 +154,6 @@ export const orgAdminServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
await projectKeyDAL.create(
|
|
||||||
{
|
|
||||||
encryptedKey: newWsMember.workspaceEncryptedKey,
|
|
||||||
nonce: newWsMember.workspaceEncryptedNonce,
|
|
||||||
senderId: ghostUser.id,
|
|
||||||
receiverId: actorId,
|
|
||||||
projectId
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
return newProjectMembership;
|
return newProjectMembership;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
OrgMembershipStatus,
|
OrgMembershipStatus,
|
||||||
ProjectMembershipRole,
|
ProjectMembershipRole,
|
||||||
ProjectVersion,
|
ProjectVersion,
|
||||||
SecretKeyEncoding,
|
|
||||||
TableName,
|
TableName,
|
||||||
TProjectMemberships,
|
TProjectMemberships,
|
||||||
TProjectUserMembershipRolesInsert,
|
TProjectUserMembershipRolesInsert,
|
||||||
@@ -58,8 +57,6 @@ import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
|||||||
import { TokenType } from "../auth-token/auth-token-types";
|
import { TokenType } from "../auth-token/auth-token-types";
|
||||||
import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal";
|
import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { assignWorkspaceKeysToMembers, createProjectKey } from "../project/project-fns";
|
|
||||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
|
||||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
@@ -137,7 +134,6 @@ type TOrgServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "updateById">;
|
|
||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "create">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "create">;
|
||||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||||
loginService: Pick<TAuthLoginFactory, "generateUserTokens">;
|
loginService: Pick<TAuthLoginFactory, "generateUserTokens">;
|
||||||
@@ -169,7 +165,6 @@ export const orgServiceFactory = ({
|
|||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
samlConfigDAL,
|
samlConfigDAL,
|
||||||
oidcConfigDAL,
|
oidcConfigDAL,
|
||||||
projectBotDAL,
|
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
identityMetadataDAL,
|
identityMetadataDAL,
|
||||||
projectBotService,
|
projectBotService,
|
||||||
@@ -287,15 +282,7 @@ export const orgServiceFactory = ({
|
|||||||
user.id,
|
user.id,
|
||||||
{
|
{
|
||||||
encryptionVersion: 2,
|
encryptionVersion: 2,
|
||||||
protectedKey: encKeys.protectedKey,
|
publicKey: encKeys.publicKey
|
||||||
protectedKeyIV: encKeys.protectedKeyIV,
|
|
||||||
protectedKeyTag: encKeys.protectedKeyTag,
|
|
||||||
publicKey: encKeys.publicKey,
|
|
||||||
encryptedPrivateKey: encKeys.encryptedPrivateKey,
|
|
||||||
iv: encKeys.encryptedPrivateKeyIV,
|
|
||||||
tag: encKeys.encryptedPrivateKeyTag,
|
|
||||||
salt: encKeys.salt,
|
|
||||||
verifier: encKeys.verifier
|
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -885,29 +872,10 @@ export const orgServiceFactory = ({
|
|||||||
// So what we do is we generate a random secure password and then encrypt it with a random pub-private key
|
// So what we do is we generate a random secure password and then encrypt it with a random pub-private key
|
||||||
// Then when user sign in (as login is not possible as isAccepted is false) we rencrypt the private key with the user password
|
// Then when user sign in (as login is not possible as isAccepted is false) we rencrypt the private key with the user password
|
||||||
if (!inviteeUser || (inviteeUser && !inviteeUser?.isAccepted && !existingEncrytionKey)) {
|
if (!inviteeUser || (inviteeUser && !inviteeUser?.isAccepted && !existingEncrytionKey)) {
|
||||||
const serverGeneratedPassword = crypto.randomBytes(32).toString("hex");
|
|
||||||
const { tag, encoding, ciphertext, iv } = crypto
|
|
||||||
.encryption()
|
|
||||||
.symmetric()
|
|
||||||
.encryptWithRootEncryptionKey(serverGeneratedPassword);
|
|
||||||
const encKeys = await generateUserSrpKeys(inviteeEmail, serverGeneratedPassword);
|
|
||||||
await userDAL.createUserEncryption(
|
await userDAL.createUserEncryption(
|
||||||
{
|
{
|
||||||
userId: inviteeUserId,
|
userId: inviteeUserId,
|
||||||
encryptionVersion: 2,
|
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,
|
|
||||||
serverEncryptedPrivateKeyEncoding: encoding,
|
|
||||||
serverEncryptedPrivateKeyTag: tag,
|
|
||||||
serverEncryptedPrivateKeyIV: iv,
|
|
||||||
serverEncryptedPrivateKey: ciphertext
|
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -1069,106 +1037,6 @@ export const orgServiceFactory = ({
|
|||||||
|
|
||||||
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
|
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
|
||||||
|
|
||||||
// this will auto generate bot
|
|
||||||
const { botKey, bot: autoGeneratedBot } = await projectBotService.getBotKey(projectId, true);
|
|
||||||
|
|
||||||
const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx);
|
|
||||||
let ghostUserId = ghostUser?.id;
|
|
||||||
|
|
||||||
// backfill missing ghost user
|
|
||||||
if (!ghostUserId) {
|
|
||||||
const newGhostUser = await addGhostUser(project.orgId, tx);
|
|
||||||
const projectMembership = await projectMembershipDAL.create(
|
|
||||||
{
|
|
||||||
userId: newGhostUser.user.id,
|
|
||||||
projectId: project.id
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
await projectUserMembershipRoleDAL.create(
|
|
||||||
{ projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin },
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({
|
|
||||||
publicKey: newGhostUser.keys.publicKey,
|
|
||||||
privateKey: newGhostUser.keys.plainPrivateKey,
|
|
||||||
plainProjectKey: botKey
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4. Save the project key for the ghost user.
|
|
||||||
await projectKeyDAL.create(
|
|
||||||
{
|
|
||||||
projectId: project.id,
|
|
||||||
receiverId: newGhostUser.user.id,
|
|
||||||
encryptedKey: encryptedProjectKey,
|
|
||||||
nonce: encryptedProjectKeyIv,
|
|
||||||
senderId: newGhostUser.user.id
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
const { iv, tag, ciphertext, encoding, algorithm } = crypto
|
|
||||||
.encryption()
|
|
||||||
.symmetric()
|
|
||||||
.encryptWithRootEncryptionKey(newGhostUser.keys.plainPrivateKey);
|
|
||||||
if (autoGeneratedBot) {
|
|
||||||
await projectBotDAL.updateById(
|
|
||||||
autoGeneratedBot.id,
|
|
||||||
{
|
|
||||||
tag,
|
|
||||||
iv,
|
|
||||||
encryptedProjectKey,
|
|
||||||
encryptedProjectKeyNonce: encryptedProjectKeyIv,
|
|
||||||
encryptedPrivateKey: ciphertext,
|
|
||||||
isActive: true,
|
|
||||||
publicKey: newGhostUser.keys.publicKey,
|
|
||||||
senderId: newGhostUser.user.id,
|
|
||||||
algorithm,
|
|
||||||
keyEncoding: encoding
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
}
|
|
||||||
ghostUserId = newGhostUser.user.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bot = await projectBotDAL.findOne({ projectId }, tx);
|
|
||||||
if (!bot) {
|
|
||||||
throw new NotFoundError({
|
|
||||||
name: "InviteUser",
|
|
||||||
message: `Failed to find project bot for project with ID '${projectId}'`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUserId, projectId, tx);
|
|
||||||
if (!ghostUserLatestKey) {
|
|
||||||
throw new NotFoundError({
|
|
||||||
name: "InviteUser",
|
|
||||||
message: `Failed to find project owner's latest key for project with ID '${projectId}'`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const botPrivateKey = crypto
|
|
||||||
.encryption()
|
|
||||||
.symmetric()
|
|
||||||
.decryptWithRootEncryptionKey({
|
|
||||||
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
|
||||||
iv: bot.iv,
|
|
||||||
tag: bot.tag,
|
|
||||||
ciphertext: bot.encryptedPrivateKey
|
|
||||||
});
|
|
||||||
|
|
||||||
const newWsMembers = assignWorkspaceKeysToMembers({
|
|
||||||
decryptKey: ghostUserLatestKey,
|
|
||||||
userPrivateKey: botPrivateKey,
|
|
||||||
members: userWithEncryptionKeyInvitedToProject.map((userEnc) => ({
|
|
||||||
orgMembershipId: userEnc.userId,
|
|
||||||
projectMembershipRole: ProjectMembershipRole.Admin,
|
|
||||||
userPublicKey: userEnc.publicKey
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
|
|
||||||
const projectMemberships = await projectMembershipDAL.insertMany(
|
const projectMemberships = await projectMembershipDAL.insertMany(
|
||||||
userWithEncryptionKeyInvitedToProject.map((userEnc) => ({
|
userWithEncryptionKeyInvitedToProject.map((userEnc) => ({
|
||||||
projectId,
|
projectId,
|
||||||
@@ -1191,16 +1059,6 @@ export const orgServiceFactory = ({
|
|||||||
});
|
});
|
||||||
await projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
|
await projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
|
||||||
|
|
||||||
await projectKeyDAL.insertMany(
|
|
||||||
newWsMembers.map((el) => ({
|
|
||||||
encryptedKey: el.workspaceEncryptedKey,
|
|
||||||
nonce: el.workspaceEncryptedNonce,
|
|
||||||
senderId: ghostUserId,
|
|
||||||
receiverId: el.orgMembershipId,
|
|
||||||
projectId
|
|
||||||
})),
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
mailsForProjectInvitation.push({
|
mailsForProjectInvitation.push({
|
||||||
email: userWithEncryptionKeyInvitedToProject
|
email: userWithEncryptionKeyInvitedToProject
|
||||||
.filter((el) => !userIdsWithOrgInvitation.has(el.userId))
|
.filter((el) => !userIdsWithOrgInvitation.has(el.userId))
|
||||||
|
|||||||
@@ -42,6 +42,13 @@ export const getBotKeyFnFactory = (
|
|||||||
message: `Project bot not found for project with ID '${projectId}'. Please ask an administrator to log-in to the Infisical Console.`
|
message: `Project bot not found for project with ID '${projectId}'. Please ask an administrator to log-in to the Infisical Console.`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!projectV1Keys.senderPublicKey) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Project bot not found for project with ID '${projectId}'. Please ask an administrator to log-in to the Infisical Console and upgrade the project.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let userPrivateKey = "";
|
let userPrivateKey = "";
|
||||||
if (
|
if (
|
||||||
projectV1Keys?.serverEncryptedPrivateKey &&
|
projectV1Keys?.serverEncryptedPrivateKey &&
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const projectKeyDALFactory = (db: TDbClient) => {
|
|||||||
userId: string,
|
userId: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
): Promise<(TProjectKeys & { sender: { publicKey: string } }) | undefined> => {
|
): Promise<(TProjectKeys & { sender: { publicKey?: string } }) | undefined> => {
|
||||||
try {
|
try {
|
||||||
const projectKey = await (tx || db.replicaNode())(TableName.ProjectKeys)
|
const projectKey = await (tx || db.replicaNode())(TableName.ProjectKeys)
|
||||||
.join(TableName.Users, `${TableName.ProjectKeys}.senderId`, `${TableName.Users}.id`)
|
.join(TableName.Users, `${TableName.ProjectKeys}.senderId`, `${TableName.Users}.id`)
|
||||||
@@ -25,7 +25,7 @@ export const projectKeyDALFactory = (db: TDbClient) => {
|
|||||||
.select(db.ref("publicKey").withSchema(TableName.UserEncryptionKey))
|
.select(db.ref("publicKey").withSchema(TableName.UserEncryptionKey))
|
||||||
.first();
|
.first();
|
||||||
if (projectKey) {
|
if (projectKey) {
|
||||||
return { ...projectKey, sender: { publicKey: projectKey.publicKey } };
|
return { ...projectKey, sender: { publicKey: projectKey.publicKey || undefined } };
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find latest project key" });
|
throw new DatabaseError({ error, name: "Find latest project key" });
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
|
|||||||
import { AddUserToWsDTO, TBootstrapSshProjectDTO } from "./project-types";
|
import { AddUserToWsDTO, TBootstrapSshProjectDTO } from "./project-types";
|
||||||
|
|
||||||
export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateKey }: AddUserToWsDTO) => {
|
export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateKey }: AddUserToWsDTO) => {
|
||||||
|
if (!decryptKey.sender.publicKey) {
|
||||||
|
throw new Error("Decrypt key sender public key not found");
|
||||||
|
}
|
||||||
|
|
||||||
const plaintextProjectKey = crypto.encryption().asymmetric().decrypt({
|
const plaintextProjectKey = crypto.encryption().asymmetric().decrypt({
|
||||||
ciphertext: decryptKey.encryptedKey,
|
ciphertext: decryptKey.encryptedKey,
|
||||||
nonce: decryptKey.nonce,
|
nonce: decryptKey.nonce,
|
||||||
|
|||||||
@@ -121,6 +121,10 @@ export const projectQueueFactory = ({
|
|||||||
tag: data.encryptedPrivateKey.encryptedKeyTag
|
tag: data.encryptedPrivateKey.encryptedKeyTag
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!oldProjectKey.sender.publicKey) {
|
||||||
|
throw new Error("Old project key sender public key not found");
|
||||||
|
}
|
||||||
|
|
||||||
const decryptedPlainProjectKey = crypto.encryption().asymmetric().decrypt({
|
const decryptedPlainProjectKey = crypto.encryption().asymmetric().decrypt({
|
||||||
ciphertext: oldProjectKey.encryptedKey,
|
ciphertext: oldProjectKey.encryptedKey,
|
||||||
nonce: oldProjectKey.nonce,
|
nonce: oldProjectKey.nonce,
|
||||||
@@ -290,6 +294,10 @@ export const projectQueueFactory = ({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!user.publicKey) {
|
||||||
|
throw new Error(`User with ID ${key.receiverId} has no public key during upgrade.`);
|
||||||
|
}
|
||||||
|
|
||||||
const [newMember] = assignWorkspaceKeysToMembers({
|
const [newMember] = assignWorkspaceKeysToMembers({
|
||||||
decryptKey: ghostUserLatestKey,
|
decryptKey: ghostUserLatestKey,
|
||||||
userPrivateKey: ghostUser.keys.plainPrivateKey,
|
userPrivateKey: ghostUser.keys.plainPrivateKey,
|
||||||
|
|||||||
@@ -55,13 +55,10 @@ import { validateMicrosoftTeamsChannelsSchema } from "../microsoft-teams/microso
|
|||||||
import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal";
|
import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal";
|
||||||
import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal";
|
import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal";
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TOrgServiceFactory } from "../org/org-service";
|
|
||||||
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
|
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
|
||||||
import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal";
|
import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal";
|
||||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
|
||||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||||
@@ -78,7 +75,7 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
|||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
|
import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
|
||||||
import { TProjectDALFactory } from "./project-dal";
|
import { TProjectDALFactory } from "./project-dal";
|
||||||
import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } from "./project-fns";
|
import { bootstrapSshProject } from "./project-fns";
|
||||||
import { TProjectQueueFactory } from "./project-queue";
|
import { TProjectQueueFactory } from "./project-queue";
|
||||||
import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal";
|
import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal";
|
||||||
import {
|
import {
|
||||||
@@ -123,6 +120,7 @@ export const DEFAULT_PROJECT_ENVS = [
|
|||||||
|
|
||||||
type TProjectServiceFactoryDep = {
|
type TProjectServiceFactoryDep = {
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
|
identityProjectDAL: Pick<TIdentityProjectDALFactory, "create">;
|
||||||
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "transaction" | "create" | "findOne" | "updateById">;
|
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "transaction" | "create" | "findOne" | "updateById">;
|
||||||
projectQueue: TProjectQueueFactory;
|
projectQueue: TProjectQueueFactory;
|
||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
@@ -132,9 +130,7 @@ type TProjectServiceFactoryDep = {
|
|||||||
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">;
|
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">;
|
||||||
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||||
identityProjectDAL: TIdentityProjectDALFactory;
|
|
||||||
identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">;
|
identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
|
|
||||||
projectMembershipDAL: Pick<
|
projectMembershipDAL: Pick<
|
||||||
TProjectMembershipDALFactory,
|
TProjectMembershipDALFactory,
|
||||||
"create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers"
|
"create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers"
|
||||||
@@ -167,12 +163,10 @@ type TProjectServiceFactoryDep = {
|
|||||||
sshHostDAL: Pick<TSshHostDALFactory, "find" | "findSshHostsWithLoginMappings">;
|
sshHostDAL: Pick<TSshHostDALFactory, "find" | "findSshHostsWithLoginMappings">;
|
||||||
sshHostGroupDAL: Pick<TSshHostGroupDALFactory, "find" | "findSshHostGroupsWithLoginMappings">;
|
sshHostGroupDAL: Pick<TSshHostGroupDALFactory, "find" | "findSshHostGroupsWithLoginMappings">;
|
||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "invalidateGetPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "invalidateGetPlan">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
||||||
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
|
|
||||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find" | "insertMany" | "delete">;
|
projectRoleDAL: Pick<TProjectRoleDALFactory, "find" | "insertMany" | "delete">;
|
||||||
kmsService: Pick<
|
kmsService: Pick<
|
||||||
TKmsServiceFactory,
|
TKmsServiceFactory,
|
||||||
@@ -196,27 +190,25 @@ export const projectServiceFactory = ({
|
|||||||
secretDAL,
|
secretDAL,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
projectQueue,
|
projectQueue,
|
||||||
projectKeyDAL,
|
|
||||||
permissionService,
|
permissionService,
|
||||||
projectBotService,
|
projectBotService,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
orgService,
|
|
||||||
identityProjectDAL,
|
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
identityProjectMembershipRoleDAL,
|
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
certificateTemplateDAL,
|
certificateTemplateDAL,
|
||||||
pkiCollectionDAL,
|
pkiCollectionDAL,
|
||||||
pkiAlertDAL,
|
pkiAlertDAL,
|
||||||
pkiSubscriberDAL,
|
pkiSubscriberDAL,
|
||||||
|
identityProjectDAL,
|
||||||
|
identityProjectMembershipRoleDAL,
|
||||||
sshCertificateAuthorityDAL,
|
sshCertificateAuthorityDAL,
|
||||||
sshCertificateAuthoritySecretDAL,
|
sshCertificateAuthoritySecretDAL,
|
||||||
sshCertificateDAL,
|
sshCertificateDAL,
|
||||||
@@ -225,7 +217,6 @@ export const projectServiceFactory = ({
|
|||||||
sshHostGroupDAL,
|
sshHostGroupDAL,
|
||||||
keyStore,
|
keyStore,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectBotDAL,
|
|
||||||
projectSlackConfigDAL,
|
projectSlackConfigDAL,
|
||||||
projectMicrosoftTeamsConfigDAL,
|
projectMicrosoftTeamsConfigDAL,
|
||||||
slackIntegrationDAL,
|
slackIntegrationDAL,
|
||||||
@@ -253,7 +244,7 @@ export const projectServiceFactory = ({
|
|||||||
type = ProjectType.SecretManager
|
type = ProjectType.SecretManager
|
||||||
}: TCreateProjectDTO) => {
|
}: TCreateProjectDTO) => {
|
||||||
const organization = await orgDAL.findOne({ id: actorOrgId });
|
const organization = await orgDAL.findOne({ id: actorOrgId });
|
||||||
const { permission, membership: orgMembership } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
organization.id,
|
organization.id,
|
||||||
@@ -277,7 +268,6 @@ export const projectServiceFactory = ({
|
|||||||
message: "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces."
|
message: "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const ghostUser = await orgService.addGhostUser(organization.id, tx);
|
|
||||||
|
|
||||||
if (kmsKeyId) {
|
if (kmsKeyId) {
|
||||||
const kms = await kmsService.getKmsById(kmsKeyId, tx);
|
const kms = await kmsService.getKmsById(kmsKeyId, tx);
|
||||||
@@ -329,19 +319,6 @@ export const projectServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// set ghost user as admin of project
|
|
||||||
const projectMembership = await projectMembershipDAL.create(
|
|
||||||
{
|
|
||||||
userId: ghostUser.user.id,
|
|
||||||
projectId: project.id
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
await projectUserMembershipRoleDAL.create(
|
|
||||||
{ projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin },
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
// set default environments and root folder for provided environments
|
// set default environments and root folder for provided environments
|
||||||
let envs: TProjectEnvironments[] = [];
|
let envs: TProjectEnvironments[] = [];
|
||||||
if (projectTemplate) {
|
if (projectTemplate) {
|
||||||
@@ -374,55 +351,6 @@ export const projectServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Create a random key that we'll use as the project key.
|
|
||||||
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({
|
|
||||||
publicKey: ghostUser.keys.publicKey,
|
|
||||||
privateKey: ghostUser.keys.plainPrivateKey
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4. Save the project key for the ghost user.
|
|
||||||
await projectKeyDAL.create(
|
|
||||||
{
|
|
||||||
projectId: project.id,
|
|
||||||
receiverId: ghostUser.user.id,
|
|
||||||
encryptedKey: encryptedProjectKey,
|
|
||||||
nonce: encryptedProjectKeyIv,
|
|
||||||
senderId: ghostUser.user.id
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
const { iv, tag, ciphertext, encoding, algorithm } = crypto
|
|
||||||
.encryption()
|
|
||||||
.symmetric()
|
|
||||||
.encryptWithRootEncryptionKey(ghostUser.keys.plainPrivateKey);
|
|
||||||
|
|
||||||
// 5. Create & a bot for the project
|
|
||||||
await projectBotDAL.create(
|
|
||||||
{
|
|
||||||
name: "Infisical Bot (Ghost)",
|
|
||||||
projectId: project.id,
|
|
||||||
tag,
|
|
||||||
iv,
|
|
||||||
encryptedProjectKey,
|
|
||||||
encryptedProjectKeyNonce: encryptedProjectKeyIv,
|
|
||||||
encryptedPrivateKey: ciphertext,
|
|
||||||
isActive: true,
|
|
||||||
publicKey: ghostUser.keys.publicKey,
|
|
||||||
senderId: ghostUser.user.id,
|
|
||||||
algorithm,
|
|
||||||
keyEncoding: encoding
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
// Find the ghost users latest key
|
|
||||||
const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);
|
|
||||||
|
|
||||||
if (!latestKey) {
|
|
||||||
throw new Error("Latest key not found for user");
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the project is being created by a user, add the user to the project as an admin
|
// If the project is being created by a user, add the user to the project as an admin
|
||||||
if (actor === ActorType.USER) {
|
if (actor === ActorType.USER) {
|
||||||
// Find public key of user
|
// Find public key of user
|
||||||
@@ -432,17 +360,6 @@ export const projectServiceFactory = ({
|
|||||||
throw new Error("User not found");
|
throw new Error("User not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const [projectAdmin] = assignWorkspaceKeysToMembers({
|
|
||||||
decryptKey: latestKey,
|
|
||||||
userPrivateKey: ghostUser.keys.plainPrivateKey,
|
|
||||||
members: [
|
|
||||||
{
|
|
||||||
userPublicKey: user.publicKey,
|
|
||||||
orgMembershipId: orgMembership.id
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create a membership for the user
|
// Create a membership for the user
|
||||||
const userProjectMembership = await projectMembershipDAL.create(
|
const userProjectMembership = await projectMembershipDAL.create(
|
||||||
{
|
{
|
||||||
@@ -455,18 +372,6 @@ export const projectServiceFactory = ({
|
|||||||
{ projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin },
|
{ projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin },
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create a project key for the user
|
|
||||||
await projectKeyDAL.create(
|
|
||||||
{
|
|
||||||
encryptedKey: projectAdmin.workspaceEncryptedKey,
|
|
||||||
nonce: projectAdmin.workspaceEncryptedNonce,
|
|
||||||
senderId: ghostUser.user.id,
|
|
||||||
receiverId: user.id,
|
|
||||||
projectId: project.id
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the project is being created by an identity, add the identity to the project as an admin
|
// If the project is being created by an identity, add the identity to the project as an admin
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export type TUpgradeProjectDTO = {
|
|||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type AddUserToWsDTO = {
|
export type AddUserToWsDTO = {
|
||||||
decryptKey: TProjectKeys & { sender: { publicKey: string } };
|
decryptKey: TProjectKeys & { sender: { publicKey?: string } };
|
||||||
userPrivateKey: string;
|
userPrivateKey: string;
|
||||||
members: {
|
members: {
|
||||||
orgMembershipId: string;
|
orgMembershipId: string;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const reminderServiceFactory = ({
|
|||||||
const $manageReminderRecipients = async (reminderId: string, newRecipients?: string[] | null): Promise<void> => {
|
const $manageReminderRecipients = async (reminderId: string, newRecipients?: string[] | null): Promise<void> => {
|
||||||
if (!newRecipients || newRecipients.length === 0) {
|
if (!newRecipients || newRecipients.length === 0) {
|
||||||
// If no recipients provided, remove all existing recipients
|
// If no recipients provided, remove all existing recipients
|
||||||
await reminderRecipientDAL.deleteById(reminderId);
|
await reminderRecipientDAL.delete({ reminderId });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
@@ -175,7 +173,11 @@ export const userServiceFactory = ({
|
|||||||
const getMe = async (userId: string) => {
|
const getMe = async (userId: string) => {
|
||||||
const user = await userDAL.findUserEncKeyByUserId(userId);
|
const user = await userDAL.findUserEncKeyByUserId(userId);
|
||||||
if (!user) throw new NotFoundError({ message: `User with ID '${userId}' not found`, name: "GetMe" });
|
if (!user) throw new NotFoundError({ message: `User with ID '${userId}' not found`, name: "GetMe" });
|
||||||
return user;
|
|
||||||
|
return {
|
||||||
|
...user,
|
||||||
|
encryptionVersion: user.encryptionVersion!
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteUser = async (userId: string) => {
|
const deleteUser = async (userId: string) => {
|
||||||
@@ -212,25 +214,6 @@ export const userServiceFactory = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserPrivateKey = async (userId: string) => {
|
|
||||||
const user = await userDAL.findUserEncKeyByUserId(userId);
|
|
||||||
if (!user?.serverEncryptedPrivateKey || !user.serverEncryptedPrivateKeyIV || !user.serverEncryptedPrivateKeyTag) {
|
|
||||||
throw new NotFoundError({ message: `Private key for user with ID '${userId}' not found` });
|
|
||||||
}
|
|
||||||
|
|
||||||
const privateKey = crypto
|
|
||||||
.encryption()
|
|
||||||
.symmetric()
|
|
||||||
.decryptWithRootEncryptionKey({
|
|
||||||
ciphertext: user.serverEncryptedPrivateKey,
|
|
||||||
tag: user.serverEncryptedPrivateKeyTag,
|
|
||||||
iv: user.serverEncryptedPrivateKeyIV,
|
|
||||||
keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
|
|
||||||
});
|
|
||||||
|
|
||||||
return privateKey;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUserProjectFavorites = async (userId: string, orgId: string) => {
|
const getUserProjectFavorites = async (userId: string, orgId: string) => {
|
||||||
const orgMembership = await orgMembershipDAL.findOne({
|
const orgMembership = await orgMembershipDAL.findOne({
|
||||||
userId,
|
userId,
|
||||||
@@ -311,7 +294,6 @@ export const userServiceFactory = ({
|
|||||||
listUserGroups,
|
listUserGroups,
|
||||||
getUserAction,
|
getUserAction,
|
||||||
unlockUser,
|
unlockUser,
|
||||||
getUserPrivateKey,
|
|
||||||
getAllMyAccounts,
|
getAllMyAccounts,
|
||||||
getUserProjectFavorites,
|
getUserProjectFavorites,
|
||||||
removeMyDuplicateAccounts,
|
removeMyDuplicateAccounts,
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Project Events"
|
||||||
|
openapi: "POST /api/v1/events/subscribe/project-events"
|
||||||
|
---
|
||||||
@@ -41,6 +41,8 @@
|
|||||||
"group": "Platform Reference",
|
"group": "Platform Reference",
|
||||||
"pages": [
|
"pages": [
|
||||||
"documentation/platform/organization",
|
"documentation/platform/organization",
|
||||||
|
"documentation/platform/event-subscriptions",
|
||||||
|
"documentation/platform/folder",
|
||||||
{
|
{
|
||||||
"group": "Projects",
|
"group": "Projects",
|
||||||
"pages": [
|
"pages": [
|
||||||
@@ -777,6 +779,10 @@
|
|||||||
"group": "Admin",
|
"group": "Admin",
|
||||||
"pages": ["api-reference/endpoints/admin/bootstrap-instance"]
|
"pages": ["api-reference/endpoints/admin/bootstrap-instance"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"group": "Events",
|
||||||
|
"pages": ["api-reference/endpoints/events/project-events"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"group": "Identities",
|
"group": "Identities",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
---
|
||||||
|
title: "Event Subscriptions"
|
||||||
|
sidebarTitle: "Events"
|
||||||
|
description: "Subscribe to events in Infisical for real-time updates"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
**Note:** Event Subscriptions is a paid feature. - **Infisical Cloud users:** Event Subscriptions is available under
|
||||||
|
the **Enterprise Tier**. - **Self-Hosted Infisical:** Please contact [[email protected]](mailto:[email protected])
|
||||||
|
to purchase an enterprise license.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
Event Subscriptions in Infisical allow you to receive real-time notifications when specific actions occur within your account or organization. These notifications include changes to secrets, users, teams, and many more **coming soon**.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
- Server receives message over pubsub connection indicating changes have occurred
|
||||||
|
- Server processes the change notification
|
||||||
|
- Updated data is synchronized across all connected Infisical instances
|
||||||
|
- Client applications receive real-time updates through [Server-Sent Events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
|
||||||
|
- All servers maintain consistent state without manual intervention
|
||||||
|
|
||||||
|
This ensures your infrastructure stays up-to-date automatically, without requiring restarts or manual synchronization.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Event Subscriptions are designed for real-time communication and do not include persistence or replay
|
||||||
|
capabilities—events are delivered once and are not stored for future retrieval.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
## Supported Resources
|
||||||
|
|
||||||
|
You can currently subscribe to notifications for the following resources and event types:
|
||||||
|
|
||||||
|
- **Secrets**
|
||||||
|
- `secret:created`: Triggered when a secret is created
|
||||||
|
- `secret:updated`: Triggered when a secret is updated
|
||||||
|
- `secret:deleted`: Triggered when a secret is deleted
|
||||||
|
|
||||||
|
## Permissions Setup
|
||||||
|
|
||||||
|
To receive events on a supported resource, the identity must have `Subscribe` action permission on that resource.
|
||||||
|
|
||||||
|
Follow these steps to set up the necessary permissions:
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Select a project and copy the Project ID">
|
||||||
|

|
||||||
|
|
||||||
|
On your project page, open **Project Settings** from the sidebar.
|
||||||
|
|
||||||
|
In the Project name section, click **Copy Project ID** to copy your Project ID, or extract it from the URL:
|
||||||
|
`https://app.infisical.com/project/<your_project_id>/settings`
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Navigate to Access Management and open Project Roles">
|
||||||
|
  Navigate to **Access Management**, then select **Project Roles**.
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Select an existing role or create a new one">
|
||||||
|
 You can either edit an existing role or create a new role
|
||||||
|
for event subscriptions.
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Assign policies to the role">
|
||||||
|
 Select the specific resources that the role should have access
|
||||||
|
to. 
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Enable the Subscribe action in permissions">
|
||||||
|

|
||||||
|
|
||||||
|
Ensure the **Subscribe** action is selected for the relevant resources and events.
|
||||||
|
|
||||||
|
## Conditions
|
||||||
|
|
||||||
|
By default, the role will have access to all events for the selected resources in this project.
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="Full Access">
|
||||||
|

|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="Path Prefix">
|
||||||
|

|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="Environment">
|
||||||
|

|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Currently, events are only available via [API](/api-reference/endpoints/events) but will soon be available in our SDKs, Kubernetes Operator, and more.
|
||||||
|
|
||||||
|
### API Usage
|
||||||
|
|
||||||
|
You need an auth token to use this API. To get an authentication token, follow the authentication guide for one of our supported auth methods from the [machine identities documentation](/documentation/platform/identities/machine-identities#authentication-methods).
|
||||||
|
|
||||||
|
#### Creating a Subscription
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Request Parameters:**
|
||||||
|
|
||||||
|
- `projectId`: Project whose events you want to subscribe to
|
||||||
|
- `register`: List of event filters
|
||||||
|
- `conditions`: Conditions to filter events on
|
||||||
|
- `environmentSlug`: Project environment
|
||||||
|
- `secretPath`: Path of the secrets
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The subscribe endpoint responds with a `text/event-stream` content type to initiate SSE streaming.
|
||||||
|
|
||||||
|
For more specific details, please refer to our [API Reference](/api-reference/endpoints/events).
|
||||||
@@ -6,6 +6,7 @@ description: "Learn how to automatically rotate Azure Client Secrets."
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Create an [Azure Client Secret Connection](/integrations/app-connections/azure-client-secrets).
|
- Create an [Azure Client Secret Connection](/integrations/app-connections/azure-client-secrets).
|
||||||
|
- Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply.
|
||||||
|
|
||||||
## Create an Azure Client Secret Rotation in Infisical
|
## Create an Azure Client Secret Rotation in Infisical
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ description: "Learn how to automatically rotate LDAP passwords."
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Create an [LDAP Connection](/integrations/app-connections/ldap) with the **Secret Rotation** requirements
|
- Create an [LDAP Connection](/integrations/app-connections/ldap) with the **Secret Rotation** requirements
|
||||||
|
- Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply.
|
||||||
|
|
||||||
## Create an LDAP Password Rotation in Infisical
|
## Create an LDAP Password Rotation in Infisical
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ An example creation statement might look like:
|
|||||||
To learn more about Microsoft SQL Server's permission system, please visit their [documentation](https://learn.microsoft.com/en-us/sql/t-sql/statements/grant-transact-sql?view=sql-server-ver16).
|
To learn more about Microsoft SQL Server's permission system, please visit their [documentation](https://learn.microsoft.com/en-us/sql/t-sql/statements/grant-transact-sql?view=sql-server-ver16).
|
||||||
</Tip>
|
</Tip>
|
||||||
|
|
||||||
|
3. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply.
|
||||||
|
|
||||||
## Create a Microsoft SQL Server Credentials Rotation in Infisical
|
## Create a Microsoft SQL Server Credentials Rotation in Infisical
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ description: "Learn how to automatically rotate MySQL credentials."
|
|||||||
<Tip>
|
<Tip>
|
||||||
To learn more about the MySQL permission system, please visit their [documentation](https://dev.mysql.com/doc/refman/8.4/en/grant.html).
|
To learn more about the MySQL permission system, please visit their [documentation](https://dev.mysql.com/doc/refman/8.4/en/grant.html).
|
||||||
</Tip>
|
</Tip>
|
||||||
|
3. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply.
|
||||||
|
|
||||||
## Create a MySQL Credentials Rotation in Infisical
|
## Create a MySQL Credentials Rotation in Infisical
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ description: "Learn how to automatically rotate Oracle Database credentials."
|
|||||||
To learn more about the Oracle Database permission system, please visit their [documentation](https://docs.oracle.com/en/database/oracle/oracle-database/19/dbseg/configuring-privilege-and-role-authorization.html).
|
To learn more about the Oracle Database permission system, please visit their [documentation](https://docs.oracle.com/en/database/oracle/oracle-database/19/dbseg/configuring-privilege-and-role-authorization.html).
|
||||||
</Tip>
|
</Tip>
|
||||||
|
|
||||||
|
3. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply.
|
||||||
|
|
||||||
## Create an Oracle Database Credentials Rotation in Infisical
|
## Create an Oracle Database Credentials Rotation in Infisical
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ description: "Learn how to automatically rotate PostgreSQL credentials."
|
|||||||
To learn more about PostgreSQL's permission system, please visit their [documentation](https://www.postgresql.org/docs/current/sql-grant.html).
|
To learn more about PostgreSQL's permission system, please visit their [documentation](https://www.postgresql.org/docs/current/sql-grant.html).
|
||||||
</Tip>
|
</Tip>
|
||||||
|
|
||||||
|
3. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply.
|
||||||
|
|
||||||
## Create a PostgreSQL Credentials Rotation in Infisical
|
## Create a PostgreSQL Credentials Rotation in Infisical
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 476 KiB |
|
After Width: | Height: | Size: 747 KiB |
|
After Width: | Height: | Size: 910 KiB |
|
After Width: | Height: | Size: 531 KiB |
|
After Width: | Height: | Size: 401 KiB |
|
After Width: | Height: | Size: 484 KiB |
|
After Width: | Height: | Size: 397 KiB |
|
After Width: | Height: | Size: 734 KiB |
|
After Width: | Height: | Size: 441 KiB |
|
After Width: | Height: | Size: 735 KiB |
|
After Width: | Height: | Size: 697 KiB |
|
After Width: | Height: | Size: 704 KiB |
|
After Width: | Height: | Size: 879 KiB |
|
After Width: | Height: | Size: 273 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 858 KiB |
|
After Width: | Height: | Size: 866 KiB |
|
After Width: | Height: | Size: 895 KiB |
|
After Width: | Height: | Size: 834 KiB |
|
After Width: | Height: | Size: 678 KiB |
@@ -7,6 +7,7 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
|
|||||||
|
|
||||||
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
||||||
- Create an [AWS Connection](/integrations/app-connections/aws) with the required **Secret Sync** permissions
|
- Create an [AWS Connection](/integrations/app-connections/aws) with the required **Secret Sync** permissions
|
||||||
|
- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply.
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Infisical UI">
|
<Tab title="Infisical UI">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ description: "Learn how to configure an AWS Secrets Manager Sync for Infisical."
|
|||||||
|
|
||||||
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
||||||
- Create an [AWS Connection](/integrations/app-connections/aws) with the required **Secret Sync** permissions
|
- Create an [AWS Connection](/integrations/app-connections/aws) with the required **Secret Sync** permissions
|
||||||
|
- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply.
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Infisical UI">
|
<Tab title="Infisical UI">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ description: "Learn how to configure an Azure App Configuration Sync for Infisic
|
|||||||
|
|
||||||
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
||||||
- Create an [Azure App Configuration Connection](/integrations/app-connections/azure-app-configuration)
|
- Create an [Azure App Configuration Connection](/integrations/app-connections/azure-app-configuration)
|
||||||
|
- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply.
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
The Azure App Configuration Secret Sync requires the following permissions to be set on the user / service principal
|
The Azure App Configuration Secret Sync requires the following permissions to be set on the user / service principal
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ description: "Learn how to configure a Azure DevOps Sync for Infisical."
|
|||||||
|
|
||||||
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
||||||
- Create an [Azure DevOps Connection](/integrations/app-connections/azure-devops)
|
- Create an [Azure DevOps Connection](/integrations/app-connections/azure-devops)
|
||||||
|
- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply.
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Infisical UI">
|
<Tab title="Infisical UI">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ description: "Learn how to configure a Azure Key Vault Sync for Infisical."
|
|||||||
|
|
||||||
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
||||||
- Create an [Azure Key Vault Connection](/integrations/app-connections/azure-key-vault)
|
- Create an [Azure Key Vault Connection](/integrations/app-connections/azure-key-vault)
|
||||||
|
- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply.
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
The Azure Key Vault Secret Sync requires the following secrets permissions to be set on the user / service principal
|
The Azure Key Vault Secret Sync requires the following secrets permissions to be set on the user / service principal
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical."
|
|||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply.
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Infisical UI">
|
<Tab title="Infisical UI">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ description: "Learn how to configure a GitHub Sync for Infisical."
|
|||||||
|
|
||||||
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
||||||
- Create a [GitHub Connection](/integrations/app-connections/github)
|
- Create a [GitHub Connection](/integrations/app-connections/github)
|
||||||
|
- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply.
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Infisical UI">
|
<Tab title="Infisical UI">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ description: "Learn how to configure a GitLab Sync for Infisical."
|
|||||||
|
|
||||||
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
||||||
- Create a [GitLab Connection](/integrations/app-connections/gitlab)
|
- Create a [GitLab Connection](/integrations/app-connections/gitlab)
|
||||||
|
- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply.
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Infisical UI">
|
<Tab title="Infisical UI">
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ description: "Learn how to configure an Oracle Cloud Infrastructure Vault Sync f
|
|||||||
- Create an [OCI Connection](/integrations/app-connections/oci) with the required **Secret Sync** permissions
|
- Create an [OCI Connection](/integrations/app-connections/oci) with the required **Secret Sync** permissions
|
||||||
- [Create](https://docs.oracle.com/en-us/iaas/Content/Identity/compartments/To_create_a_compartment.htm) or use an existing OCI Compartment (which the OCI Connection is authorized to access)
|
- [Create](https://docs.oracle.com/en-us/iaas/Content/Identity/compartments/To_create_a_compartment.htm) or use an existing OCI Compartment (which the OCI Connection is authorized to access)
|
||||||
- [Create](https://docs.oracle.com/en-us/iaas/Content/KeyManagement/Tasks/managingvaults_topic-To_create_a_new_vault.htm#createnewvault) or use an existing OCI Vault
|
- [Create](https://docs.oracle.com/en-us/iaas/Content/KeyManagement/Tasks/managingvaults_topic-To_create_a_new_vault.htm#createnewvault) or use an existing OCI Vault
|
||||||
|
- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply.
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Infisical UI">
|
<Tab title="Infisical UI">
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import crypto from "crypto";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import jsrp from "jsrp";
|
|
||||||
|
|
||||||
import { useServerConfig } from "@app/context";
|
|
||||||
import { initProjectHelper } from "@app/helpers/project";
|
import { initProjectHelper } from "@app/helpers/project";
|
||||||
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
|
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
|
||||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||||
@@ -14,15 +10,9 @@ import { onRequestError } from "@app/hooks/api/reactQuery";
|
|||||||
|
|
||||||
import InputField from "../basic/InputField";
|
import InputField from "../basic/InputField";
|
||||||
import checkPassword from "../utilities/checks/password/checkPassword";
|
import checkPassword from "../utilities/checks/password/checkPassword";
|
||||||
import Aes256Gcm from "../utilities/cryptography/aes-256-gcm";
|
|
||||||
import { deriveArgonKey, generateKeyPair } from "../utilities/cryptography/crypto";
|
|
||||||
import { saveTokenToLocalStorage } from "../utilities/saveTokenToLocalStorage";
|
|
||||||
import SecurityClient from "../utilities/SecurityClient";
|
import SecurityClient from "../utilities/SecurityClient";
|
||||||
import { Button, Input } from "../v2";
|
import { Button, Input } from "../v2";
|
||||||
|
|
||||||
// eslint-disable-next-line new-cap
|
|
||||||
const client = new jsrp.client();
|
|
||||||
|
|
||||||
interface UserInfoStepProps {
|
interface UserInfoStepProps {
|
||||||
incrementStep: () => void;
|
incrementStep: () => void;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -76,7 +66,6 @@ export default function UserInfoStep({
|
|||||||
}: UserInfoStepProps): JSX.Element {
|
}: UserInfoStepProps): JSX.Element {
|
||||||
const [nameError, setNameError] = useState(false);
|
const [nameError, setNameError] = useState(false);
|
||||||
const [organizationNameError, setOrganizationNameError] = useState(false);
|
const [organizationNameError, setOrganizationNameError] = useState(false);
|
||||||
const { config } = useServerConfig();
|
|
||||||
|
|
||||||
const [errors, setErrors] = useState<Errors>({});
|
const [errors, setErrors] = useState<Errors>({});
|
||||||
|
|
||||||
@@ -108,109 +97,41 @@ export default function UserInfoStep({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!errorCheck) {
|
if (!errorCheck) {
|
||||||
// Generate a random pair of a public and a private key
|
try {
|
||||||
const pair = await generateKeyPair(config.fipsEnabled);
|
const response = await completeAccountSignup({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
firstName: name.split(" ")[0],
|
||||||
|
lastName: name.split(" ").slice(1).join(" "),
|
||||||
|
providerAuthToken,
|
||||||
|
organizationName,
|
||||||
|
attributionSource
|
||||||
|
});
|
||||||
|
|
||||||
localStorage.setItem("PRIVATE_KEY", pair.privateKey);
|
// unset signup JWT token and set JWT token
|
||||||
|
SecurityClient.setSignupToken("");
|
||||||
|
SecurityClient.setToken(response.token);
|
||||||
|
SecurityClient.setProviderAuthToken("");
|
||||||
|
|
||||||
client.init(
|
if (response.organizationId) {
|
||||||
{
|
await selectOrganization({ organizationId: response.organizationId });
|
||||||
username: email,
|
|
||||||
password
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => {
|
|
||||||
try {
|
|
||||||
// TODO: moduralize into KeyService
|
|
||||||
const derivedKey = await deriveArgonKey({
|
|
||||||
password,
|
|
||||||
salt: result.salt,
|
|
||||||
mem: 65536,
|
|
||||||
time: 3,
|
|
||||||
parallelism: 1,
|
|
||||||
hashLen: 32
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!derivedKey) throw new Error("Failed to derive key from password");
|
|
||||||
|
|
||||||
const key = crypto.randomBytes(32);
|
|
||||||
|
|
||||||
// create encrypted private key by encrypting the private
|
|
||||||
// key with the symmetric key [key]
|
|
||||||
const {
|
|
||||||
ciphertext: encryptedPrivateKey,
|
|
||||||
iv: encryptedPrivateKeyIV,
|
|
||||||
tag: encryptedPrivateKeyTag
|
|
||||||
} = Aes256Gcm.encrypt({
|
|
||||||
text: pair.privateKey,
|
|
||||||
secret: key
|
|
||||||
});
|
|
||||||
|
|
||||||
// create the protected key by encrypting the symmetric key
|
|
||||||
// [key] with the derived key
|
|
||||||
const {
|
|
||||||
ciphertext: protectedKey,
|
|
||||||
iv: protectedKeyIV,
|
|
||||||
tag: protectedKeyTag
|
|
||||||
} = Aes256Gcm.encrypt({
|
|
||||||
text: key.toString("hex"),
|
|
||||||
secret: Buffer.from(derivedKey.hash)
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await completeAccountSignup({
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
firstName: name.split(" ")[0],
|
|
||||||
lastName: name.split(" ").slice(1).join(" "),
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
publicKey: pair.publicKey,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
encryptedPrivateKeyIV,
|
|
||||||
encryptedPrivateKeyTag,
|
|
||||||
providerAuthToken,
|
|
||||||
salt: result.salt,
|
|
||||||
verifier: result.verifier,
|
|
||||||
organizationName,
|
|
||||||
attributionSource
|
|
||||||
});
|
|
||||||
|
|
||||||
// unset signup JWT token and set JWT token
|
|
||||||
SecurityClient.setSignupToken("");
|
|
||||||
SecurityClient.setToken(response.token);
|
|
||||||
SecurityClient.setProviderAuthToken("");
|
|
||||||
|
|
||||||
if (response.organizationId) {
|
|
||||||
await selectOrganization({ organizationId: response.organizationId });
|
|
||||||
}
|
|
||||||
|
|
||||||
saveTokenToLocalStorage({
|
|
||||||
publicKey: pair.publicKey,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv: encryptedPrivateKeyIV,
|
|
||||||
tag: encryptedPrivateKeyTag,
|
|
||||||
privateKey: pair.privateKey
|
|
||||||
});
|
|
||||||
|
|
||||||
const userOrgs = await fetchOrganizations();
|
|
||||||
|
|
||||||
const orgId = userOrgs[0]?.id;
|
|
||||||
await initProjectHelper({
|
|
||||||
projectName: "Example Project"
|
|
||||||
});
|
|
||||||
|
|
||||||
localStorage.setItem("orgData.id", orgId);
|
|
||||||
|
|
||||||
incrementStep();
|
|
||||||
} catch (error) {
|
|
||||||
onRequestError(error);
|
|
||||||
setIsLoading(false);
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
const userOrgs = await fetchOrganizations();
|
||||||
|
|
||||||
|
const orgId = userOrgs[0]?.id;
|
||||||
|
await initProjectHelper({
|
||||||
|
projectName: "Example Project"
|
||||||
|
});
|
||||||
|
|
||||||
|
localStorage.setItem("orgData.id", orgId);
|
||||||
|
|
||||||
|
incrementStep();
|
||||||
|
} catch (error) {
|
||||||
|
onRequestError(error);
|
||||||
|
setIsLoading(false);
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
/* eslint-disable new-cap */
|
|
||||||
import crypto from "crypto";
|
|
||||||
|
|
||||||
import jsrp from "jsrp";
|
|
||||||
|
|
||||||
import { changePassword, srp1 } from "@app/hooks/api/auth/queries";
|
|
||||||
|
|
||||||
import Aes256Gcm from "./cryptography/aes-256-gcm";
|
|
||||||
import { deriveArgonKey } from "./cryptography/crypto";
|
|
||||||
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
|
|
||||||
|
|
||||||
const clientOldPassword = new jsrp.client();
|
|
||||||
const clientNewPassword = new jsrp.client();
|
|
||||||
|
|
||||||
type Params = {
|
|
||||||
email: string;
|
|
||||||
currentPassword: string;
|
|
||||||
newPassword: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const attemptChangePassword = ({ email, currentPassword, newPassword }: Params): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
clientOldPassword.init({ username: email, password: currentPassword }, async () => {
|
|
||||||
let serverPublicKey;
|
|
||||||
let salt;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const clientPublicKey = clientOldPassword.getPublicKey();
|
|
||||||
|
|
||||||
const res = await srp1({ clientPublicKey });
|
|
||||||
|
|
||||||
serverPublicKey = res.serverPublicKey;
|
|
||||||
salt = res.salt;
|
|
||||||
|
|
||||||
clientOldPassword.setSalt(salt);
|
|
||||||
clientOldPassword.setServerPublicKey(serverPublicKey);
|
|
||||||
|
|
||||||
const clientProof = clientOldPassword.getProof();
|
|
||||||
|
|
||||||
clientNewPassword.init({ username: email, password: newPassword }, async () => {
|
|
||||||
clientNewPassword.createVerifier(async (_err, result) => {
|
|
||||||
try {
|
|
||||||
const derivedKey = await deriveArgonKey({
|
|
||||||
password: newPassword,
|
|
||||||
salt: result.salt,
|
|
||||||
mem: 65536,
|
|
||||||
time: 3,
|
|
||||||
parallelism: 1,
|
|
||||||
hashLen: 32
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!derivedKey) throw new Error("Failed to derive key from password");
|
|
||||||
|
|
||||||
const key = crypto.randomBytes(32);
|
|
||||||
|
|
||||||
const {
|
|
||||||
ciphertext: encryptedPrivateKey,
|
|
||||||
iv: encryptedPrivateKeyIV,
|
|
||||||
tag: encryptedPrivateKeyTag
|
|
||||||
} = Aes256Gcm.encrypt({
|
|
||||||
text: localStorage.getItem("PRIVATE_KEY") as string,
|
|
||||||
secret: key
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
ciphertext: protectedKey,
|
|
||||||
iv: protectedKeyIV,
|
|
||||||
tag: protectedKeyTag
|
|
||||||
} = Aes256Gcm.encrypt({
|
|
||||||
text: key.toString("hex"),
|
|
||||||
secret: Buffer.from(derivedKey.hash)
|
|
||||||
});
|
|
||||||
|
|
||||||
await changePassword({
|
|
||||||
password: newPassword,
|
|
||||||
clientProof,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
encryptedPrivateKeyIV,
|
|
||||||
encryptedPrivateKeyTag,
|
|
||||||
salt: result.salt,
|
|
||||||
verifier: result.verifier
|
|
||||||
});
|
|
||||||
|
|
||||||
saveTokenToLocalStorage({
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv: encryptedPrivateKeyIV,
|
|
||||||
tag: encryptedPrivateKeyTag
|
|
||||||
});
|
|
||||||
|
|
||||||
resolve();
|
|
||||||
} catch (err2) {
|
|
||||||
console.error(err2);
|
|
||||||
reject(err2);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default attemptChangePassword;
|
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
/* eslint-disable prefer-destructuring */
|
/* eslint-disable prefer-destructuring */
|
||||||
|
import axios from "axios";
|
||||||
import jsrp from "jsrp";
|
import jsrp from "jsrp";
|
||||||
|
|
||||||
import { decryptPrivateKeyHelper } from "@app/helpers/key";
|
import { login1, login2, loginV3 } from "@app/hooks/api/auth/queries";
|
||||||
import { login1, login2 } from "@app/hooks/api/auth/queries";
|
|
||||||
|
|
||||||
|
import { createNotification } from "../notifications";
|
||||||
import Telemetry from "./telemetry/Telemetry";
|
import Telemetry from "./telemetry/Telemetry";
|
||||||
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
|
import { LoginMode } from "./attemptLogin";
|
||||||
import SecurityClient from "./SecurityClient";
|
import SecurityClient from "./SecurityClient";
|
||||||
|
|
||||||
// eslint-disable-next-line new-cap
|
// eslint-disable-next-line new-cap
|
||||||
const client = new jsrp.client();
|
const client = new jsrp.client();
|
||||||
|
|
||||||
export interface IsCliLoginSuccessful {
|
export interface IsCliLoginSuccessful {
|
||||||
mfaEnabled: boolean;
|
|
||||||
loginResponse?: {
|
loginResponse?: {
|
||||||
email: string;
|
email: string;
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
@@ -31,14 +31,62 @@ const attemptLogin = async ({
|
|||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
providerAuthToken,
|
providerAuthToken,
|
||||||
captchaToken
|
captchaToken,
|
||||||
|
loginMode = LoginMode.ServerSide
|
||||||
}: {
|
}: {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
providerAuthToken?: string;
|
providerAuthToken?: string;
|
||||||
captchaToken?: string;
|
captchaToken?: string;
|
||||||
|
loginMode?: LoginMode;
|
||||||
}): Promise<IsCliLoginSuccessful> => {
|
}): Promise<IsCliLoginSuccessful> => {
|
||||||
const telemetry = new Telemetry().getInstance();
|
const telemetry = new Telemetry().getInstance();
|
||||||
|
|
||||||
|
if (loginMode === LoginMode.ServerSide) {
|
||||||
|
console.log("Attempting login with server side...");
|
||||||
|
const data = await loginV3({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
providerAuthToken,
|
||||||
|
captchaToken
|
||||||
|
}).catch((err) => {
|
||||||
|
if (axios.isAxiosError(err) && err.response?.status === 400) {
|
||||||
|
if (err.response?.data?.error === "LegacyEncryptionScheme") {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to login without SRP, attempting to authenticate with legacy SRP authentication.",
|
||||||
|
type: "info"
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data === null) {
|
||||||
|
return attemptLogin({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
providerAuthToken,
|
||||||
|
captchaToken,
|
||||||
|
loginMode: LoginMode.LegacySrp
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SecurityClient.setProviderAuthToken("");
|
||||||
|
SecurityClient.setToken(data.accessToken);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
loginResponse: {
|
||||||
|
email,
|
||||||
|
privateKey: "",
|
||||||
|
JTWToken: data.accessToken
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
client.init(
|
client.init(
|
||||||
{
|
{
|
||||||
@@ -58,79 +106,26 @@ const attemptLogin = async ({
|
|||||||
client.setServerPublicKey(serverPublicKey);
|
client.setServerPublicKey(serverPublicKey);
|
||||||
const clientProof = client.getProof(); // called M1
|
const clientProof = client.getProof(); // called M1
|
||||||
|
|
||||||
const {
|
const { encryptionVersion, token, encryptedPrivateKey, iv, tag } = await login2({
|
||||||
mfaEnabled,
|
|
||||||
encryptionVersion,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
token,
|
|
||||||
publicKey,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv,
|
|
||||||
tag
|
|
||||||
} = await login2({
|
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
clientProof,
|
clientProof,
|
||||||
providerAuthToken,
|
providerAuthToken,
|
||||||
captchaToken
|
captchaToken
|
||||||
});
|
});
|
||||||
if (mfaEnabled) {
|
if (encryptionVersion && encryptedPrivateKey && iv && tag && token) {
|
||||||
// case: MFA is enabled
|
|
||||||
|
|
||||||
// set temporary (MFA) JWT token
|
|
||||||
SecurityClient.setMfaToken(token);
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
mfaEnabled,
|
|
||||||
success: true
|
|
||||||
});
|
|
||||||
} else if (
|
|
||||||
!mfaEnabled &&
|
|
||||||
encryptionVersion &&
|
|
||||||
encryptedPrivateKey &&
|
|
||||||
iv &&
|
|
||||||
tag &&
|
|
||||||
token
|
|
||||||
) {
|
|
||||||
// case: MFA is not enabled
|
|
||||||
|
|
||||||
// unset provider auth token in case it was used
|
|
||||||
SecurityClient.setProviderAuthToken("");
|
SecurityClient.setProviderAuthToken("");
|
||||||
// set JWT token
|
|
||||||
SecurityClient.setToken(token);
|
SecurityClient.setToken(token);
|
||||||
|
|
||||||
const privateKey = await decryptPrivateKeyHelper({
|
|
||||||
encryptionVersion,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
password,
|
|
||||||
salt,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag
|
|
||||||
});
|
|
||||||
|
|
||||||
saveTokenToLocalStorage({
|
|
||||||
publicKey,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
privateKey
|
|
||||||
});
|
|
||||||
|
|
||||||
if (email) {
|
if (email) {
|
||||||
telemetry.identify(email, email);
|
telemetry.identify(email, email);
|
||||||
telemetry.capture("User Logged In");
|
telemetry.capture("User Logged In");
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
mfaEnabled: false,
|
|
||||||
loginResponse: {
|
loginResponse: {
|
||||||
email,
|
email,
|
||||||
privateKey,
|
privateKey: "",
|
||||||
JTWToken: token
|
JTWToken: token
|
||||||
},
|
},
|
||||||
success: true
|
success: true
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
/* eslint-disable prefer-destructuring */
|
|
||||||
import jsrp from "jsrp";
|
|
||||||
|
|
||||||
import { decryptPrivateKeyHelper } from "@app/helpers/key";
|
|
||||||
import { login1, verifyMfaToken } from "@app/hooks/api/auth/queries";
|
|
||||||
|
|
||||||
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
|
|
||||||
import SecurityClient from "./SecurityClient";
|
|
||||||
|
|
||||||
// eslint-disable-next-line new-cap
|
|
||||||
const client = new jsrp.client();
|
|
||||||
|
|
||||||
interface IsMfaLoginSuccessful {
|
|
||||||
success: boolean;
|
|
||||||
loginResponse: {
|
|
||||||
privateKey: string;
|
|
||||||
JTWToken: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether or not MFA-login is successful for user with email [email]
|
|
||||||
* and MFA token [mfaToken]
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {String} obj.email - email of user
|
|
||||||
* @param {String} obj.mfaToken - MFA code/token
|
|
||||||
*/
|
|
||||||
const attemptLoginMfa = async ({
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
providerAuthToken,
|
|
||||||
mfaToken
|
|
||||||
}: {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
providerAuthToken?: string;
|
|
||||||
mfaToken: string;
|
|
||||||
}): Promise<IsMfaLoginSuccessful> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
client.init(
|
|
||||||
{
|
|
||||||
username: email,
|
|
||||||
password
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
try {
|
|
||||||
const clientPublicKey = client.getPublicKey();
|
|
||||||
const { salt } = await login1({
|
|
||||||
email,
|
|
||||||
clientPublicKey,
|
|
||||||
providerAuthToken
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
encryptionVersion,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
token,
|
|
||||||
publicKey,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv,
|
|
||||||
tag
|
|
||||||
} = await verifyMfaToken({
|
|
||||||
email,
|
|
||||||
mfaCode: mfaToken
|
|
||||||
});
|
|
||||||
|
|
||||||
// unset temporary (MFA) JWT token and set JWT token
|
|
||||||
SecurityClient.setMfaToken("");
|
|
||||||
SecurityClient.setToken(token);
|
|
||||||
SecurityClient.setProviderAuthToken("");
|
|
||||||
|
|
||||||
const privateKey = await decryptPrivateKeyHelper({
|
|
||||||
encryptionVersion,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
password,
|
|
||||||
salt,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag
|
|
||||||
});
|
|
||||||
|
|
||||||
saveTokenToLocalStorage({
|
|
||||||
publicKey,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
privateKey
|
|
||||||
});
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
success: true,
|
|
||||||
loginResponse: {
|
|
||||||
privateKey,
|
|
||||||
JTWToken: token
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default attemptLoginMfa;
|
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
/* eslint-disable prefer-destructuring */
|
/* eslint-disable prefer-destructuring */
|
||||||
|
import axios from "axios";
|
||||||
import jsrp from "jsrp";
|
import jsrp from "jsrp";
|
||||||
|
|
||||||
import { decryptPrivateKeyHelper } from "@app/helpers/key";
|
import { login1, login2, loginV3 } from "@app/hooks/api/auth/queries";
|
||||||
import { login1, login2 } from "@app/hooks/api/auth/queries";
|
|
||||||
|
|
||||||
|
import { createNotification } from "../notifications";
|
||||||
import Telemetry from "./telemetry/Telemetry";
|
import Telemetry from "./telemetry/Telemetry";
|
||||||
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
|
|
||||||
import SecurityClient from "./SecurityClient";
|
import SecurityClient from "./SecurityClient";
|
||||||
|
|
||||||
|
export enum LoginMode {
|
||||||
|
LegacySrp = "legacy-srp",
|
||||||
|
ServerSide = "server-side"
|
||||||
|
}
|
||||||
|
|
||||||
interface IsLoginSuccessful {
|
interface IsLoginSuccessful {
|
||||||
mfaEnabled: boolean;
|
|
||||||
success: boolean;
|
success: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,14 +27,62 @@ const attemptLogin = async ({
|
|||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
providerAuthToken,
|
providerAuthToken,
|
||||||
captchaToken
|
captchaToken,
|
||||||
|
loginMode = LoginMode.ServerSide
|
||||||
}: {
|
}: {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
providerAuthToken?: string;
|
providerAuthToken?: string;
|
||||||
captchaToken?: string;
|
captchaToken?: string;
|
||||||
|
loginMode?: LoginMode;
|
||||||
}): Promise<IsLoginSuccessful> => {
|
}): Promise<IsLoginSuccessful> => {
|
||||||
const telemetry = new Telemetry().getInstance();
|
const telemetry = new Telemetry().getInstance();
|
||||||
|
|
||||||
|
if (loginMode === LoginMode.ServerSide) {
|
||||||
|
console.log("Attempting login with server side...");
|
||||||
|
const data = await loginV3({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
providerAuthToken,
|
||||||
|
captchaToken
|
||||||
|
}).catch((err) => {
|
||||||
|
if (axios.isAxiosError(err) && err.response?.status === 400) {
|
||||||
|
if (err.response?.data?.error === "LegacyEncryptionScheme") {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to login without SRP, attempting to authenticate with legacy SRP authentication.",
|
||||||
|
type: "info"
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data === null) {
|
||||||
|
return attemptLogin({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
providerAuthToken,
|
||||||
|
captchaToken,
|
||||||
|
loginMode: LoginMode.LegacySrp
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SecurityClient.setProviderAuthToken("");
|
||||||
|
SecurityClient.setToken(data.accessToken);
|
||||||
|
|
||||||
|
if (email) {
|
||||||
|
telemetry.identify(email, email);
|
||||||
|
telemetry.capture("User Logged In");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line new-cap
|
// eslint-disable-next-line new-cap
|
||||||
const client = new jsrp.client();
|
const client = new jsrp.client();
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
@@ -48,18 +100,7 @@ const attemptLogin = async ({
|
|||||||
client.setServerPublicKey(serverPublicKey);
|
client.setServerPublicKey(serverPublicKey);
|
||||||
const clientProof = client.getProof(); // called M1
|
const clientProof = client.getProof(); // called M1
|
||||||
|
|
||||||
const {
|
const { encryptionVersion, token, encryptedPrivateKey, iv, tag } = await login2({
|
||||||
mfaEnabled,
|
|
||||||
encryptionVersion,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
token,
|
|
||||||
publicKey,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv,
|
|
||||||
tag
|
|
||||||
} = await login2({
|
|
||||||
captchaToken,
|
captchaToken,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
@@ -67,56 +108,22 @@ const attemptLogin = async ({
|
|||||||
providerAuthToken
|
providerAuthToken
|
||||||
});
|
});
|
||||||
|
|
||||||
if (mfaEnabled) {
|
if (encryptionVersion && encryptedPrivateKey && iv && tag && token) {
|
||||||
// case: MFA is enabled
|
|
||||||
|
|
||||||
// set temporary (MFA) JWT token
|
|
||||||
SecurityClient.setMfaToken(token);
|
|
||||||
|
|
||||||
return {
|
|
||||||
mfaEnabled,
|
|
||||||
success: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!mfaEnabled && encryptionVersion && encryptedPrivateKey && iv && tag && token) {
|
|
||||||
// case: MFA is not enabled
|
|
||||||
|
|
||||||
// unset provider auth token in case it was used
|
// unset provider auth token in case it was used
|
||||||
SecurityClient.setProviderAuthToken("");
|
SecurityClient.setProviderAuthToken("");
|
||||||
// set JWT token
|
// set JWT token
|
||||||
SecurityClient.setToken(token);
|
SecurityClient.setToken(token);
|
||||||
|
|
||||||
const privateKey = await decryptPrivateKeyHelper({
|
|
||||||
encryptionVersion,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
password,
|
|
||||||
salt,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag
|
|
||||||
});
|
|
||||||
|
|
||||||
saveTokenToLocalStorage({
|
|
||||||
publicKey,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
privateKey
|
|
||||||
});
|
|
||||||
|
|
||||||
if (email) {
|
if (email) {
|
||||||
telemetry.identify(email, email);
|
telemetry.identify(email, email);
|
||||||
telemetry.capture("User Logged In");
|
telemetry.capture("User Logged In");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mfaEnabled: false,
|
|
||||||
success: true
|
success: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { success: false, mfaEnabled: false };
|
return { success: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default attemptLogin;
|
export default attemptLogin;
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
/* eslint-disable prefer-destructuring */
|
|
||||||
import jsrp from "jsrp";
|
|
||||||
|
|
||||||
import { decryptPrivateKeyHelper } from "@app/helpers/key";
|
|
||||||
import { login1, verifyMfaToken } from "@app/hooks/api/auth/queries";
|
|
||||||
|
|
||||||
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
|
|
||||||
import SecurityClient from "./SecurityClient";
|
|
||||||
|
|
||||||
// eslint-disable-next-line new-cap
|
|
||||||
const client = new jsrp.client();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether or not MFA-login is successful for user with email [email]
|
|
||||||
* and MFA token [mfaToken]
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {String} obj.email - email of user
|
|
||||||
* @param {String} obj.mfaToken - MFA code/token
|
|
||||||
*/
|
|
||||||
const attemptLoginMfa = async ({
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
providerAuthToken,
|
|
||||||
mfaToken
|
|
||||||
}: {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
providerAuthToken?: string;
|
|
||||||
mfaToken: string;
|
|
||||||
}): Promise<boolean> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
client.init(
|
|
||||||
{
|
|
||||||
username: email,
|
|
||||||
password
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
try {
|
|
||||||
const clientPublicKey = client.getPublicKey();
|
|
||||||
const { salt } = await login1({
|
|
||||||
email,
|
|
||||||
clientPublicKey,
|
|
||||||
providerAuthToken
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
encryptionVersion,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
token,
|
|
||||||
publicKey,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv,
|
|
||||||
tag
|
|
||||||
} = await verifyMfaToken({
|
|
||||||
email,
|
|
||||||
mfaCode: mfaToken
|
|
||||||
});
|
|
||||||
|
|
||||||
// unset temporary (MFA) JWT token and set JWT token
|
|
||||||
SecurityClient.setMfaToken("");
|
|
||||||
SecurityClient.setToken(token);
|
|
||||||
SecurityClient.setProviderAuthToken("");
|
|
||||||
|
|
||||||
const privateKey = await decryptPrivateKeyHelper({
|
|
||||||
encryptionVersion,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
password,
|
|
||||||
salt,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag
|
|
||||||
});
|
|
||||||
|
|
||||||
saveTokenToLocalStorage({
|
|
||||||
publicKey,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
privateKey
|
|
||||||
});
|
|
||||||
|
|
||||||
resolve(true);
|
|
||||||
} catch (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default attemptLoginMfa;
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
/* eslint-disable new-cap */
|
|
||||||
import crypto from "crypto";
|
|
||||||
|
|
||||||
import jsrp from "jsrp";
|
|
||||||
|
|
||||||
import { issueBackupPrivateKey, srp1 } from "@app/hooks/api/auth/queries";
|
|
||||||
|
|
||||||
import generateBackupPDF from "../generateBackupPDF";
|
|
||||||
import Aes256Gcm from "./aes-256-gcm";
|
|
||||||
|
|
||||||
const clientPassword = new jsrp.client();
|
|
||||||
const clientKey = new jsrp.client();
|
|
||||||
|
|
||||||
interface BackupKeyProps {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
personalName: string;
|
|
||||||
setBackupKeyError: (value: boolean) => void;
|
|
||||||
setBackupKeyIssued: (value: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function issue a backup key for a user
|
|
||||||
* @param {obkect} obj
|
|
||||||
* @param {string} obj.email - email of a user issuing a backup key
|
|
||||||
* @param {string} obj.password - password of a user issuing a backup key
|
|
||||||
* @param {string} obj.personalName - name of a user issuing a backup key
|
|
||||||
* @param {function} obj.setBackupKeyError - state function that turns true if there is an erorr with a backup key
|
|
||||||
* @param {function} obj.setBackupKeyIssued - state function that turns true if a backup key was issued correctly
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const issueBackupKey = async ({
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
personalName,
|
|
||||||
setBackupKeyError,
|
|
||||||
setBackupKeyIssued
|
|
||||||
}: BackupKeyProps) => {
|
|
||||||
try {
|
|
||||||
setBackupKeyError(false);
|
|
||||||
setBackupKeyIssued(false);
|
|
||||||
clientPassword.init(
|
|
||||||
{
|
|
||||||
username: email,
|
|
||||||
password
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
const clientPublicKey = clientPassword.getPublicKey();
|
|
||||||
|
|
||||||
let serverPublicKey;
|
|
||||||
let salt;
|
|
||||||
try {
|
|
||||||
const res = await srp1({
|
|
||||||
clientPublicKey
|
|
||||||
});
|
|
||||||
serverPublicKey = res.serverPublicKey;
|
|
||||||
salt = res.salt;
|
|
||||||
} catch (err) {
|
|
||||||
setBackupKeyError(true);
|
|
||||||
console.log("Wrong current password", err, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
clientPassword.setSalt(salt as string);
|
|
||||||
clientPassword.setServerPublicKey(serverPublicKey as string);
|
|
||||||
const clientProof = clientPassword.getProof(); // called M1
|
|
||||||
|
|
||||||
const generatedKey = crypto.randomBytes(16).toString("hex");
|
|
||||||
|
|
||||||
clientKey.init(
|
|
||||||
{
|
|
||||||
username: email,
|
|
||||||
password: generatedKey
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
clientKey.createVerifier(
|
|
||||||
async (_err: any, result: { salt: string; verifier: string }) => {
|
|
||||||
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
|
|
||||||
text: String(localStorage.getItem("PRIVATE_KEY")),
|
|
||||||
secret: generatedKey
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await issueBackupPrivateKey({
|
|
||||||
encryptedPrivateKey: ciphertext,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
salt: result.salt,
|
|
||||||
verifier: result.verifier,
|
|
||||||
clientProof
|
|
||||||
});
|
|
||||||
|
|
||||||
generateBackupPDF({
|
|
||||||
personalName,
|
|
||||||
personalEmail: email,
|
|
||||||
generatedKey
|
|
||||||
});
|
|
||||||
setBackupKeyIssued(true);
|
|
||||||
} catch {
|
|
||||||
setBackupKeyError(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
setBackupKeyError(true);
|
|
||||||
console.log("Failed to issue a backup key");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default issueBackupKey;
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
interface Props {
|
|
||||||
protectedKey?: string;
|
|
||||||
protectedKeyIV?: string;
|
|
||||||
protectedKeyTag?: string;
|
|
||||||
publicKey?: string;
|
|
||||||
encryptedPrivateKey?: string;
|
|
||||||
iv?: string;
|
|
||||||
tag?: string;
|
|
||||||
privateKey?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const saveTokenToLocalStorage = ({
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
publicKey,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
privateKey
|
|
||||||
}: Props) => {
|
|
||||||
try {
|
|
||||||
if (protectedKey) {
|
|
||||||
localStorage.removeItem("protectedKey");
|
|
||||||
localStorage.setItem("protectedKey", protectedKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (protectedKeyIV) {
|
|
||||||
localStorage.removeItem("protectedKeyIV");
|
|
||||||
localStorage.setItem("protectedKeyIV", protectedKeyIV);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (protectedKeyTag) {
|
|
||||||
localStorage.removeItem("protectedKeyTag");
|
|
||||||
localStorage.setItem("protectedKeyTag", protectedKeyTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (publicKey) {
|
|
||||||
localStorage.removeItem("publicKey");
|
|
||||||
localStorage.setItem("publicKey", publicKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (encryptedPrivateKey) {
|
|
||||||
localStorage.removeItem("encryptedPrivateKey");
|
|
||||||
localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (iv) {
|
|
||||||
localStorage.removeItem("iv");
|
|
||||||
localStorage.setItem("iv", iv);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tag) {
|
|
||||||
localStorage.removeItem("tag");
|
|
||||||
localStorage.setItem("tag", tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (privateKey) {
|
|
||||||
localStorage.removeItem("PRIVATE_KEY");
|
|
||||||
localStorage.setItem("PRIVATE_KEY", privateKey);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof Error) {
|
|
||||||
throw new Error(`Unable to send the tokens in local storage:${err.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -8,26 +8,24 @@ import { organizationKeys } from "../organization/queries";
|
|||||||
import { setAuthToken } from "../reactQuery";
|
import { setAuthToken } from "../reactQuery";
|
||||||
import { workspaceKeys } from "../workspace";
|
import { workspaceKeys } from "../workspace";
|
||||||
import {
|
import {
|
||||||
ChangePasswordDTO,
|
|
||||||
CompleteAccountDTO,
|
CompleteAccountDTO,
|
||||||
CompleteAccountSignupDTO,
|
CompleteAccountSignupDTO,
|
||||||
GetAuthTokenAPI,
|
GetAuthTokenAPI,
|
||||||
GetBackupEncryptedPrivateKeyDTO,
|
GetBackupEncryptedPrivateKeyDTO,
|
||||||
IssueBackupPrivateKeyDTO,
|
|
||||||
Login1DTO,
|
Login1DTO,
|
||||||
Login1Res,
|
Login1Res,
|
||||||
Login2DTO,
|
Login2DTO,
|
||||||
Login2Res,
|
Login2Res,
|
||||||
LoginLDAPDTO,
|
LoginLDAPDTO,
|
||||||
LoginLDAPRes,
|
LoginLDAPRes,
|
||||||
|
LoginV3DTO,
|
||||||
|
LoginV3Res,
|
||||||
MfaMethod,
|
MfaMethod,
|
||||||
ResetPasswordDTO,
|
ResetPasswordDTO,
|
||||||
ResetPasswordV2DTO,
|
ResetPasswordV2DTO,
|
||||||
ResetUserPasswordV2DTO,
|
ResetUserPasswordV2DTO,
|
||||||
SendMfaTokenDTO,
|
SendMfaTokenDTO,
|
||||||
SetupPasswordDTO,
|
SetupPasswordDTO,
|
||||||
SRP1DTO,
|
|
||||||
SRPR1Res,
|
|
||||||
TOauthTokenExchangeDTO,
|
TOauthTokenExchangeDTO,
|
||||||
UserAgentType,
|
UserAgentType,
|
||||||
UserEncryptionVersion,
|
UserEncryptionVersion,
|
||||||
@@ -50,21 +48,14 @@ export const login2 = async (loginDetails: Login2DTO) => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => {
|
export const loginV3 = async (loginDetails: LoginV3DTO) => {
|
||||||
const { data } = await apiRequest.post<LoginLDAPRes>("/api/v1/ldap/login", loginLDAPDetails); // return if account is complete or not + provider auth token
|
const { data } = await apiRequest.post<LoginV3Res>("/api/v3/auth/login", loginDetails);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useLogin1 = () => {
|
export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => {
|
||||||
return useMutation({
|
const { data } = await apiRequest.post<LoginLDAPRes>("/api/v1/ldap/login", loginLDAPDetails); // return if account is complete or not + provider auth token
|
||||||
mutationFn: async (details: {
|
return data;
|
||||||
email: string;
|
|
||||||
clientPublicKey: string;
|
|
||||||
providerAuthToken?: string;
|
|
||||||
}) => {
|
|
||||||
return login1(details);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const selectOrganization = async (data: {
|
export const selectOrganization = async (data: {
|
||||||
@@ -143,11 +134,6 @@ export const useOauthTokenExchange = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const srp1 = async (details: SRP1DTO) => {
|
|
||||||
const { data } = await apiRequest.post<SRPR1Res>("/api/v1/password/srp1", details);
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const completeAccountSignup = async (details: CompleteAccountSignupDTO) => {
|
export const completeAccountSignup = async (details: CompleteAccountSignupDTO) => {
|
||||||
const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", details);
|
const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", details);
|
||||||
return data;
|
return data;
|
||||||
@@ -158,14 +144,6 @@ export const completeAccountSignupInvite = async (details: CompleteAccountDTO) =
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCompleteAccountSignup = () => {
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async (details: CompleteAccountSignupDTO) => {
|
|
||||||
return completeAccountSignup(details);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useSendMfaToken = () => {
|
export const useSendMfaToken = () => {
|
||||||
return useMutation<object, object, SendMfaTokenDTO>({
|
return useMutation<object, object, SendMfaTokenDTO>({
|
||||||
mutationFn: async ({ email }) => {
|
mutationFn: async ({ email }) => {
|
||||||
@@ -263,11 +241,6 @@ export const useVerifyPasswordResetCode = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const issueBackupPrivateKey = async (details: IssueBackupPrivateKeyDTO) => {
|
|
||||||
const { data } = await apiRequest.post("/api/v1/password/backup-private-key", details);
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBackupEncryptedPrivateKey = async ({
|
export const getBackupEncryptedPrivateKey = async ({
|
||||||
verificationToken
|
verificationToken
|
||||||
}: GetBackupEncryptedPrivateKeyDTO) => {
|
}: GetBackupEncryptedPrivateKeyDTO) => {
|
||||||
@@ -328,20 +301,6 @@ export const useResetUserPasswordV2 = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const changePassword = async (details: ChangePasswordDTO) => {
|
|
||||||
const { data } = await apiRequest.post("/api/v1/password/change-password", details);
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useChangePassword = () => {
|
|
||||||
// note: use after srp1
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async (details: ChangePasswordDTO) => {
|
|
||||||
return changePassword(details);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Refresh token is set as cookie when logged in
|
// Refresh token is set as cookie when logged in
|
||||||
// Using that we fetch the auth bearer token needed for auth calls
|
// Using that we fetch the auth bearer token needed for auth calls
|
||||||
export const fetchAuthToken = async () => {
|
export const fetchAuthToken = async () => {
|
||||||
|
|||||||