mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-02 16:55:02 +00:00
Compare commits
11 Commits
doc/add-gi
...
feat/totp-
Author | SHA1 | Date | |
---|---|---|---|
682b552fdc | |||
774371a218 | |||
c4b54de303 | |||
433971a72d | |||
6bae3628c0 | |||
4cb935dae7 | |||
5b0dbf04b2 | |||
b050db84ab | |||
8fef6911f1 | |||
44ba31a743 | |||
6bdbac4750 |
61
backend/package-lock.json
generated
61
backend/package-lock.json
generated
@ -75,6 +75,7 @@
|
|||||||
"openid-client": "^5.6.5",
|
"openid-client": "^5.6.5",
|
||||||
"ora": "^7.0.1",
|
"ora": "^7.0.1",
|
||||||
"oracledb": "^6.4.0",
|
"oracledb": "^6.4.0",
|
||||||
|
"otplib": "^12.0.1",
|
||||||
"passport-github": "^1.1.0",
|
"passport-github": "^1.1.0",
|
||||||
"passport-gitlab2": "^5.0.0",
|
"passport-gitlab2": "^5.0.0",
|
||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
@ -6815,6 +6816,48 @@
|
|||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@otplib/core": {
|
||||||
|
"version": "12.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz",
|
||||||
|
"integrity": "sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA=="
|
||||||
|
},
|
||||||
|
"node_modules/@otplib/plugin-crypto": {
|
||||||
|
"version": "12.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@otplib/plugin-crypto/-/plugin-crypto-12.0.1.tgz",
|
||||||
|
"integrity": "sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@otplib/core": "^12.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@otplib/plugin-thirty-two": {
|
||||||
|
"version": "12.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@otplib/plugin-thirty-two/-/plugin-thirty-two-12.0.1.tgz",
|
||||||
|
"integrity": "sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@otplib/core": "^12.0.1",
|
||||||
|
"thirty-two": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@otplib/preset-default": {
|
||||||
|
"version": "12.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@otplib/preset-default/-/preset-default-12.0.1.tgz",
|
||||||
|
"integrity": "sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@otplib/core": "^12.0.1",
|
||||||
|
"@otplib/plugin-crypto": "^12.0.1",
|
||||||
|
"@otplib/plugin-thirty-two": "^12.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@otplib/preset-v11": {
|
||||||
|
"version": "12.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@otplib/preset-v11/-/preset-v11-12.0.1.tgz",
|
||||||
|
"integrity": "sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@otplib/core": "^12.0.1",
|
||||||
|
"@otplib/plugin-crypto": "^12.0.1",
|
||||||
|
"@otplib/plugin-thirty-two": "^12.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@peculiar/asn1-cms": {
|
"node_modules/@peculiar/asn1-cms": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.3.8.tgz",
|
||||||
@ -16453,6 +16496,16 @@
|
|||||||
"node": ">=14.6"
|
"node": ">=14.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/otplib": {
|
||||||
|
"version": "12.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/otplib/-/otplib-12.0.1.tgz",
|
||||||
|
"integrity": "sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@otplib/core": "^12.0.1",
|
||||||
|
"@otplib/preset-default": "^12.0.1",
|
||||||
|
"@otplib/preset-v11": "^12.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/p-finally": {
|
"node_modules/p-finally": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||||
@ -19553,6 +19606,14 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/thirty-two": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.2.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/thread-stream": {
|
"node_modules/thread-stream": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz",
|
||||||
|
@ -181,6 +181,7 @@
|
|||||||
"openid-client": "^5.6.5",
|
"openid-client": "^5.6.5",
|
||||||
"ora": "^7.0.1",
|
"ora": "^7.0.1",
|
||||||
"oracledb": "^6.4.0",
|
"oracledb": "^6.4.0",
|
||||||
|
"otplib": "^12.0.1",
|
||||||
"passport-github": "^1.1.0",
|
"passport-github": "^1.1.0",
|
||||||
"passport-gitlab2": "^5.0.0",
|
"passport-gitlab2": "^5.0.0",
|
||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@ -79,6 +79,7 @@ import { TServiceTokenServiceFactory } from "@app/services/service-token/service
|
|||||||
import { TSlackServiceFactory } from "@app/services/slack/slack-service";
|
import { TSlackServiceFactory } from "@app/services/slack/slack-service";
|
||||||
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
||||||
import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
|
import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
|
||||||
|
import { TTotpServiceFactory } from "@app/services/totp/totp-service";
|
||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
import { TUserServiceFactory } from "@app/services/user/user-service";
|
import { TUserServiceFactory } from "@app/services/user/user-service";
|
||||||
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
|
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
|
||||||
@ -193,6 +194,7 @@ declare module "fastify" {
|
|||||||
migration: TExternalMigrationServiceFactory;
|
migration: TExternalMigrationServiceFactory;
|
||||||
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
|
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
|
||||||
projectTemplate: TProjectTemplateServiceFactory;
|
projectTemplate: TProjectTemplateServiceFactory;
|
||||||
|
totp: TTotpServiceFactory;
|
||||||
};
|
};
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
// everywhere else access using service layer
|
// everywhere else access using service layer
|
||||||
|
4
backend/src/@types/knex.d.ts
vendored
4
backend/src/@types/knex.d.ts
vendored
@ -314,6 +314,9 @@ import {
|
|||||||
TSuperAdmin,
|
TSuperAdmin,
|
||||||
TSuperAdminInsert,
|
TSuperAdminInsert,
|
||||||
TSuperAdminUpdate,
|
TSuperAdminUpdate,
|
||||||
|
TTotpConfigs,
|
||||||
|
TTotpConfigsInsert,
|
||||||
|
TTotpConfigsUpdate,
|
||||||
TTrustedIps,
|
TTrustedIps,
|
||||||
TTrustedIpsInsert,
|
TTrustedIpsInsert,
|
||||||
TTrustedIpsUpdate,
|
TTrustedIpsUpdate,
|
||||||
@ -826,5 +829,6 @@ declare module "knex/types/tables" {
|
|||||||
TProjectTemplatesInsert,
|
TProjectTemplatesInsert,
|
||||||
TProjectTemplatesUpdate
|
TProjectTemplatesUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.TotpConfig]: KnexOriginal.CompositeTableType<TTotpConfigs, TTotpConfigsInsert, TTotpConfigsUpdate>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
54
backend/src/db/migrations/20241112082701_add-totp-support.ts
Normal file
54
backend/src/db/migrations/20241112082701_add-totp-support.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.TotpConfig))) {
|
||||||
|
await knex.schema.createTable(TableName.TotpConfig, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.uuid("userId").notNullable();
|
||||||
|
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||||
|
t.boolean("isVerified").defaultTo(false).notNullable();
|
||||||
|
t.binary("encryptedRecoveryCodes").notNullable();
|
||||||
|
t.binary("encryptedSecret").notNullable();
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.unique("userId");
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.TotpConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
const doesOrgMfaMethodColExist = await knex.schema.hasColumn(TableName.Organization, "selectedMfaMethod");
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||||
|
if (!doesOrgMfaMethodColExist) {
|
||||||
|
t.string("selectedMfaMethod");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const doesUserSelectedMfaMethodColExist = await knex.schema.hasColumn(TableName.Users, "selectedMfaMethod");
|
||||||
|
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||||
|
if (!doesUserSelectedMfaMethodColExist) {
|
||||||
|
t.string("selectedMfaMethod");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.TotpConfig);
|
||||||
|
await knex.schema.dropTableIfExists(TableName.TotpConfig);
|
||||||
|
|
||||||
|
const doesOrgMfaMethodColExist = await knex.schema.hasColumn(TableName.Organization, "selectedMfaMethod");
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||||
|
if (doesOrgMfaMethodColExist) {
|
||||||
|
t.dropColumn("selectedMfaMethod");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const doesUserSelectedMfaMethodColExist = await knex.schema.hasColumn(TableName.Users, "selectedMfaMethod");
|
||||||
|
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||||
|
if (doesUserSelectedMfaMethodColExist) {
|
||||||
|
t.dropColumn("selectedMfaMethod");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -106,6 +106,7 @@ export * from "./secrets-v2";
|
|||||||
export * from "./service-tokens";
|
export * from "./service-tokens";
|
||||||
export * from "./slack-integrations";
|
export * from "./slack-integrations";
|
||||||
export * from "./super-admin";
|
export * from "./super-admin";
|
||||||
|
export * from "./totp-configs";
|
||||||
export * from "./trusted-ips";
|
export * from "./trusted-ips";
|
||||||
export * from "./user-actions";
|
export * from "./user-actions";
|
||||||
export * from "./user-aliases";
|
export * from "./user-aliases";
|
||||||
|
@ -117,6 +117,7 @@ export enum TableName {
|
|||||||
ExternalKms = "external_kms",
|
ExternalKms = "external_kms",
|
||||||
InternalKms = "internal_kms",
|
InternalKms = "internal_kms",
|
||||||
InternalKmsKeyVersion = "internal_kms_key_version",
|
InternalKmsKeyVersion = "internal_kms_key_version",
|
||||||
|
TotpConfig = "totp_configs",
|
||||||
// @depreciated
|
// @depreciated
|
||||||
KmsKeyVersion = "kms_key_versions",
|
KmsKeyVersion = "kms_key_versions",
|
||||||
WorkflowIntegrations = "workflow_integrations",
|
WorkflowIntegrations = "workflow_integrations",
|
||||||
|
@ -21,7 +21,8 @@ export const OrganizationsSchema = z.object({
|
|||||||
kmsDefaultKeyId: z.string().uuid().nullable().optional(),
|
kmsDefaultKeyId: z.string().uuid().nullable().optional(),
|
||||||
kmsEncryptedDataKey: zodBuffer.nullable().optional(),
|
kmsEncryptedDataKey: zodBuffer.nullable().optional(),
|
||||||
defaultMembershipRole: z.string().default("member"),
|
defaultMembershipRole: z.string().default("member"),
|
||||||
enforceMfa: z.boolean().default(false)
|
enforceMfa: z.boolean().default(false),
|
||||||
|
selectedMfaMethod: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||||
|
24
backend/src/db/schemas/totp-configs.ts
Normal file
24
backend/src/db/schemas/totp-configs.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const TotpConfigsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
userId: z.string().uuid(),
|
||||||
|
isVerified: z.boolean().default(false),
|
||||||
|
encryptedRecoveryCodes: zodBuffer,
|
||||||
|
encryptedSecret: zodBuffer,
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TTotpConfigs = z.infer<typeof TotpConfigsSchema>;
|
||||||
|
export type TTotpConfigsInsert = Omit<z.input<typeof TotpConfigsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TTotpConfigsUpdate = Partial<Omit<z.input<typeof TotpConfigsSchema>, TImmutableDBKeys>>;
|
@ -26,7 +26,8 @@ export const UsersSchema = z.object({
|
|||||||
consecutiveFailedMfaAttempts: z.number().default(0).nullable().optional(),
|
consecutiveFailedMfaAttempts: z.number().default(0).nullable().optional(),
|
||||||
isLocked: z.boolean().default(false).nullable().optional(),
|
isLocked: z.boolean().default(false).nullable().optional(),
|
||||||
temporaryLockDateEnd: z.date().nullable().optional(),
|
temporaryLockDateEnd: z.date().nullable().optional(),
|
||||||
consecutiveFailedPasswordAttempts: z.number().default(0).nullable().optional()
|
consecutiveFailedPasswordAttempts: z.number().default(0).nullable().optional(),
|
||||||
|
selectedMfaMethod: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TUsers = z.infer<typeof UsersSchema>;
|
export type TUsers = z.infer<typeof UsersSchema>;
|
||||||
|
@ -122,6 +122,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
`email: ${email} firstName: ${profile.firstName as string}`
|
`email: ${email} firstName: ${profile.firstName as string}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
throw new Error("Invalid saml request. Missing email or first name");
|
||||||
}
|
}
|
||||||
|
|
||||||
const userMetadata = Object.keys(profile.attributes || {})
|
const userMetadata = Object.keys(profile.attributes || {})
|
||||||
|
@ -201,6 +201,8 @@ import { getServerCfg, superAdminServiceFactory } from "@app/services/super-admi
|
|||||||
import { telemetryDALFactory } from "@app/services/telemetry/telemetry-dal";
|
import { telemetryDALFactory } from "@app/services/telemetry/telemetry-dal";
|
||||||
import { telemetryQueueServiceFactory } from "@app/services/telemetry/telemetry-queue";
|
import { telemetryQueueServiceFactory } from "@app/services/telemetry/telemetry-queue";
|
||||||
import { telemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
|
import { telemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
|
||||||
|
import { totpConfigDALFactory } from "@app/services/totp/totp-config-dal";
|
||||||
|
import { totpServiceFactory } from "@app/services/totp/totp-service";
|
||||||
import { userDALFactory } from "@app/services/user/user-dal";
|
import { userDALFactory } from "@app/services/user/user-dal";
|
||||||
import { userServiceFactory } from "@app/services/user/user-service";
|
import { userServiceFactory } from "@app/services/user/user-service";
|
||||||
import { userAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
import { userAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||||
@ -348,6 +350,7 @@ export const registerRoutes = async (
|
|||||||
const slackIntegrationDAL = slackIntegrationDALFactory(db);
|
const slackIntegrationDAL = slackIntegrationDALFactory(db);
|
||||||
const projectSlackConfigDAL = projectSlackConfigDALFactory(db);
|
const projectSlackConfigDAL = projectSlackConfigDALFactory(db);
|
||||||
const workflowIntegrationDAL = workflowIntegrationDALFactory(db);
|
const workflowIntegrationDAL = workflowIntegrationDALFactory(db);
|
||||||
|
const totpConfigDAL = totpConfigDALFactory(db);
|
||||||
|
|
||||||
const externalGroupOrgRoleMappingDAL = externalGroupOrgRoleMappingDALFactory(db);
|
const externalGroupOrgRoleMappingDAL = externalGroupOrgRoleMappingDALFactory(db);
|
||||||
|
|
||||||
@ -511,12 +514,19 @@ export const registerRoutes = async (
|
|||||||
projectMembershipDAL
|
projectMembershipDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL });
|
const totpService = totpServiceFactory({
|
||||||
|
totpConfigDAL,
|
||||||
|
userDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, totpService });
|
||||||
const passwordService = authPaswordServiceFactory({
|
const passwordService = authPaswordServiceFactory({
|
||||||
tokenService,
|
tokenService,
|
||||||
smtpService,
|
smtpService,
|
||||||
authDAL,
|
authDAL,
|
||||||
userDAL
|
userDAL,
|
||||||
|
totpConfigDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL, projectDAL });
|
const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL, projectDAL });
|
||||||
@ -1369,7 +1379,8 @@ export const registerRoutes = async (
|
|||||||
workflowIntegration: workflowIntegrationService,
|
workflowIntegration: workflowIntegrationService,
|
||||||
migration: migrationService,
|
migration: migrationService,
|
||||||
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService,
|
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService,
|
||||||
projectTemplate: projectTemplateService
|
projectTemplate: projectTemplateService,
|
||||||
|
totp: totpService
|
||||||
});
|
});
|
||||||
|
|
||||||
const cronJobs: CronJob[] = [];
|
const cronJobs: CronJob[] = [];
|
||||||
|
@ -108,7 +108,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
|||||||
tokenVersionId: tokenVersion.id,
|
tokenVersionId: tokenVersion.id,
|
||||||
accessVersion: tokenVersion.accessVersion,
|
accessVersion: tokenVersion.accessVersion,
|
||||||
organizationId: decodedToken.organizationId,
|
organizationId: decodedToken.organizationId,
|
||||||
isMfaVerified: decodedToken.isMfaVerified
|
isMfaVerified: decodedToken.isMfaVerified,
|
||||||
|
mfaMethod: decodedToken.mfaMethod
|
||||||
},
|
},
|
||||||
appCfg.AUTH_SECRET,
|
appCfg.AUTH_SECRET,
|
||||||
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }
|
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }
|
||||||
|
@ -15,7 +15,7 @@ import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
|
|||||||
import { getLastMidnightDateISO } from "@app/lib/fn";
|
import { getLastMidnightDateISO } from "@app/lib/fn";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
import { integrationAuthPubSchema } from "../sanitizedSchemas";
|
import { integrationAuthPubSchema } from "../sanitizedSchemas";
|
||||||
|
|
||||||
@ -259,7 +259,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
message: "Membership role must be a valid slug"
|
message: "Membership role must be a valid slug"
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
enforceMfa: z.boolean().optional()
|
enforceMfa: z.boolean().optional(),
|
||||||
|
selectedMfaMethod: z.nativeEnum(MfaMethod).optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
@ -169,4 +169,103 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
return groupMemberships;
|
return groupMemberships;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/me/totp",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
isVerified: z.boolean(),
|
||||||
|
recoveryCodes: z.string().array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
return server.services.totp.getUserTotpConfig({
|
||||||
|
userId: req.permission.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/me/totp",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
return server.services.totp.deleteUserTotpConfig({
|
||||||
|
userId: req.permission.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/me/totp/register",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
otpUrl: z.string(),
|
||||||
|
recoveryCodes: z.string().array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT], {
|
||||||
|
requireOrg: false
|
||||||
|
}),
|
||||||
|
handler: async (req) => {
|
||||||
|
return server.services.totp.registerUserTotp({
|
||||||
|
userId: req.permission.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/me/totp/verify",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
totp: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT], {
|
||||||
|
requireOrg: false
|
||||||
|
}),
|
||||||
|
handler: async (req) => {
|
||||||
|
return server.services.totp.verifyUserTotpConfig({
|
||||||
|
userId: req.permission.id,
|
||||||
|
totp: req.body.totp
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/me/totp/recovery-codes",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
return server.services.totp.createUserTotpRecoveryCodes({
|
||||||
|
userId: req.permission.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -2,8 +2,9 @@ import jwt from "jsonwebtoken";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { mfaRateLimit } from "@app/server/config/rateLimiter";
|
import { mfaRateLimit } from "@app/server/config/rateLimiter";
|
||||||
import { AuthModeMfaJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
|
import { AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
@ -49,6 +50,38 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/mfa/check/totp",
|
||||||
|
config: {
|
||||||
|
rateLimit: mfaRateLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
isVerified: z.boolean()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
try {
|
||||||
|
const totpConfig = await server.services.totp.getUserTotpConfig({
|
||||||
|
userId: req.mfa.userId
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isVerified: Boolean(totpConfig)
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundError || error instanceof BadRequestError) {
|
||||||
|
return { isVerified: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
url: "/mfa/verify",
|
url: "/mfa/verify",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -57,7 +90,8 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
mfaToken: z.string().trim()
|
mfaToken: z.string().trim(),
|
||||||
|
mfaMethod: z.nativeEnum(MfaMethod).optional().default(MfaMethod.EMAIL)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -86,7 +120,8 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
|||||||
ip: req.realIp,
|
ip: req.realIp,
|
||||||
userId: req.mfa.userId,
|
userId: req.mfa.userId,
|
||||||
orgId: req.mfa.orgId,
|
orgId: req.mfa.orgId,
|
||||||
mfaToken: req.body.mfaToken
|
mfaToken: req.body.mfaToken,
|
||||||
|
mfaMethod: req.body.mfaMethod
|
||||||
});
|
});
|
||||||
|
|
||||||
void res.setCookie("jid", token.refresh, {
|
void res.setCookie("jid", token.refresh, {
|
||||||
|
@ -4,7 +4,7 @@ import { AuthTokenSessionsSchema, OrganizationsSchema, UserEncryptionKeysSchema,
|
|||||||
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
|
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
|
||||||
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMethod, AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerUserRouter = async (server: FastifyZodProvider) => {
|
export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@ -56,7 +56,8 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
isMfaEnabled: z.boolean()
|
isMfaEnabled: z.boolean().optional(),
|
||||||
|
selectedMfaMethod: z.nativeEnum(MfaMethod).optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -66,7 +67,12 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
preHandler: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
preHandler: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const user = await server.services.user.toggleUserMfa(req.permission.id, req.body.isMfaEnabled);
|
const user = await server.services.user.updateUserMfa({
|
||||||
|
userId: req.permission.id,
|
||||||
|
isMfaEnabled: req.body.isMfaEnabled,
|
||||||
|
selectedMfaMethod: req.body.selectedMfaMethod
|
||||||
|
});
|
||||||
|
|
||||||
return { user };
|
return { user };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -48,7 +48,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
isMfaEnabled: z.boolean()
|
isMfaEnabled: z.boolean(),
|
||||||
|
mfaMethod: z.string().optional()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -64,7 +65,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
if (tokens.isMfaEnabled) {
|
if (tokens.isMfaEnabled) {
|
||||||
return {
|
return {
|
||||||
token: tokens.mfa as string,
|
token: tokens.mfa as string,
|
||||||
isMfaEnabled: true
|
isMfaEnabled: true,
|
||||||
|
mfaMethod: tokens.mfaMethod
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ import { TokenType } from "../auth-token/auth-token-types";
|
|||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
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 { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { enforceUserLockStatus, validateProviderAuthToken } from "./auth-fns";
|
import { enforceUserLockStatus, validateProviderAuthToken } from "./auth-fns";
|
||||||
import {
|
import {
|
||||||
@ -26,13 +27,14 @@ import {
|
|||||||
TOauthTokenExchangeDTO,
|
TOauthTokenExchangeDTO,
|
||||||
TVerifyMfaTokenDTO
|
TVerifyMfaTokenDTO
|
||||||
} from "./auth-login-type";
|
} from "./auth-login-type";
|
||||||
import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType } from "./auth-type";
|
import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "./auth-type";
|
||||||
|
|
||||||
type TAuthLoginServiceFactoryDep = {
|
type TAuthLoginServiceFactoryDep = {
|
||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
orgDAL: TOrgDALFactory;
|
orgDAL: TOrgDALFactory;
|
||||||
tokenService: TAuthTokenServiceFactory;
|
tokenService: TAuthTokenServiceFactory;
|
||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
|
totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
||||||
@ -40,7 +42,8 @@ export const authLoginServiceFactory = ({
|
|||||||
userDAL,
|
userDAL,
|
||||||
tokenService,
|
tokenService,
|
||||||
smtpService,
|
smtpService,
|
||||||
orgDAL
|
orgDAL,
|
||||||
|
totpService
|
||||||
}: TAuthLoginServiceFactoryDep) => {
|
}: TAuthLoginServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
* Private
|
* Private
|
||||||
@ -100,7 +103,8 @@ export const authLoginServiceFactory = ({
|
|||||||
userAgent,
|
userAgent,
|
||||||
organizationId,
|
organizationId,
|
||||||
authMethod,
|
authMethod,
|
||||||
isMfaVerified
|
isMfaVerified,
|
||||||
|
mfaMethod
|
||||||
}: {
|
}: {
|
||||||
user: TUsers;
|
user: TUsers;
|
||||||
ip: string;
|
ip: string;
|
||||||
@ -108,6 +112,7 @@ export const authLoginServiceFactory = ({
|
|||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
authMethod: AuthMethod;
|
authMethod: AuthMethod;
|
||||||
isMfaVerified?: boolean;
|
isMfaVerified?: boolean;
|
||||||
|
mfaMethod?: MfaMethod;
|
||||||
}) => {
|
}) => {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
await updateUserDeviceSession(user, ip, userAgent);
|
await updateUserDeviceSession(user, ip, userAgent);
|
||||||
@ -126,7 +131,8 @@ export const authLoginServiceFactory = ({
|
|||||||
tokenVersionId: tokenSession.id,
|
tokenVersionId: tokenSession.id,
|
||||||
accessVersion: tokenSession.accessVersion,
|
accessVersion: tokenSession.accessVersion,
|
||||||
organizationId,
|
organizationId,
|
||||||
isMfaVerified
|
isMfaVerified,
|
||||||
|
mfaMethod
|
||||||
},
|
},
|
||||||
cfg.AUTH_SECRET,
|
cfg.AUTH_SECRET,
|
||||||
{ expiresIn: cfg.JWT_AUTH_LIFETIME }
|
{ expiresIn: cfg.JWT_AUTH_LIFETIME }
|
||||||
@ -140,7 +146,8 @@ export const authLoginServiceFactory = ({
|
|||||||
tokenVersionId: tokenSession.id,
|
tokenVersionId: tokenSession.id,
|
||||||
refreshVersion: tokenSession.refreshVersion,
|
refreshVersion: tokenSession.refreshVersion,
|
||||||
organizationId,
|
organizationId,
|
||||||
isMfaVerified
|
isMfaVerified,
|
||||||
|
mfaMethod
|
||||||
},
|
},
|
||||||
cfg.AUTH_SECRET,
|
cfg.AUTH_SECRET,
|
||||||
{ expiresIn: cfg.JWT_REFRESH_LIFETIME }
|
{ expiresIn: cfg.JWT_REFRESH_LIFETIME }
|
||||||
@ -353,8 +360,12 @@ export const authLoginServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// send multi factor auth token if they it enabled
|
const shouldCheckMfa = selectedOrg.enforceMfa || user.isMfaEnabled;
|
||||||
if ((selectedOrg.enforceMfa || user.isMfaEnabled) && user.email && !decodedToken.isMfaVerified) {
|
const orgMfaMethod = selectedOrg.enforceMfa ? selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL : undefined;
|
||||||
|
const userMfaMethod = user.isMfaEnabled ? user.selectedMfaMethod ?? MfaMethod.EMAIL : undefined;
|
||||||
|
const mfaMethod = orgMfaMethod ?? userMfaMethod;
|
||||||
|
|
||||||
|
if (shouldCheckMfa && (!decodedToken.isMfaVerified || decodedToken.mfaMethod !== mfaMethod)) {
|
||||||
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
||||||
|
|
||||||
const mfaToken = jwt.sign(
|
const mfaToken = jwt.sign(
|
||||||
@ -369,12 +380,14 @@ export const authLoginServiceFactory = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
await sendUserMfaCode({
|
if (mfaMethod === MfaMethod.EMAIL && user.email) {
|
||||||
userId: user.id,
|
await sendUserMfaCode({
|
||||||
email: user.email
|
userId: user.id,
|
||||||
});
|
email: user.email
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return { isMfaEnabled: true, mfa: mfaToken } as const;
|
return { isMfaEnabled: true, mfa: mfaToken, mfaMethod } as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokens = await generateUserTokens({
|
const tokens = await generateUserTokens({
|
||||||
@ -383,7 +396,8 @@ export const authLoginServiceFactory = ({
|
|||||||
userAgent,
|
userAgent,
|
||||||
ip: ipAddress,
|
ip: ipAddress,
|
||||||
organizationId,
|
organizationId,
|
||||||
isMfaVerified: decodedToken.isMfaVerified
|
isMfaVerified: decodedToken.isMfaVerified,
|
||||||
|
mfaMethod: decodedToken.mfaMethod
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -458,17 +472,39 @@ export const authLoginServiceFactory = ({
|
|||||||
* Multi factor authentication verification of code
|
* Multi factor authentication verification of code
|
||||||
* Third step of login in which user completes with mfa
|
* Third step of login in which user completes with mfa
|
||||||
* */
|
* */
|
||||||
const verifyMfaToken = async ({ userId, mfaToken, mfaJwtToken, ip, userAgent, orgId }: TVerifyMfaTokenDTO) => {
|
const verifyMfaToken = async ({
|
||||||
|
userId,
|
||||||
|
mfaToken,
|
||||||
|
mfaMethod,
|
||||||
|
mfaJwtToken,
|
||||||
|
ip,
|
||||||
|
userAgent,
|
||||||
|
orgId
|
||||||
|
}: TVerifyMfaTokenDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const user = await userDAL.findById(userId);
|
const user = await userDAL.findById(userId);
|
||||||
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await tokenService.validateTokenForUser({
|
if (mfaMethod === MfaMethod.EMAIL) {
|
||||||
type: TokenType.TOKEN_EMAIL_MFA,
|
await tokenService.validateTokenForUser({
|
||||||
userId,
|
type: TokenType.TOKEN_EMAIL_MFA,
|
||||||
code: mfaToken
|
userId,
|
||||||
});
|
code: mfaToken
|
||||||
|
});
|
||||||
|
} else if (mfaMethod === MfaMethod.TOTP) {
|
||||||
|
if (mfaToken.length === 6) {
|
||||||
|
await totpService.verifyUserTotp({
|
||||||
|
userId,
|
||||||
|
totp: mfaToken
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await totpService.verifyWithUserRecoveryCode({
|
||||||
|
userId,
|
||||||
|
recoveryCode: mfaToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const updatedUser = await processFailedMfaAttempt(userId);
|
const updatedUser = await processFailedMfaAttempt(userId);
|
||||||
if (updatedUser.isLocked) {
|
if (updatedUser.isLocked) {
|
||||||
@ -513,7 +549,8 @@ export const authLoginServiceFactory = ({
|
|||||||
userAgent,
|
userAgent,
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
authMethod: decodedToken.authMethod,
|
authMethod: decodedToken.authMethod,
|
||||||
isMfaVerified: true
|
isMfaVerified: true,
|
||||||
|
mfaMethod
|
||||||
});
|
});
|
||||||
|
|
||||||
return { token, user: userEnc };
|
return { token, user: userEnc };
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { AuthMethod } from "./auth-type";
|
import { AuthMethod, MfaMethod } from "./auth-type";
|
||||||
|
|
||||||
export type TLoginGenServerPublicKeyDTO = {
|
export type TLoginGenServerPublicKeyDTO = {
|
||||||
email: string;
|
email: string;
|
||||||
@ -19,6 +19,7 @@ export type TLoginClientProofDTO = {
|
|||||||
export type TVerifyMfaTokenDTO = {
|
export type TVerifyMfaTokenDTO = {
|
||||||
userId: string;
|
userId: string;
|
||||||
mfaToken: string;
|
mfaToken: string;
|
||||||
|
mfaMethod: MfaMethod;
|
||||||
mfaJwtToken: string;
|
mfaJwtToken: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
|
@ -8,6 +8,7 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
|||||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
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 { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
|
import { TTotpConfigDALFactory } from "../totp/totp-config-dal";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TAuthDALFactory } from "./auth-dal";
|
import { TAuthDALFactory } from "./auth-dal";
|
||||||
import { TChangePasswordDTO, TCreateBackupPrivateKeyDTO, TResetPasswordViaBackupKeyDTO } from "./auth-password-type";
|
import { TChangePasswordDTO, TCreateBackupPrivateKeyDTO, TResetPasswordViaBackupKeyDTO } from "./auth-password-type";
|
||||||
@ -18,6 +19,7 @@ type TAuthPasswordServiceFactoryDep = {
|
|||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
tokenService: TAuthTokenServiceFactory;
|
tokenService: TAuthTokenServiceFactory;
|
||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
|
totpConfigDAL: Pick<TTotpConfigDALFactory, "delete">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAuthPasswordFactory = ReturnType<typeof authPaswordServiceFactory>;
|
export type TAuthPasswordFactory = ReturnType<typeof authPaswordServiceFactory>;
|
||||||
@ -25,7 +27,8 @@ export const authPaswordServiceFactory = ({
|
|||||||
authDAL,
|
authDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
tokenService,
|
tokenService,
|
||||||
smtpService
|
smtpService,
|
||||||
|
totpConfigDAL
|
||||||
}: TAuthPasswordServiceFactoryDep) => {
|
}: TAuthPasswordServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
* Pre setup for pass change with srp protocol
|
* Pre setup for pass change with srp protocol
|
||||||
@ -185,6 +188,12 @@ export const authPaswordServiceFactory = ({
|
|||||||
temporaryLockDateEnd: null,
|
temporaryLockDateEnd: null,
|
||||||
consecutiveFailedMfaAttempts: 0
|
consecutiveFailedMfaAttempts: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* we reset the mobile authenticator configs of the user
|
||||||
|
because we want this to be one of the recovery modes from account lockout */
|
||||||
|
await totpConfigDAL.delete({
|
||||||
|
userId
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -53,6 +53,7 @@ export type AuthModeJwtTokenPayload = {
|
|||||||
accessVersion: number;
|
accessVersion: number;
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
isMfaVerified?: boolean;
|
isMfaVerified?: boolean;
|
||||||
|
mfaMethod?: MfaMethod;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AuthModeMfaJwtTokenPayload = {
|
export type AuthModeMfaJwtTokenPayload = {
|
||||||
@ -71,6 +72,7 @@ export type AuthModeRefreshJwtTokenPayload = {
|
|||||||
refreshVersion: number;
|
refreshVersion: number;
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
isMfaVerified?: boolean;
|
isMfaVerified?: boolean;
|
||||||
|
mfaMethod?: MfaMethod;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AuthModeProviderJwtTokenPayload = {
|
export type AuthModeProviderJwtTokenPayload = {
|
||||||
@ -85,3 +87,8 @@ export type AuthModeProviderSignUpTokenPayload = {
|
|||||||
authTokenType: AuthTokenType.SIGNUP_TOKEN;
|
authTokenType: AuthTokenType.SIGNUP_TOKEN;
|
||||||
userId: string;
|
userId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum MfaMethod {
|
||||||
|
EMAIL = "email",
|
||||||
|
TOTP = "totp"
|
||||||
|
}
|
||||||
|
@ -268,7 +268,7 @@ export const orgServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
orgId,
|
orgId,
|
||||||
data: { name, slug, authEnforced, scimEnabled, defaultMembershipRoleSlug, enforceMfa }
|
data: { name, slug, authEnforced, scimEnabled, defaultMembershipRoleSlug, enforceMfa, selectedMfaMethod }
|
||||||
}: TUpdateOrgDTO) => {
|
}: TUpdateOrgDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
@ -333,7 +333,8 @@ export const orgServiceFactory = ({
|
|||||||
authEnforced,
|
authEnforced,
|
||||||
scimEnabled,
|
scimEnabled,
|
||||||
defaultMembershipRole,
|
defaultMembershipRole,
|
||||||
enforceMfa
|
enforceMfa,
|
||||||
|
selectedMfaMethod
|
||||||
});
|
});
|
||||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||||
return org;
|
return org;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { TOrgPermission } from "@app/lib/types";
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType, MfaMethod } from "../auth/auth-type";
|
||||||
|
|
||||||
export type TUpdateOrgMembershipDTO = {
|
export type TUpdateOrgMembershipDTO = {
|
||||||
userId: string;
|
userId: string;
|
||||||
@ -65,6 +65,7 @@ export type TUpdateOrgDTO = {
|
|||||||
scimEnabled: boolean;
|
scimEnabled: boolean;
|
||||||
defaultMembershipRoleSlug: string;
|
defaultMembershipRoleSlug: string;
|
||||||
enforceMfa: boolean;
|
enforceMfa: boolean;
|
||||||
|
selectedMfaMethod: MfaMethod;
|
||||||
}>;
|
}>;
|
||||||
} & TOrgPermission;
|
} & TOrgPermission;
|
||||||
|
|
||||||
|
11
backend/src/services/totp/totp-config-dal.ts
Normal file
11
backend/src/services/totp/totp-config-dal.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TTotpConfigDALFactory = ReturnType<typeof totpConfigDALFactory>;
|
||||||
|
|
||||||
|
export const totpConfigDALFactory = (db: TDbClient) => {
|
||||||
|
const totpConfigDal = ormify(db, TableName.TotpConfig);
|
||||||
|
|
||||||
|
return totpConfigDal;
|
||||||
|
};
|
3
backend/src/services/totp/totp-fns.ts
Normal file
3
backend/src/services/totp/totp-fns.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
export const generateRecoveryCode = () => String(crypto.randomInt(10 ** 7, 10 ** 8 - 1));
|
270
backend/src/services/totp/totp-service.ts
Normal file
270
backend/src/services/totp/totp-service.ts
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
import { authenticator } from "otplib";
|
||||||
|
|
||||||
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
|
import { TTotpConfigDALFactory } from "./totp-config-dal";
|
||||||
|
import { generateRecoveryCode } from "./totp-fns";
|
||||||
|
import {
|
||||||
|
TCreateUserTotpRecoveryCodesDTO,
|
||||||
|
TDeleteUserTotpConfigDTO,
|
||||||
|
TGetUserTotpConfigDTO,
|
||||||
|
TRegisterUserTotpDTO,
|
||||||
|
TVerifyUserTotpConfigDTO,
|
||||||
|
TVerifyUserTotpDTO,
|
||||||
|
TVerifyWithUserRecoveryCodeDTO
|
||||||
|
} from "./totp-types";
|
||||||
|
|
||||||
|
type TTotpServiceFactoryDep = {
|
||||||
|
userDAL: TUserDALFactory;
|
||||||
|
totpConfigDAL: TTotpConfigDALFactory;
|
||||||
|
kmsService: TKmsServiceFactory;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TTotpServiceFactory = ReturnType<typeof totpServiceFactory>;
|
||||||
|
|
||||||
|
const MAX_RECOVERY_CODE_LIMIT = 10;
|
||||||
|
|
||||||
|
export const totpServiceFactory = ({ totpConfigDAL, kmsService, userDAL }: TTotpServiceFactoryDep) => {
|
||||||
|
const getUserTotpConfig = async ({ userId }: TGetUserTotpConfigDTO) => {
|
||||||
|
const totpConfig = await totpConfigDAL.findOne({
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!totpConfig) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "TOTP configuration not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!totpConfig.isVerified) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "TOTP configuration has not been verified"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const recoveryCodes = decryptWithRoot(totpConfig.encryptedRecoveryCodes).toString().split(",");
|
||||||
|
|
||||||
|
return {
|
||||||
|
isVerified: totpConfig.isVerified,
|
||||||
|
recoveryCodes
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerUserTotp = async ({ userId }: TRegisterUserTotpDTO) => {
|
||||||
|
const totpConfig = await totpConfigDAL.transaction(async (tx) => {
|
||||||
|
const verifiedTotpConfig = await totpConfigDAL.findOne(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
isVerified: true
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (verifiedTotpConfig) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "TOTP configuration for user already exists"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const unverifiedTotpConfig = await totpConfigDAL.findOne({
|
||||||
|
userId,
|
||||||
|
isVerified: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (unverifiedTotpConfig) {
|
||||||
|
return unverifiedTotpConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||||
|
|
||||||
|
// create new TOTP configuration
|
||||||
|
const secret = authenticator.generateSecret();
|
||||||
|
const encryptedSecret = encryptWithRoot(Buffer.from(secret));
|
||||||
|
const recoveryCodes = Array.from({ length: MAX_RECOVERY_CODE_LIMIT }).map(generateRecoveryCode);
|
||||||
|
const encryptedRecoveryCodes = encryptWithRoot(Buffer.from(recoveryCodes.join(",")));
|
||||||
|
const newTotpConfig = await totpConfigDAL.create({
|
||||||
|
userId,
|
||||||
|
encryptedRecoveryCodes,
|
||||||
|
encryptedSecret
|
||||||
|
});
|
||||||
|
|
||||||
|
return newTotpConfig;
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await userDAL.findById(userId);
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
|
||||||
|
const secret = decryptWithRoot(totpConfig.encryptedSecret).toString();
|
||||||
|
const recoveryCodes = decryptWithRoot(totpConfig.encryptedRecoveryCodes).toString().split(",");
|
||||||
|
const otpUrl = authenticator.keyuri(user.username, "Infisical", secret);
|
||||||
|
|
||||||
|
return {
|
||||||
|
otpUrl,
|
||||||
|
recoveryCodes
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyUserTotpConfig = async ({ userId, totp }: TVerifyUserTotpConfigDTO) => {
|
||||||
|
const totpConfig = await totpConfigDAL.findOne({
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!totpConfig) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "TOTP configuration not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totpConfig.isVerified) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "TOTP configuration has already been verified"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const secret = decryptWithRoot(totpConfig.encryptedSecret).toString();
|
||||||
|
const isValid = authenticator.verify({
|
||||||
|
token: totp,
|
||||||
|
secret
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
await totpConfigDAL.updateById(totpConfig.id, {
|
||||||
|
isVerified: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid TOTP token"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyUserTotp = async ({ userId, totp }: TVerifyUserTotpDTO) => {
|
||||||
|
const totpConfig = await totpConfigDAL.findOne({
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!totpConfig) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "TOTP configuration not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!totpConfig.isVerified) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "TOTP configuration has not been verified"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const secret = decryptWithRoot(totpConfig.encryptedSecret).toString();
|
||||||
|
const isValid = authenticator.verify({
|
||||||
|
token: totp,
|
||||||
|
secret
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Invalid TOTP"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyWithUserRecoveryCode = async ({ userId, recoveryCode }: TVerifyWithUserRecoveryCodeDTO) => {
|
||||||
|
const totpConfig = await totpConfigDAL.findOne({
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!totpConfig) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "TOTP configuration not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!totpConfig.isVerified) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "TOTP configuration has not been verified"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||||
|
|
||||||
|
const recoveryCodes = decryptWithRoot(totpConfig.encryptedRecoveryCodes).toString().split(",");
|
||||||
|
const matchingCode = recoveryCodes.find((code) => recoveryCode === code);
|
||||||
|
if (!matchingCode) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Invalid TOTP recovery code"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedRecoveryCodes = recoveryCodes.filter((code) => code !== matchingCode);
|
||||||
|
const encryptedRecoveryCodes = encryptWithRoot(Buffer.from(updatedRecoveryCodes.join(",")));
|
||||||
|
await totpConfigDAL.updateById(totpConfig.id, {
|
||||||
|
encryptedRecoveryCodes
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUserTotpConfig = async ({ userId }: TDeleteUserTotpConfigDTO) => {
|
||||||
|
const totpConfig = await totpConfigDAL.findOne({
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!totpConfig) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "TOTP configuration not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await totpConfigDAL.deleteById(totpConfig.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUserTotpRecoveryCodes = async ({ userId }: TCreateUserTotpRecoveryCodesDTO) => {
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||||
|
|
||||||
|
return totpConfigDAL.transaction(async (tx) => {
|
||||||
|
const totpConfig = await totpConfigDAL.findOne(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
isVerified: true
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!totpConfig) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "Valid TOTP configuration not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const recoveryCodes = decryptWithRoot(totpConfig.encryptedRecoveryCodes).toString().split(",");
|
||||||
|
if (recoveryCodes.length >= MAX_RECOVERY_CODE_LIMIT) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Cannot have more than ${MAX_RECOVERY_CODE_LIMIT} recovery codes at a time`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const toGenerateCount = MAX_RECOVERY_CODE_LIMIT - recoveryCodes.length;
|
||||||
|
const newRecoveryCodes = Array.from({ length: toGenerateCount }).map(generateRecoveryCode);
|
||||||
|
const encryptedRecoveryCodes = encryptWithRoot(Buffer.from([...recoveryCodes, ...newRecoveryCodes].join(",")));
|
||||||
|
|
||||||
|
await totpConfigDAL.updateById(totpConfig.id, {
|
||||||
|
encryptedRecoveryCodes
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
registerUserTotp,
|
||||||
|
verifyUserTotpConfig,
|
||||||
|
getUserTotpConfig,
|
||||||
|
verifyUserTotp,
|
||||||
|
verifyWithUserRecoveryCode,
|
||||||
|
deleteUserTotpConfig,
|
||||||
|
createUserTotpRecoveryCodes
|
||||||
|
};
|
||||||
|
};
|
30
backend/src/services/totp/totp-types.ts
Normal file
30
backend/src/services/totp/totp-types.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
export type TRegisterUserTotpDTO = {
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TVerifyUserTotpConfigDTO = {
|
||||||
|
userId: string;
|
||||||
|
totp: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetUserTotpConfigDTO = {
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TVerifyUserTotpDTO = {
|
||||||
|
userId: string;
|
||||||
|
totp: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TVerifyWithUserRecoveryCodeDTO = {
|
||||||
|
userId: string;
|
||||||
|
recoveryCode: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteUserTotpConfigDTO = {
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateUserTotpRecoveryCodesDTO = {
|
||||||
|
userId: string;
|
||||||
|
};
|
@ -15,7 +15,7 @@ import { AuthMethod } from "../auth/auth-type";
|
|||||||
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
|
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
import { TUserDALFactory } from "./user-dal";
|
import { TUserDALFactory } from "./user-dal";
|
||||||
import { TListUserGroupsDTO } from "./user-types";
|
import { TListUserGroupsDTO, TUpdateUserMfaDTO } from "./user-types";
|
||||||
|
|
||||||
type TUserServiceFactoryDep = {
|
type TUserServiceFactoryDep = {
|
||||||
userDAL: Pick<
|
userDAL: Pick<
|
||||||
@ -171,15 +171,24 @@ export const userServiceFactory = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleUserMfa = async (userId: string, isMfaEnabled: boolean) => {
|
const updateUserMfa = async ({ userId, isMfaEnabled, selectedMfaMethod }: TUpdateUserMfaDTO) => {
|
||||||
const user = await userDAL.findById(userId);
|
const user = await userDAL.findById(userId);
|
||||||
|
|
||||||
if (!user || !user.email) throw new BadRequestError({ name: "Failed to toggle MFA" });
|
if (!user || !user.email) throw new BadRequestError({ name: "Failed to toggle MFA" });
|
||||||
|
|
||||||
|
let mfaMethods;
|
||||||
|
if (isMfaEnabled === undefined) {
|
||||||
|
mfaMethods = undefined;
|
||||||
|
} else {
|
||||||
|
mfaMethods = isMfaEnabled ? ["email"] : [];
|
||||||
|
}
|
||||||
|
|
||||||
const updatedUser = await userDAL.updateById(userId, {
|
const updatedUser = await userDAL.updateById(userId, {
|
||||||
isMfaEnabled,
|
isMfaEnabled,
|
||||||
mfaMethods: isMfaEnabled ? ["email"] : []
|
mfaMethods,
|
||||||
|
selectedMfaMethod
|
||||||
});
|
});
|
||||||
|
|
||||||
return updatedUser;
|
return updatedUser;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -327,7 +336,7 @@ export const userServiceFactory = ({
|
|||||||
return {
|
return {
|
||||||
sendEmailVerificationCode,
|
sendEmailVerificationCode,
|
||||||
verifyEmailVerificationCode,
|
verifyEmailVerificationCode,
|
||||||
toggleUserMfa,
|
updateUserMfa,
|
||||||
updateUserName,
|
updateUserName,
|
||||||
updateAuthMethods,
|
updateAuthMethods,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { TOrgPermission } from "@app/lib/types";
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { MfaMethod } from "../auth/auth-type";
|
||||||
|
|
||||||
export type TListUserGroupsDTO = {
|
export type TListUserGroupsDTO = {
|
||||||
username: string;
|
username: string;
|
||||||
} & Omit<TOrgPermission, "orgId">;
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
@ -8,3 +10,9 @@ export enum UserEncryption {
|
|||||||
V1 = 1,
|
V1 = 1,
|
||||||
V2 = 2
|
V2 = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TUpdateUserMfaDTO = {
|
||||||
|
userId: string;
|
||||||
|
isMfaEnabled?: boolean;
|
||||||
|
selectedMfaMethod?: MfaMethod;
|
||||||
|
};
|
||||||
|
@ -138,6 +138,7 @@ type GetOrganizationsResponse struct {
|
|||||||
type SelectOrganizationResponse struct {
|
type SelectOrganizationResponse struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
MfaEnabled bool `json:"isMfaEnabled"`
|
MfaEnabled bool `json:"isMfaEnabled"`
|
||||||
|
MfaMethod string `json:"mfaMethod"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SelectOrganizationRequest struct {
|
type SelectOrganizationRequest struct {
|
||||||
@ -260,8 +261,9 @@ type GetLoginTwoV2Response struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type VerifyMfaTokenRequest struct {
|
type VerifyMfaTokenRequest struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
MFAToken string `json:"mfaToken"`
|
MFAToken string `json:"mfaToken"`
|
||||||
|
MFAMethod string `json:"mfaMethod"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type VerifyMfaTokenResponse struct {
|
type VerifyMfaTokenResponse struct {
|
||||||
|
@ -79,13 +79,14 @@ var initCmd = &cobra.Command{
|
|||||||
if tokenResponse.MfaEnabled {
|
if tokenResponse.MfaEnabled {
|
||||||
i := 1
|
i := 1
|
||||||
for i < 6 {
|
for i < 6 {
|
||||||
mfaVerifyCode := askForMFACode()
|
mfaVerifyCode := askForMFACode(tokenResponse.MfaMethod)
|
||||||
|
|
||||||
httpClient := resty.New()
|
httpClient := resty.New()
|
||||||
httpClient.SetAuthToken(tokenResponse.Token)
|
httpClient.SetAuthToken(tokenResponse.Token)
|
||||||
verifyMFAresponse, mfaErrorResponse, requestError := api.CallVerifyMfaToken(httpClient, api.VerifyMfaTokenRequest{
|
verifyMFAresponse, mfaErrorResponse, requestError := api.CallVerifyMfaToken(httpClient, api.VerifyMfaTokenRequest{
|
||||||
Email: userCreds.UserCredentials.Email,
|
Email: userCreds.UserCredentials.Email,
|
||||||
MFAToken: mfaVerifyCode,
|
MFAToken: mfaVerifyCode,
|
||||||
|
MFAMethod: tokenResponse.MfaMethod,
|
||||||
})
|
})
|
||||||
if requestError != nil {
|
if requestError != nil {
|
||||||
util.HandleError(err)
|
util.HandleError(err)
|
||||||
@ -99,7 +100,7 @@ var initCmd = &cobra.Command{
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if mfaErrorResponse.Context.Code == "mfa_expired" {
|
if mfaErrorResponse.Context.Code == "mfa_expired" {
|
||||||
util.PrintErrorMessageAndExit("Your 2FA verification code has expired, please try logging in again")
|
util.PrintErrorMessageAndExit("Your 2FA verification code has expired, please try logging in again")
|
||||||
break
|
break
|
||||||
|
@ -343,7 +343,7 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) {
|
|||||||
if loginTwoResponse.MfaEnabled {
|
if loginTwoResponse.MfaEnabled {
|
||||||
i := 1
|
i := 1
|
||||||
for i < 6 {
|
for i < 6 {
|
||||||
mfaVerifyCode := askForMFACode()
|
mfaVerifyCode := askForMFACode("email")
|
||||||
|
|
||||||
httpClient := resty.New()
|
httpClient := resty.New()
|
||||||
httpClient.SetAuthToken(loginTwoResponse.Token)
|
httpClient.SetAuthToken(loginTwoResponse.Token)
|
||||||
@ -756,13 +756,14 @@ func GetJwtTokenWithOrganizationId(oldJwtToken string, email string) string {
|
|||||||
if selectedOrgRes.MfaEnabled {
|
if selectedOrgRes.MfaEnabled {
|
||||||
i := 1
|
i := 1
|
||||||
for i < 6 {
|
for i < 6 {
|
||||||
mfaVerifyCode := askForMFACode()
|
mfaVerifyCode := askForMFACode(selectedOrgRes.MfaMethod)
|
||||||
|
|
||||||
httpClient := resty.New()
|
httpClient := resty.New()
|
||||||
httpClient.SetAuthToken(selectedOrgRes.Token)
|
httpClient.SetAuthToken(selectedOrgRes.Token)
|
||||||
verifyMFAresponse, mfaErrorResponse, requestError := api.CallVerifyMfaToken(httpClient, api.VerifyMfaTokenRequest{
|
verifyMFAresponse, mfaErrorResponse, requestError := api.CallVerifyMfaToken(httpClient, api.VerifyMfaTokenRequest{
|
||||||
Email: email,
|
Email: email,
|
||||||
MFAToken: mfaVerifyCode,
|
MFAToken: mfaVerifyCode,
|
||||||
|
MFAMethod: selectedOrgRes.MfaMethod,
|
||||||
})
|
})
|
||||||
if requestError != nil {
|
if requestError != nil {
|
||||||
util.HandleError(err)
|
util.HandleError(err)
|
||||||
@ -817,9 +818,15 @@ func generateFromPassword(password string, salt []byte, p *params) (hash []byte,
|
|||||||
return hash, nil
|
return hash, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func askForMFACode() string {
|
func askForMFACode(mfaMethod string) string {
|
||||||
|
var label string
|
||||||
|
if mfaMethod == "totp" {
|
||||||
|
label = "Enter the verification code from your mobile authenticator app or use a recovery code"
|
||||||
|
} else {
|
||||||
|
label = "Enter the 2FA verification code sent to your email"
|
||||||
|
}
|
||||||
mfaCodePromptUI := promptui.Prompt{
|
mfaCodePromptUI := promptui.Prompt{
|
||||||
Label: "Enter the 2FA verification code sent to your email",
|
Label: label,
|
||||||
}
|
}
|
||||||
|
|
||||||
mfaVerifyCode, err := mfaCodePromptUI.Run()
|
mfaVerifyCode, err := mfaCodePromptUI.Run()
|
||||||
|
@ -4,19 +4,18 @@ sidebarTitle: "MFA"
|
|||||||
description: "Learn how to secure your Infisical account with MFA."
|
description: "Learn how to secure your Infisical account with MFA."
|
||||||
---
|
---
|
||||||
|
|
||||||
MFA requires users to provide multiple forms of identification to access their account. Currently, this means logging in with your password and a 6-digit code sent to your email.
|
MFA requires users to provide multiple forms of identification to access their account.
|
||||||
|
|
||||||
## Email 2FA
|
## Email 2FA
|
||||||
|
|
||||||
Check the box in Personal Settings > Two-factor Authentication to enable email-based 2FA.
|
If 2-factor authentication is enabled in the Personal settings page, email will be used for MFA by default.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
<Note>
|
## Mobile Authenticator 2FA
|
||||||
Infisical currently supports email-based 2FA. We're actively working on
|
|
||||||
building support for other forms of identification via SMS and Authenticator
|
You can use any mobile authenticator app (Authy, Google Authenticator, Duo, etc.) to secure your account. After registration with an authenticator, select **Mobile Authenticator** as your 2FA method.
|
||||||
App.
|

|
||||||
</Note>
|
|
||||||
|
|
||||||
## Entra ID / Azure AD MFA
|
## Entra ID / Azure AD MFA
|
||||||
|
|
||||||
@ -25,32 +24,39 @@ Check the box in Personal Settings > Two-factor Authentication to enable email-b
|
|||||||
|
|
||||||
We also encourage you to have your team download and setup the
|
We also encourage you to have your team download and setup the
|
||||||
[Microsoft Authenticator App](https://www.microsoft.com/en-us/security/mobile-authenticator-app) prior to enabling MFA.
|
[Microsoft Authenticator App](https://www.microsoft.com/en-us/security/mobile-authenticator-app) prior to enabling MFA.
|
||||||
|
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Open your Infisical Application in the Microsoft Entra Admin Center">
|
<Step title="Open your Infisical Application in the Microsoft Entra Admin Center">
|
||||||

|

|
||||||
<Step title="Tap on Conditional Access under the Security Tab">
|
</Step>
|
||||||

|
<Step title="Tap on Conditional Access under the Security Tab">
|
||||||
</Step>
|

|
||||||

|
</Step>
|
||||||
</Step>
|
<Step title="Tap on Create New Policy from Templates">
|
||||||
<Step title="Select Require MFA for All Users and Tap on Review + Create">
|

|
||||||

|
</Step>
|
||||||
<Note>
|
<Step title="Select Require MFA for All Users and Tap on Review + Create">
|
||||||
By default all users except the configuring admin will be setup to require MFA.
|

|
||||||
</Note>
|
<Note>
|
||||||
</Step>
|
By default all users except the configuring admin will be setup to require
|
||||||
<Step title="Set Policy State to Enabled and Tap on Create">
|
MFA. Microsoft encourages keeping at least one admin excluded from MFA to
|
||||||

|
prevent accidental lockout.
|
||||||
</Step>
|
</Note>
|
||||||
<Step title="MFA is now Required When Accessing Infisical">
|
</Step>
|
||||||

|
<Step title="Set Policy State to Enabled and Tap on Create">
|
||||||
<Note>
|

|
||||||
</Note>
|
</Step>
|
||||||
</Step>
|
<Step title="MFA is now Required When Accessing Infisical">
|
||||||
</Steps>
|

|
||||||
|
<Note>
|
||||||
|
If users have not setup MFA for Entra / Azure they will be prompted to do
|
||||||
|
so at this time.
|
||||||
|
</Note>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
BIN
docs/images/mfa-authenticator.png
Normal file
BIN
docs/images/mfa-authenticator.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 557 KiB |
Binary file not shown.
Before Width: | Height: | Size: 236 KiB After Width: | Height: | Size: 555 KiB |
199
frontend/package-lock.json
generated
199
frontend/package-lock.json
generated
@ -75,6 +75,7 @@
|
|||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"picomatch": "^2.3.1",
|
"picomatch": "^2.3.1",
|
||||||
"posthog-js": "^1.105.6",
|
"posthog-js": "^1.105.6",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"query-string": "^7.1.3",
|
"query-string": "^7.1.3",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
@ -120,6 +121,7 @@
|
|||||||
"@types/jsrp": "^0.2.4",
|
"@types/jsrp": "^0.2.4",
|
||||||
"@types/node": "^18.11.9",
|
"@types/node": "^18.11.9",
|
||||||
"@types/picomatch": "^2.3.0",
|
"@types/picomatch": "^2.3.0",
|
||||||
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
"@types/sanitize-html": "^2.9.0",
|
"@types/sanitize-html": "^2.9.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||||
@ -8857,6 +8859,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
|
||||||
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
|
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/qrcode": {
|
||||||
|
"version": "1.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz",
|
||||||
|
"integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.9.11",
|
"version": "6.9.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz",
|
||||||
@ -9785,7 +9796,6 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -9794,7 +9804,6 @@
|
|||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color-convert": "^2.0.1"
|
"color-convert": "^2.0.1"
|
||||||
},
|
},
|
||||||
@ -11076,7 +11085,6 @@
|
|||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@ -11376,6 +11384,29 @@
|
|||||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
|
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/cliui": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.0",
|
||||||
|
"wrap-ansi": "^6.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cliui/node_modules/wrap-ansi": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/clone": {
|
"node_modules/clone": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
|
||||||
@ -12281,6 +12312,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/decamelize": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/decode-named-character-reference": {
|
"node_modules/decode-named-character-reference": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz",
|
||||||
@ -12677,6 +12716,11 @@
|
|||||||
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
|
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/dijkstrajs": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="
|
||||||
|
},
|
||||||
"node_modules/dir-glob": {
|
"node_modules/dir-glob": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||||
@ -14943,6 +14987,14 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-caller-file": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||||
|
"engines": {
|
||||||
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-intrinsic": {
|
"node_modules/get-intrinsic": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||||
@ -16212,7 +16264,6 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -19339,7 +19390,6 @@
|
|||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@ -19445,7 +19495,6 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -19666,6 +19715,14 @@
|
|||||||
"pathe": "^1.1.2"
|
"pathe": "^1.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pngjs": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pnp-webpack-plugin": {
|
"node_modules/pnp-webpack-plugin": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.7.0.tgz",
|
||||||
@ -20550,6 +20607,22 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qrcode": {
|
||||||
|
"version": "1.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||||
|
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||||
|
"dependencies": {
|
||||||
|
"dijkstrajs": "^1.0.1",
|
||||||
|
"pngjs": "^5.0.0",
|
||||||
|
"yargs": "^15.3.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"qrcode": "bin/qrcode"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.13.0",
|
"version": "6.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||||
@ -21846,6 +21919,14 @@
|
|||||||
"throttleit": "^1.0.0"
|
"throttleit": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-directory": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/require-from-string": {
|
"node_modules/require-from-string": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
@ -21855,6 +21936,11 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-main-filename": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
|
||||||
|
},
|
||||||
"node_modules/requireindex": {
|
"node_modules/requireindex": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
|
||||||
@ -22314,6 +22400,11 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-blocking": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
|
||||||
|
},
|
||||||
"node_modules/set-cookie-parser": {
|
"node_modules/set-cookie-parser": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
|
||||||
@ -22900,7 +22991,6 @@
|
|||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"emoji-regex": "^8.0.0",
|
"emoji-regex": "^8.0.0",
|
||||||
"is-fullwidth-code-point": "^3.0.0",
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
@ -22934,8 +23024,7 @@
|
|||||||
"node_modules/string-width/node_modules/emoji-regex": {
|
"node_modules/string-width/node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/string.prototype.matchall": {
|
"node_modules/string.prototype.matchall": {
|
||||||
"version": "4.0.10",
|
"version": "4.0.10",
|
||||||
@ -23006,7 +23095,6 @@
|
|||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^5.0.1"
|
"ansi-regex": "^5.0.1"
|
||||||
},
|
},
|
||||||
@ -24902,6 +24990,11 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/which-module": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="
|
||||||
|
},
|
||||||
"node_modules/which-typed-array": {
|
"node_modules/which-typed-array": {
|
||||||
"version": "1.1.13",
|
"version": "1.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
|
||||||
@ -25066,6 +25159,11 @@
|
|||||||
"node": ">=0.4"
|
"node": ">=0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/y18n": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
@ -25079,6 +25177,87 @@
|
|||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yargs": {
|
||||||
|
"version": "15.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||||
|
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^6.0.0",
|
||||||
|
"decamelize": "^1.2.0",
|
||||||
|
"find-up": "^4.1.0",
|
||||||
|
"get-caller-file": "^2.0.1",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"require-main-filename": "^2.0.0",
|
||||||
|
"set-blocking": "^2.0.0",
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"which-module": "^2.0.0",
|
||||||
|
"y18n": "^4.0.0",
|
||||||
|
"yargs-parser": "^18.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs-parser": {
|
||||||
|
"version": "18.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||||
|
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"camelcase": "^5.0.0",
|
||||||
|
"decamelize": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs/node_modules/find-up": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||||
|
"dependencies": {
|
||||||
|
"locate-path": "^5.0.0",
|
||||||
|
"path-exists": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs/node_modules/locate-path": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||||
|
"dependencies": {
|
||||||
|
"p-locate": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs/node_modules/p-limit": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||||
|
"dependencies": {
|
||||||
|
"p-try": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs/node_modules/p-locate": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||||
|
"dependencies": {
|
||||||
|
"p-limit": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yauzl": {
|
"node_modules/yauzl": {
|
||||||
"version": "2.10.0",
|
"version": "2.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||||
|
@ -88,6 +88,7 @@
|
|||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"picomatch": "^2.3.1",
|
"picomatch": "^2.3.1",
|
||||||
"posthog-js": "^1.105.6",
|
"posthog-js": "^1.105.6",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"query-string": "^7.1.3",
|
"query-string": "^7.1.3",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
@ -133,6 +134,7 @@
|
|||||||
"@types/jsrp": "^0.2.4",
|
"@types/jsrp": "^0.2.4",
|
||||||
"@types/node": "^18.11.9",
|
"@types/node": "^18.11.9",
|
||||||
"@types/picomatch": "^2.3.0",
|
"@types/picomatch": "^2.3.0",
|
||||||
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
"@types/sanitize-html": "^2.9.0",
|
"@types/sanitize-html": "^2.9.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||||
|
76
frontend/src/components/mfa/TotpRegistration.tsx
Normal file
76
frontend/src/components/mfa/TotpRegistration.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import QRCode from "qrcode";
|
||||||
|
|
||||||
|
import { useGetUserTotpRegistration } from "@app/hooks/api";
|
||||||
|
import { useVerifyUserTotpRegistration } from "@app/hooks/api/users/mutation";
|
||||||
|
|
||||||
|
import { createNotification } from "../notifications";
|
||||||
|
import { Button, ContentLoader, Input } from "../v2";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onComplete?: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TotpRegistration = ({ onComplete }: Props) => {
|
||||||
|
const { data: registration, isLoading } = useGetUserTotpRegistration();
|
||||||
|
const { mutateAsync: verifyUserTotp, isLoading: isVerifyLoading } =
|
||||||
|
useVerifyUserTotpRegistration();
|
||||||
|
const [qrCodeUrl, setQrCodeUrl] = useState("");
|
||||||
|
const [totp, setTotp] = useState("");
|
||||||
|
|
||||||
|
const handleTotpVerify = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
await verifyUserTotp({
|
||||||
|
totp
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully configured mobile authenticator",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onComplete) {
|
||||||
|
onComplete();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const generateQRCode = async () => {
|
||||||
|
if (registration?.otpUrl) {
|
||||||
|
const url = await QRCode.toDataURL(registration.otpUrl);
|
||||||
|
setQrCodeUrl(url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
generateQRCode();
|
||||||
|
}, [registration]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <ContentLoader />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="flex max-w-sm flex-col text-bunker-200">
|
||||||
|
<div className="mb-4 text-center">
|
||||||
|
Download a two-step verification app (Duo, Google Authenticator, etc.) and scan the QR code.
|
||||||
|
</div>
|
||||||
|
<div className="mb-10 flex items-center justify-center">
|
||||||
|
<img src={qrCodeUrl} alt="registration-qr" />
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handleTotpVerify}>
|
||||||
|
<div className="mb-4 text-center">Enter the resulting verification code</div>
|
||||||
|
<div className="mb-4 flex flex-row gap-2">
|
||||||
|
<Input
|
||||||
|
onChange={(e) => setTotp(e.target.value)}
|
||||||
|
value={totp}
|
||||||
|
placeholder="Verification code"
|
||||||
|
/>
|
||||||
|
<Button isLoading={isVerifyLoading} type="submit">
|
||||||
|
Enable MFA
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TotpRegistration;
|
@ -19,6 +19,7 @@ import {
|
|||||||
Login2Res,
|
Login2Res,
|
||||||
LoginLDAPDTO,
|
LoginLDAPDTO,
|
||||||
LoginLDAPRes,
|
LoginLDAPRes,
|
||||||
|
MfaMethod,
|
||||||
ResetPasswordDTO,
|
ResetPasswordDTO,
|
||||||
SendMfaTokenDTO,
|
SendMfaTokenDTO,
|
||||||
SRP1DTO,
|
SRP1DTO,
|
||||||
@ -65,10 +66,11 @@ export const selectOrganization = async (data: {
|
|||||||
organizationId: string;
|
organizationId: string;
|
||||||
userAgent?: UserAgentType;
|
userAgent?: UserAgentType;
|
||||||
}) => {
|
}) => {
|
||||||
const { data: res } = await apiRequest.post<{ token: string; isMfaEnabled: boolean }>(
|
const { data: res } = await apiRequest.post<{
|
||||||
"/api/v3/auth/select-organization",
|
token: string;
|
||||||
data
|
isMfaEnabled: boolean;
|
||||||
);
|
mfaMethod?: MfaMethod;
|
||||||
|
}>("/api/v3/auth/select-organization", data);
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -154,10 +156,19 @@ export const useSendMfaToken = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const verifyMfaToken = async ({ email, mfaCode }: { email: string; mfaCode: string }) => {
|
export const verifyMfaToken = async ({
|
||||||
|
email,
|
||||||
|
mfaCode,
|
||||||
|
mfaMethod
|
||||||
|
}: {
|
||||||
|
email: string;
|
||||||
|
mfaCode: string;
|
||||||
|
mfaMethod?: string;
|
||||||
|
}) => {
|
||||||
const { data } = await apiRequest.post("/api/v2/auth/mfa/verify", {
|
const { data } = await apiRequest.post("/api/v2/auth/mfa/verify", {
|
||||||
email,
|
email,
|
||||||
mfaToken: mfaCode
|
mfaToken: mfaCode,
|
||||||
|
mfaMethod
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@ -165,10 +176,11 @@ export const verifyMfaToken = async ({ email, mfaCode }: { email: string; mfaCod
|
|||||||
|
|
||||||
export const useVerifyMfaToken = () => {
|
export const useVerifyMfaToken = () => {
|
||||||
return useMutation<VerifyMfaTokenRes, {}, VerifyMfaTokenDTO>({
|
return useMutation<VerifyMfaTokenRes, {}, VerifyMfaTokenDTO>({
|
||||||
mutationFn: async ({ email, mfaCode }) => {
|
mutationFn: async ({ email, mfaCode, mfaMethod }) => {
|
||||||
return verifyMfaToken({
|
return verifyMfaToken({
|
||||||
email,
|
email,
|
||||||
mfaCode
|
mfaCode,
|
||||||
|
mfaMethod
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -302,3 +314,9 @@ export const useGetAuthToken = () =>
|
|||||||
onSuccess: (data) => setAuthToken(data.token),
|
onSuccess: (data) => setAuthToken(data.token),
|
||||||
retry: 0
|
retry: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const checkUserTotpMfa = async () => {
|
||||||
|
const { data } = await apiRequest.get<{ isVerified: boolean }>("/api/v2/auth/mfa/check/totp");
|
||||||
|
|
||||||
|
return data.isVerified;
|
||||||
|
};
|
||||||
|
@ -9,6 +9,7 @@ export type SendMfaTokenDTO = {
|
|||||||
export type VerifyMfaTokenDTO = {
|
export type VerifyMfaTokenDTO = {
|
||||||
email: string;
|
email: string;
|
||||||
mfaCode: string;
|
mfaCode: string;
|
||||||
|
mfaMethod: MfaMethod;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VerifyMfaTokenRes = {
|
export type VerifyMfaTokenRes = {
|
||||||
@ -149,3 +150,8 @@ export type GetBackupEncryptedPrivateKeyDTO = {
|
|||||||
export enum UserAgentType {
|
export enum UserAgentType {
|
||||||
CLI = "cli"
|
CLI = "cli"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum MfaMethod {
|
||||||
|
EMAIL = "email",
|
||||||
|
TOTP = "totp"
|
||||||
|
}
|
||||||
|
@ -91,7 +91,8 @@ export const useUpdateOrg = () => {
|
|||||||
slug,
|
slug,
|
||||||
orgId,
|
orgId,
|
||||||
defaultMembershipRoleSlug,
|
defaultMembershipRoleSlug,
|
||||||
enforceMfa
|
enforceMfa,
|
||||||
|
selectedMfaMethod
|
||||||
}) => {
|
}) => {
|
||||||
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
|
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
|
||||||
name,
|
name,
|
||||||
@ -99,7 +100,8 @@ export const useUpdateOrg = () => {
|
|||||||
scimEnabled,
|
scimEnabled,
|
||||||
slug,
|
slug,
|
||||||
defaultMembershipRoleSlug,
|
defaultMembershipRoleSlug,
|
||||||
enforceMfa
|
enforceMfa,
|
||||||
|
selectedMfaMethod
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
import { IdentityMembershipOrg } from "@app/hooks/api/identities/types";
|
import { IdentityMembershipOrg } from "@app/hooks/api/identities/types";
|
||||||
|
|
||||||
|
import { MfaMethod } from "../auth/types";
|
||||||
|
|
||||||
export type Organization = {
|
export type Organization = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -12,6 +14,7 @@ export type Organization = {
|
|||||||
slug: string;
|
slug: string;
|
||||||
defaultMembershipRole: string;
|
defaultMembershipRole: string;
|
||||||
enforceMfa: boolean;
|
enforceMfa: boolean;
|
||||||
|
selectedMfaMethod?: MfaMethod;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateOrgDTO = {
|
export type UpdateOrgDTO = {
|
||||||
@ -22,6 +25,7 @@ export type UpdateOrgDTO = {
|
|||||||
slug?: string;
|
slug?: string;
|
||||||
defaultMembershipRoleSlug?: string;
|
defaultMembershipRoleSlug?: string;
|
||||||
enforceMfa?: boolean;
|
enforceMfa?: boolean;
|
||||||
|
selectedMfaMethod?: MfaMethod;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BillingDetails = {
|
export type BillingDetails = {
|
||||||
|
@ -21,12 +21,13 @@ export {
|
|||||||
useGetOrgUsers,
|
useGetOrgUsers,
|
||||||
useGetUser,
|
useGetUser,
|
||||||
useGetUserAction,
|
useGetUserAction,
|
||||||
|
useGetUserTotpRegistration,
|
||||||
useListUserGroupMemberships,
|
useListUserGroupMemberships,
|
||||||
useLogoutUser,
|
useLogoutUser,
|
||||||
useRegisterUserAction,
|
useRegisterUserAction,
|
||||||
useRevokeMySessions,
|
useRevokeMySessions,
|
||||||
useUpdateMfaEnabled,
|
|
||||||
useUpdateOrgMembership,
|
useUpdateOrgMembership,
|
||||||
useUpdateUserAuthMethods
|
useUpdateUserAuthMethods,
|
||||||
|
useUpdateUserMfa
|
||||||
} from "./queries";
|
} from "./queries";
|
||||||
export { userKeys } from "./query-keys";
|
export { userKeys } from "./query-keys";
|
||||||
|
@ -114,3 +114,43 @@ export const useUpdateUserProjectFavorites = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useVerifyUserTotpRegistration = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ totp }: { totp: string }) => {
|
||||||
|
await apiRequest.post("/api/v1/user/me/totp/verify", {
|
||||||
|
totp
|
||||||
|
});
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteUserTotpConfiguration = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
await apiRequest.delete("/api/v1/user/me/totp");
|
||||||
|
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(userKeys.totpConfiguration);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateNewTotpRecoveryCodes = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
await apiRequest.post("/api/v1/user/me/totp/recovery-codes");
|
||||||
|
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(userKeys.totpConfiguration);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
import { SessionStorageKeys } from "@app/const";
|
import { SessionStorageKeys } from "@app/const";
|
||||||
import { setAuthToken } from "@app/reactQuery";
|
import { setAuthToken } from "@app/reactQuery";
|
||||||
|
|
||||||
import { APIKeyDataV2 } from "../apiKeys/types";
|
import { APIKeyDataV2 } from "../apiKeys/types";
|
||||||
|
import { MfaMethod } from "../auth/types";
|
||||||
import { TGroupWithProjectMemberships } from "../groups/types";
|
import { TGroupWithProjectMemberships } from "../groups/types";
|
||||||
import { workspaceKeys } from "../workspace";
|
import { workspaceKeys } from "../workspace";
|
||||||
import { userKeys } from "./query-keys";
|
import { userKeys } from "./query-keys";
|
||||||
@ -390,14 +392,21 @@ export const useRevokeMySessions = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateMfaEnabled = () => {
|
export const useUpdateUserMfa = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ isMfaEnabled }: { isMfaEnabled: boolean }) => {
|
mutationFn: async ({
|
||||||
|
isMfaEnabled,
|
||||||
|
selectedMfaMethod
|
||||||
|
}: {
|
||||||
|
isMfaEnabled?: boolean;
|
||||||
|
selectedMfaMethod?: MfaMethod;
|
||||||
|
}) => {
|
||||||
const {
|
const {
|
||||||
data: { user }
|
data: { user }
|
||||||
} = await apiRequest.patch("/api/v2/users/me/mfa", {
|
} = await apiRequest.patch("/api/v2/users/me/mfa", {
|
||||||
isMfaEnabled
|
isMfaEnabled,
|
||||||
|
selectedMfaMethod
|
||||||
});
|
});
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
@ -446,3 +455,39 @@ export const useListUserGroupMemberships = (username: string) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetUserTotpRegistration = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: userKeys.totpRegistration,
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.post<{ otpUrl: string; recoveryCodes: string[] }>(
|
||||||
|
"/api/v1/user/me/totp/register"
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetUserTotpConfiguration = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: userKeys.totpConfiguration,
|
||||||
|
queryFn: async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await apiRequest.get<{ isVerified: boolean; recoveryCodes: string[] }>(
|
||||||
|
"/api/v1/user/me/totp"
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AxiosError && [404, 400].includes(error.response?.data?.statusCode)) {
|
||||||
|
return {
|
||||||
|
isVerified: false,
|
||||||
|
recoveryCodes: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -16,6 +16,8 @@ export const userKeys = {
|
|||||||
myAPIKeysV2: ["api-keys-v2"] as const,
|
myAPIKeysV2: ["api-keys-v2"] as const,
|
||||||
mySessions: ["sessions"] as const,
|
mySessions: ["sessions"] as const,
|
||||||
listUsers: ["user-list"] as const,
|
listUsers: ["user-list"] as const,
|
||||||
|
totpRegistration: ["totp-registration"],
|
||||||
|
totpConfiguration: ["totp-configuration"],
|
||||||
listUserGroupMemberships: (username: string) => [{ username }, "user-group-memberships"] as const,
|
listUserGroupMemberships: (username: string) => [{ username }, "user-group-memberships"] as const,
|
||||||
myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const
|
myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { MfaMethod } from "../auth/types";
|
||||||
import { UserWsKeyPair } from "../keys/types";
|
import { UserWsKeyPair } from "../keys/types";
|
||||||
import { ProjectUserMembershipTemporaryMode } from "../workspace/types";
|
import { ProjectUserMembershipTemporaryMode } from "../workspace/types";
|
||||||
|
|
||||||
@ -26,6 +27,7 @@ export type User = {
|
|||||||
authProvider?: AuthMethod;
|
authProvider?: AuthMethod;
|
||||||
authMethods: AuthMethod[];
|
authMethods: AuthMethod[];
|
||||||
isMfaEnabled: boolean;
|
isMfaEnabled: boolean;
|
||||||
|
selectedMfaMethod?: MfaMethod;
|
||||||
seenIps: string[];
|
seenIps: string[];
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
@ -78,6 +78,7 @@ import {
|
|||||||
useLogoutUser,
|
useLogoutUser,
|
||||||
useSelectOrganization
|
useSelectOrganization
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
|
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||||
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
||||||
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
|
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
|
||||||
import { Workspace } from "@app/hooks/api/types";
|
import { Workspace } from "@app/hooks/api/types";
|
||||||
@ -143,6 +144,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!);
|
const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!);
|
||||||
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
|
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
|
||||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||||
|
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||||
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||||
|
|
||||||
const workspacesWithFaveProp = useMemo(
|
const workspacesWithFaveProp = useMemo(
|
||||||
@ -214,12 +216,15 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const changeOrg = async (orgId: string) => {
|
const changeOrg = async (orgId: string) => {
|
||||||
const { token, isMfaEnabled } = await selectOrganization({
|
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
|
||||||
organizationId: orgId
|
organizationId: orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isMfaEnabled) {
|
if (isMfaEnabled) {
|
||||||
SecurityClient.setMfaToken(token);
|
SecurityClient.setMfaToken(token);
|
||||||
|
if (mfaMethod) {
|
||||||
|
setRequiredMfaMethod(mfaMethod);
|
||||||
|
}
|
||||||
toggleShowMfa.on();
|
toggleShowMfa.on();
|
||||||
setMfaSuccessCallback(() => () => changeOrg(orgId));
|
setMfaSuccessCallback(() => () => changeOrg(orgId));
|
||||||
return;
|
return;
|
||||||
@ -365,6 +370,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
|
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
|
||||||
<Mfa
|
<Mfa
|
||||||
email={user.email as string}
|
email={user.email as string}
|
||||||
|
method={requiredMfaMethod}
|
||||||
successCallback={mfaSuccessCallback}
|
successCallback={mfaSuccessCallback}
|
||||||
closeMfa={() => toggleShowMfa.off()}
|
closeMfa={() => toggleShowMfa.off()}
|
||||||
/>
|
/>
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
useLogoutUser,
|
useLogoutUser,
|
||||||
useSelectOrganization
|
useSelectOrganization
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { UserAgentType } from "@app/hooks/api/auth/types";
|
import { MfaMethod, UserAgentType } from "@app/hooks/api/auth/types";
|
||||||
import { Organization } from "@app/hooks/api/types";
|
import { Organization } from "@app/hooks/api/types";
|
||||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||||
import { getAuthToken, isLoggedIn } from "@app/reactQuery";
|
import { getAuthToken, isLoggedIn } from "@app/reactQuery";
|
||||||
@ -46,6 +46,7 @@ export default function LoginPage() {
|
|||||||
const selectOrg = useSelectOrganization();
|
const selectOrg = useSelectOrganization();
|
||||||
const { data: user, isLoading: userLoading } = useGetUser();
|
const { data: user, isLoading: userLoading } = useGetUser();
|
||||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||||
|
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||||
const [isInitialOrgCheckLoading, setIsInitialOrgCheckLoading] = useState(true);
|
const [isInitialOrgCheckLoading, setIsInitialOrgCheckLoading] = useState(true);
|
||||||
|
|
||||||
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||||
@ -90,15 +91,19 @@ export default function LoginPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { token, isMfaEnabled } = await selectOrg.mutateAsync({
|
const { token, isMfaEnabled, mfaMethod } = await selectOrg
|
||||||
organizationId: organization.id,
|
.mutateAsync({
|
||||||
userAgent: callbackPort ? UserAgentType.CLI : undefined
|
organizationId: organization.id,
|
||||||
});
|
userAgent: callbackPort ? UserAgentType.CLI : undefined
|
||||||
|
})
|
||||||
|
.finally(() => setIsInitialOrgCheckLoading(false));
|
||||||
|
|
||||||
if (isMfaEnabled) {
|
if (isMfaEnabled) {
|
||||||
SecurityClient.setMfaToken(token);
|
SecurityClient.setMfaToken(token);
|
||||||
|
if (mfaMethod) {
|
||||||
|
setRequiredMfaMethod(mfaMethod);
|
||||||
|
}
|
||||||
toggleShowMfa.on();
|
toggleShowMfa.on();
|
||||||
|
|
||||||
setMfaSuccessCallback(() => () => handleSelectOrganization(organization));
|
setMfaSuccessCallback(() => () => handleSelectOrganization(organization));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -213,7 +218,11 @@ export default function LoginPage() {
|
|||||||
<meta name="og:description" content={t("login.og-description") ?? ""} />
|
<meta name="og:description" content={t("login.og-description") ?? ""} />
|
||||||
</Head>
|
</Head>
|
||||||
{shouldShowMfa ? (
|
{shouldShowMfa ? (
|
||||||
<Mfa email={user.email as string} successCallback={mfaSuccessCallback} />
|
<Mfa
|
||||||
|
email={user.email as string}
|
||||||
|
successCallback={mfaSuccessCallback}
|
||||||
|
method={requiredMfaMethod}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="mx-auto mt-20 w-fit rounded-lg border-2 border-mineshaft-500 p-10 shadow-lg">
|
<div className="mx-auto mt-20 w-fit rounded-lg border-2 border-mineshaft-500 p-10 shadow-lg">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
|
@ -29,8 +29,10 @@ import {
|
|||||||
useSelectOrganization,
|
useSelectOrganization,
|
||||||
verifySignupInvite
|
verifySignupInvite
|
||||||
} from "@app/hooks/api/auth/queries";
|
} from "@app/hooks/api/auth/queries";
|
||||||
|
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||||
import { navigateUserToOrg } from "@app/views/Login/Login.utils";
|
import { navigateUserToOrg } from "@app/views/Login/Login.utils";
|
||||||
|
import { Mfa } from "@app/views/Login/Mfa";
|
||||||
|
|
||||||
// eslint-disable-next-line new-cap
|
// eslint-disable-next-line new-cap
|
||||||
const client = new jsrp.client();
|
const client = new jsrp.client();
|
||||||
@ -59,6 +61,7 @@ export default function SignupInvite() {
|
|||||||
const [errors, setErrors] = useState<Errors>({});
|
const [errors, setErrors] = useState<Errors>({});
|
||||||
|
|
||||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||||
|
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||||
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const parsedUrl = queryString.parse(router.asPath.split("?")[1]);
|
const parsedUrl = queryString.parse(router.asPath.split("?")[1]);
|
||||||
@ -184,12 +187,19 @@ export default function SignupInvite() {
|
|||||||
if (!orgId) throw new Error("You are not part of any organization");
|
if (!orgId) throw new Error("You are not part of any organization");
|
||||||
|
|
||||||
const completeSignupFlow = async () => {
|
const completeSignupFlow = async () => {
|
||||||
const { token: mfaToken, isMfaEnabled } = await selectOrganization({
|
const {
|
||||||
|
token: mfaToken,
|
||||||
|
isMfaEnabled,
|
||||||
|
mfaMethod
|
||||||
|
} = await selectOrganization({
|
||||||
organizationId: orgId
|
organizationId: orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isMfaEnabled) {
|
if (isMfaEnabled) {
|
||||||
SecurityClient.setMfaToken(mfaToken);
|
SecurityClient.setMfaToken(mfaToken);
|
||||||
|
if (mfaMethod) {
|
||||||
|
setRequiredMfaMethod(mfaMethod);
|
||||||
|
}
|
||||||
toggleShowMfa.on();
|
toggleShowMfa.on();
|
||||||
setMfaSuccessCallback(() => completeSignupFlow);
|
setMfaSuccessCallback(() => completeSignupFlow);
|
||||||
return;
|
return;
|
||||||
@ -390,12 +400,23 @@ export default function SignupInvite() {
|
|||||||
<title>Sign Up</title>
|
<title>Sign Up</title>
|
||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel="icon" href="/infisical.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<Link href="/">
|
{shouldShowMfa ? (
|
||||||
<div className="mb-4 mt-20 flex justify-center">
|
<Mfa
|
||||||
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical Logo" />
|
email={email}
|
||||||
</div>
|
successCallback={mfaSuccessCallback}
|
||||||
</Link>
|
method={requiredMfaMethod}
|
||||||
{step === 1 ? stepConfirmEmail : step === 2 ? main : step4}
|
closeMfa={() => toggleShowMfa.off()}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Link href="/">
|
||||||
|
<div className="mb-4 mt-20 flex justify-center">
|
||||||
|
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical Logo" />
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
{step === 1 ? stepConfirmEmail : step === 2 ? main : step4}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import ReactCodeInput from "react-code-input";
|
import ReactCodeInput from "react-code-input";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@ -6,10 +6,12 @@ import { useRouter } from "next/router";
|
|||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
|
|
||||||
import Error from "@app/components/basic/Error";
|
import Error from "@app/components/basic/Error";
|
||||||
|
import TotpRegistration from "@app/components/mfa/TotpRegistration";
|
||||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
import { Button } from "@app/components/v2";
|
import { Button, Input } from "@app/components/v2";
|
||||||
import { useSendMfaToken } from "@app/hooks/api";
|
import { useSendMfaToken } from "@app/hooks/api";
|
||||||
import { verifyMfaToken } from "@app/hooks/api/auth/queries";
|
import { checkUserTotpMfa, verifyMfaToken } from "@app/hooks/api/auth/queries";
|
||||||
|
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||||
|
|
||||||
// The style for the verification code input
|
// The style for the verification code input
|
||||||
const codeInputProps = {
|
const codeInputProps = {
|
||||||
@ -36,23 +38,39 @@ type Props = {
|
|||||||
closeMfa?: () => void;
|
closeMfa?: () => void;
|
||||||
hideLogo?: boolean;
|
hideLogo?: boolean;
|
||||||
email: string;
|
email: string;
|
||||||
|
method: MfaMethod;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Mfa = ({ successCallback, closeMfa, hideLogo, email }: Props) => {
|
export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Props) => {
|
||||||
const [mfaCode, setMfaCode] = useState("");
|
const [mfaCode, setMfaCode] = useState("");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isLoadingResend, setIsLoadingResend] = useState(false);
|
const [isLoadingResend, setIsLoadingResend] = useState(false);
|
||||||
const [triesLeft, setTriesLeft] = useState<number | undefined>(undefined);
|
const [triesLeft, setTriesLeft] = useState<number | undefined>(undefined);
|
||||||
|
const [shouldShowTotpRegistration, setShouldShowTotpRegistration] = useState(false);
|
||||||
|
|
||||||
const sendMfaToken = useSendMfaToken();
|
const sendMfaToken = useSendMfaToken();
|
||||||
|
|
||||||
const verifyMfa = async () => {
|
useEffect(() => {
|
||||||
|
if (method === MfaMethod.TOTP) {
|
||||||
|
checkUserTotpMfa().then((isVerified) => {
|
||||||
|
if (!isVerified) {
|
||||||
|
SecurityClient.setMfaToken("");
|
||||||
|
setShouldShowTotpRegistration(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const verifyMfa = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const { token } = await verifyMfaToken({
|
const { token } = await verifyMfaToken({
|
||||||
email,
|
email,
|
||||||
mfaCode
|
mfaCode,
|
||||||
|
mfaMethod: method
|
||||||
});
|
});
|
||||||
|
|
||||||
SecurityClient.setMfaToken("");
|
SecurityClient.setMfaToken("");
|
||||||
@ -92,6 +110,24 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email }: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (shouldShowTotpRegistration) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-6 text-center text-lg font-bold text-white">
|
||||||
|
Your organization requires mobile authentication to be configured.
|
||||||
|
</div>
|
||||||
|
<div className="mx-auto w-max pb-4 pt-4 md:mb-16 md:px-8">
|
||||||
|
<TotpRegistration
|
||||||
|
onComplete={async () => {
|
||||||
|
setShouldShowTotpRegistration(false);
|
||||||
|
await successCallback();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-max pb-4 pt-4 md:mb-16 md:px-8">
|
<div className="mx-auto w-max pb-4 pt-4 md:mb-16 md:px-8">
|
||||||
{!hideLogo && (
|
{!hideLogo && (
|
||||||
@ -101,52 +137,87 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<p className="text-l flex justify-center text-bunker-300">{t("mfa.step2-message")}</p>
|
{method === MfaMethod.EMAIL && (
|
||||||
<p className="text-l my-1 flex justify-center font-semibold text-bunker-300">{email}</p>
|
<>
|
||||||
<div className="mx-auto hidden w-max min-w-[20rem] md:block">
|
<p className="text-l flex justify-center text-bunker-300">{t("mfa.step2-message")}</p>
|
||||||
<ReactCodeInput
|
<p className="text-l my-1 flex justify-center font-semibold text-bunker-300">{email}</p>
|
||||||
name=""
|
</>
|
||||||
inputMode="tel"
|
|
||||||
type="text"
|
|
||||||
fields={6}
|
|
||||||
onChange={setMfaCode}
|
|
||||||
className="mt-6 mb-2"
|
|
||||||
{...codeInputProps}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{typeof triesLeft === "number" && (
|
|
||||||
<Error text={`Invalid code. You have ${triesLeft} attempt(s) remaining.`} />
|
|
||||||
)}
|
)}
|
||||||
<div className="mx-auto mt-2 flex w-1/4 min-w-[20rem] max-w-xs flex-col items-center justify-center text-center text-sm md:max-w-md md:text-left lg:w-[19%]">
|
{method === MfaMethod.TOTP && (
|
||||||
<div className="text-l w-full py-1 text-lg">
|
<>
|
||||||
<Button
|
<p className="text-l mb-4 flex max-w-xs justify-center text-center font-bold text-bunker-100">
|
||||||
onClick={() => verifyMfa()}
|
Authenticator MFA Required
|
||||||
size="sm"
|
</p>
|
||||||
isFullWidth
|
<p className="text-l flex max-w-xs justify-center text-center text-bunker-300">
|
||||||
className="h-14"
|
Open the authenticator app on your mobile device to get your verification code or enter
|
||||||
colorSchema="primary"
|
a recovery code.
|
||||||
variant="outline_bg"
|
</p>
|
||||||
isLoading={isLoading}
|
</>
|
||||||
>
|
)}
|
||||||
{String(t("mfa.verify"))}
|
<form onSubmit={verifyMfa}>
|
||||||
</Button>
|
<div className="mx-auto hidden w-max min-w-[20rem] md:block">
|
||||||
|
{method === MfaMethod.EMAIL && (
|
||||||
|
<ReactCodeInput
|
||||||
|
name=""
|
||||||
|
inputMode="tel"
|
||||||
|
type="text"
|
||||||
|
fields={6}
|
||||||
|
onChange={setMfaCode}
|
||||||
|
className="mt-6 mb-2"
|
||||||
|
{...codeInputProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{method === MfaMethod.TOTP && (
|
||||||
|
<div className="mt-6 mb-4">
|
||||||
|
<Input value={mfaCode} onChange={(e) => setMfaCode(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{typeof triesLeft === "number" && (
|
||||||
<div className="mx-auto flex max-h-24 w-full max-w-md flex-col items-center justify-center pt-2">
|
<Error text={`Invalid code. You have ${triesLeft} attempt(s) remaining.`} />
|
||||||
<div className="flex flex-row items-baseline gap-1 text-sm">
|
)}
|
||||||
<span className="text-bunker-400">{t("signup.step2-resend-alert")}</span>
|
<div className="mx-auto mt-2 flex w-1/4 min-w-[20rem] max-w-xs flex-col items-center justify-center text-center text-sm md:max-w-md md:text-left lg:w-[19%]">
|
||||||
<div className="text-md mt-2 flex flex-row text-bunker-400">
|
<div className="text-l w-full py-1 text-lg">
|
||||||
<button disabled={isLoadingResend} onClick={handleResendMfaCode} type="button">
|
<Button
|
||||||
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
size="sm"
|
||||||
{isLoadingResend
|
type="submit"
|
||||||
? t("signup.step2-resend-progress")
|
isFullWidth
|
||||||
: t("signup.step2-resend-submit")}
|
className="h-14"
|
||||||
</span>
|
colorSchema="primary"
|
||||||
</button>
|
variant="outline_bg"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
{String(t("mfa.verify"))}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="pb-2 text-sm text-bunker-400">{t("signup.step2-spam-alert")}</p>
|
</form>
|
||||||
</div>
|
{method === MfaMethod.TOTP && (
|
||||||
|
<div className="mt-2 flex flex-row justify-center text-sm text-bunker-400 ">
|
||||||
|
<Link href="/verify-email">
|
||||||
|
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||||
|
Lost your recovery codes? Reset your account
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{method === MfaMethod.EMAIL && (
|
||||||
|
<div className="mx-auto flex max-h-24 w-full max-w-md flex-col items-center justify-center pt-2">
|
||||||
|
<div className="flex flex-row items-baseline gap-1 text-sm">
|
||||||
|
<span className="text-bunker-400">{t("signup.step2-resend-alert")}</span>
|
||||||
|
<div className="text-md mt-2 flex flex-row text-bunker-400">
|
||||||
|
<button disabled={isLoadingResend} onClick={handleResendMfaCode} type="button">
|
||||||
|
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||||
|
{isLoadingResend
|
||||||
|
? t("signup.step2-resend-progress")
|
||||||
|
: t("signup.step2-resend-submit")}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="pb-2 text-sm text-bunker-400">{t("signup.step2-spam-alert")}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -16,6 +16,7 @@ import { Button, Input, Spinner } from "@app/components/v2";
|
|||||||
import { SessionStorageKeys } from "@app/const";
|
import { SessionStorageKeys } from "@app/const";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api";
|
import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api";
|
||||||
|
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||||
import { fetchMyPrivateKey } from "@app/hooks/api/users/queries";
|
import { fetchMyPrivateKey } from "@app/hooks/api/users/queries";
|
||||||
|
|
||||||
@ -36,6 +37,7 @@ export const PasswordStep = ({ providerAuthToken, email, password, setPassword }
|
|||||||
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
||||||
const { mutateAsync: oauthTokenExchange } = useOauthTokenExchange();
|
const { mutateAsync: oauthTokenExchange } = useOauthTokenExchange();
|
||||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||||
|
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||||
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||||
|
|
||||||
const { navigateToSelectOrganization } = useNavigateToSelectOrganization();
|
const { navigateToSelectOrganization } = useNavigateToSelectOrganization();
|
||||||
@ -66,12 +68,15 @@ export const PasswordStep = ({ providerAuthToken, email, password, setPassword }
|
|||||||
// case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org
|
// case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
const finishWithOrgWorkflow = async () => {
|
const finishWithOrgWorkflow = async () => {
|
||||||
const { token, isMfaEnabled } = await selectOrganization({ organizationId });
|
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({ organizationId });
|
||||||
|
|
||||||
if (isMfaEnabled) {
|
if (isMfaEnabled) {
|
||||||
SecurityClient.setMfaToken(token);
|
SecurityClient.setMfaToken(token);
|
||||||
toggleShowMfa.on();
|
|
||||||
setMfaSuccessCallback(() => finishWithOrgWorkflow);
|
setMfaSuccessCallback(() => finishWithOrgWorkflow);
|
||||||
|
if (mfaMethod) {
|
||||||
|
setRequiredMfaMethod(mfaMethod);
|
||||||
|
}
|
||||||
|
toggleShowMfa.on();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,10 +172,15 @@ export const PasswordStep = ({ providerAuthToken, email, password, setPassword }
|
|||||||
// case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org
|
// case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
const finishWithOrgWorkflow = async () => {
|
const finishWithOrgWorkflow = async () => {
|
||||||
const { token, isMfaEnabled } = await selectOrganization({ organizationId });
|
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
|
||||||
|
organizationId
|
||||||
|
});
|
||||||
|
|
||||||
if (isMfaEnabled) {
|
if (isMfaEnabled) {
|
||||||
SecurityClient.setMfaToken(token);
|
SecurityClient.setMfaToken(token);
|
||||||
|
if (mfaMethod) {
|
||||||
|
setRequiredMfaMethod(mfaMethod);
|
||||||
|
}
|
||||||
toggleShowMfa.on();
|
toggleShowMfa.on();
|
||||||
setMfaSuccessCallback(() => finishWithOrgWorkflow);
|
setMfaSuccessCallback(() => finishWithOrgWorkflow);
|
||||||
return;
|
return;
|
||||||
@ -283,6 +293,7 @@ export const PasswordStep = ({ providerAuthToken, email, password, setPassword }
|
|||||||
<Mfa
|
<Mfa
|
||||||
email={email}
|
email={email}
|
||||||
successCallback={mfaSuccessCallback}
|
successCallback={mfaSuccessCallback}
|
||||||
|
method={requiredMfaMethod}
|
||||||
closeMfa={() => toggleShowMfa.off()}
|
closeMfa={() => toggleShowMfa.off()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Switch, UpgradePlanModal } from "@app/components/v2";
|
import { FormControl, Select, SelectItem, Switch, UpgradePlanModal } from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
OrgPermissionActions,
|
OrgPermissionActions,
|
||||||
OrgPermissionSubjects,
|
OrgPermissionSubjects,
|
||||||
@ -8,6 +8,7 @@ import {
|
|||||||
useSubscription
|
useSubscription
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { useUpdateOrg } from "@app/hooks/api";
|
import { useUpdateOrg } from "@app/hooks/api";
|
||||||
|
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
export const OrgGenericAuthSection = () => {
|
export const OrgGenericAuthSection = () => {
|
||||||
@ -43,6 +44,32 @@ export const OrgGenericAuthSection = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpdateSelectedMfa = async (selectedMfaMethod: MfaMethod) => {
|
||||||
|
try {
|
||||||
|
if (!currentOrg?.id) return;
|
||||||
|
if (!subscription?.enforceMfa) {
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
orgId: currentOrg?.id,
|
||||||
|
selectedMfaMethod
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully updated selected MFA method",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: (err as { response: { data: { message: string } } }).response.data.message,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
|
<div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
@ -62,6 +89,22 @@ export const OrgGenericAuthSection = () => {
|
|||||||
<p className="text-sm text-mineshaft-300">
|
<p className="text-sm text-mineshaft-300">
|
||||||
Enforce members to authenticate with MFA in order to access the organization
|
Enforce members to authenticate with MFA in order to access the organization
|
||||||
</p>
|
</p>
|
||||||
|
{currentOrg?.enforceMfa && (
|
||||||
|
<FormControl label="Selected 2FA method" className="mt-3">
|
||||||
|
<Select
|
||||||
|
className="min-w-[20rem] border border-mineshaft-500"
|
||||||
|
onValueChange={handleUpdateSelectedMfa}
|
||||||
|
defaultValue={currentOrg.selectedMfaMethod ?? MfaMethod.EMAIL}
|
||||||
|
>
|
||||||
|
<SelectItem value={MfaMethod.EMAIL} key="mfa-method-email">
|
||||||
|
Email
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={MfaMethod.TOTP} key="mfa-method-totp">
|
||||||
|
Mobile Authenticator
|
||||||
|
</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<UpgradePlanModal
|
<UpgradePlanModal
|
||||||
isOpen={popUp.upgradePlan.isOpen}
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
|
@ -1,18 +1,108 @@
|
|||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import TotpRegistration from "@app/components/mfa/TotpRegistration";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Checkbox, EmailServiceSetupModal } from "@app/components/v2";
|
import {
|
||||||
import { useGetUser, useUpdateMfaEnabled } from "@app/hooks/api";
|
Button,
|
||||||
|
ContentLoader,
|
||||||
|
DeleteActionModal,
|
||||||
|
EmailServiceSetupModal,
|
||||||
|
FormControl,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Switch
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import { useGetUser, userKeys, useUpdateUserMfa } from "@app/hooks/api";
|
||||||
|
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||||
|
import {
|
||||||
|
useCreateNewTotpRecoveryCodes,
|
||||||
|
useDeleteUserTotpConfiguration
|
||||||
|
} from "@app/hooks/api/users/mutation";
|
||||||
|
import { useGetUserTotpConfiguration } from "@app/hooks/api/users/queries";
|
||||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
export const MFASection = () => {
|
export const MFASection = () => {
|
||||||
const { data: user } = useGetUser();
|
const { data: user } = useGetUser();
|
||||||
const { mutateAsync } = useUpdateMfaEnabled();
|
const { mutateAsync } = useUpdateUserMfa();
|
||||||
|
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["setUpEmail"] as const);
|
|
||||||
|
|
||||||
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
|
"setUpEmail",
|
||||||
|
"deleteTotpConfig"
|
||||||
|
] as const);
|
||||||
|
const [shouldShowRecoveryCodes, setShouldShowRecoveryCodes] = useToggle();
|
||||||
|
const { data: totpConfiguration, isLoading: isTotpConfigurationLoading } =
|
||||||
|
useGetUserTotpConfiguration();
|
||||||
|
const { mutateAsync: deleteTotpConfiguration } = useDeleteUserTotpConfiguration();
|
||||||
|
const { mutateAsync: createTotpRecoveryCodes } = useCreateNewTotpRecoveryCodes();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const { data: serverDetails } = useFetchServerStatus();
|
const { data: serverDetails } = useFetchServerStatus();
|
||||||
|
|
||||||
|
const handleTotpDeletion = async () => {
|
||||||
|
try {
|
||||||
|
await deleteTotpConfiguration();
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully deleted mobile authenticator",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpClose("deleteTotpConfig");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
const error = err as any;
|
||||||
|
const text = error?.response?.data?.message ?? "Failed to delete mobile authenticator";
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGenerateMoreRecoveryCodes = async () => {
|
||||||
|
try {
|
||||||
|
await createTotpRecoveryCodes();
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully generated new recovery codes",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
const error = err as any;
|
||||||
|
const text = error?.response?.data?.message ?? "Failed to generate new recovery codes";
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSelectedMfa = async (mfaMethod: MfaMethod) => {
|
||||||
|
try {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
selectedMfaMethod: mfaMethod
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully updated selected 2FA method",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
createNotification({
|
||||||
|
text: "Something went wrong while updating selected 2FA method.",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const toggleMfa = async (state: boolean) => {
|
const toggleMfa = async (state: boolean) => {
|
||||||
try {
|
try {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
@ -47,31 +137,96 @@ export const MFASection = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form>
|
<div className="mb-6 max-w-6xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-6 max-w-6xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Two-factor Authentication</p>
|
||||||
<p className="mb-8 text-xl font-semibold text-mineshaft-100">Two-factor Authentication</p>
|
{user && (
|
||||||
{user && (
|
<Switch
|
||||||
<Checkbox
|
className="data-[state=checked]:bg-primary"
|
||||||
className="data-[state=checked]:bg-primary"
|
id="isTwoFAEnabled"
|
||||||
id="isTwoFAEnabled"
|
isChecked={user?.isMfaEnabled}
|
||||||
isChecked={user?.isMfaEnabled}
|
onCheckedChange={(state) => {
|
||||||
onCheckedChange={(state) => {
|
if (serverDetails?.emailConfigured) {
|
||||||
if (serverDetails?.emailConfigured) {
|
toggleMfa(state as boolean);
|
||||||
toggleMfa(state as boolean);
|
} else {
|
||||||
} else {
|
handlePopUpOpen("setUpEmail");
|
||||||
handlePopUpOpen("setUpEmail");
|
}
|
||||||
}
|
}}
|
||||||
}}
|
>
|
||||||
|
Enable 2-factor authentication
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
{user?.isMfaEnabled && (
|
||||||
|
<FormControl label="Selected 2FA method" className="mt-3">
|
||||||
|
<Select
|
||||||
|
className="min-w-[20rem] border border-mineshaft-500"
|
||||||
|
onValueChange={updateSelectedMfa}
|
||||||
|
defaultValue={user.selectedMfaMethod ?? MfaMethod.EMAIL}
|
||||||
>
|
>
|
||||||
Enable 2-factor authentication via your personal email.
|
<SelectItem value={MfaMethod.EMAIL} key="mfa-method-email">
|
||||||
</Checkbox>
|
Email
|
||||||
)}
|
</SelectItem>
|
||||||
</div>
|
<SelectItem value={MfaMethod.TOTP} key="mfa-method-totp">
|
||||||
</form>
|
Mobile Authenticator
|
||||||
|
</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
<div className="mt-8 text-lg font-semibold text-mineshaft-100">Mobile Authenticator</div>
|
||||||
|
{isTotpConfigurationLoading ? (
|
||||||
|
<ContentLoader />
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{totpConfiguration?.isVerified ? (
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<Button colorSchema="secondary" onClick={setShouldShowRecoveryCodes.toggle}>
|
||||||
|
{shouldShowRecoveryCodes ? "Hide recovery codes" : "Show recovery codes"}
|
||||||
|
</Button>
|
||||||
|
<Button colorSchema="secondary" onClick={handleGenerateMoreRecoveryCodes}>
|
||||||
|
Generate more codes
|
||||||
|
</Button>
|
||||||
|
<Button colorSchema="danger" onClick={() => handlePopUpOpen("deleteTotpConfig")}>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{shouldShowRecoveryCodes && totpConfiguration.recoveryCodes && (
|
||||||
|
<div className="mt-4 bg-mineshaft-600 p-4">
|
||||||
|
{totpConfiguration.recoveryCodes.map((code) => (
|
||||||
|
<div key={code}>{code}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="text-sm text-gray-400">
|
||||||
|
For added security, you can configure a mobile authenticator and set it as your
|
||||||
|
selected 2FA method.
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 flex min-w-full justify-center">
|
||||||
|
<TotpRegistration
|
||||||
|
onComplete={async () => {
|
||||||
|
await queryClient.invalidateQueries(userKeys.totpConfiguration);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<EmailServiceSetupModal
|
<EmailServiceSetupModal
|
||||||
isOpen={popUp.setUpEmail?.isOpen}
|
isOpen={popUp.setUpEmail?.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
||||||
/>
|
/>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteTotpConfig.isOpen}
|
||||||
|
title="Are you sure want to delete the configured authenticator?"
|
||||||
|
subTitle="This action is irreversible. You’ll have to go through the setup process to enable it again."
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deleteTotpConfig", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={handleTotpDeletion}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -13,6 +13,7 @@ import SecurityClient from "@app/components/utilities/SecurityClient";
|
|||||||
import { Button, Input } from "@app/components/v2";
|
import { Button, Input } from "@app/components/v2";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
|
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
|
||||||
|
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||||
import ProjectService from "@app/services/ProjectService";
|
import ProjectService from "@app/services/ProjectService";
|
||||||
import { Mfa } from "@app/views/Login/Mfa";
|
import { Mfa } from "@app/views/Login/Mfa";
|
||||||
@ -57,6 +58,7 @@ export const UserInfoSSOStep = ({
|
|||||||
const [organizationNameError, setOrganizationNameError] = useState(false);
|
const [organizationNameError, setOrganizationNameError] = useState(false);
|
||||||
const [attributionSource, setAttributionSource] = useState("");
|
const [attributionSource, setAttributionSource] = useState("");
|
||||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||||
|
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
||||||
@ -178,12 +180,15 @@ export const UserInfoSSOStep = ({
|
|||||||
|
|
||||||
const completeSignupFlow = async () => {
|
const completeSignupFlow = async () => {
|
||||||
try {
|
try {
|
||||||
const { isMfaEnabled, token } = await selectOrganization({
|
const { isMfaEnabled, token, mfaMethod } = await selectOrganization({
|
||||||
organizationId: orgId
|
organizationId: orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isMfaEnabled) {
|
if (isMfaEnabled) {
|
||||||
SecurityClient.setMfaToken(token);
|
SecurityClient.setMfaToken(token);
|
||||||
|
if (mfaMethod) {
|
||||||
|
setRequiredMfaMethod(mfaMethod);
|
||||||
|
}
|
||||||
toggleShowMfa.on();
|
toggleShowMfa.on();
|
||||||
setMfaSuccessCallback(() => completeSignupFlow);
|
setMfaSuccessCallback(() => completeSignupFlow);
|
||||||
return;
|
return;
|
||||||
@ -231,6 +236,7 @@ export const UserInfoSSOStep = ({
|
|||||||
hideLogo
|
hideLogo
|
||||||
email={username}
|
email={username}
|
||||||
successCallback={mfaSuccessCallback}
|
successCallback={mfaSuccessCallback}
|
||||||
|
method={requiredMfaMethod}
|
||||||
closeMfa={() => toggleShowMfa.off()}
|
closeMfa={() => toggleShowMfa.off()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user