Implement Two-Factor Authentication (2FA) (#13141)

Implementation is very simple

Established authentication dynamic is intercepted at
getAuthTokensFromLoginToken. If 2FA is required, a pattern similar to
EmailVerification is executed. That is, getAuthTokensFromLoginToken
mutation fails with either of the following errors:

1. TWO_FACTOR_AUTHENTICATION_VERIFICATION_REQUIRED
2. TWO_FACTOR_AUTHENTICATION_PROVISION_REQUIRED

UI knows how to respond accordingly.

2FA provisioning occurs at the 2FA resolver.
2FA verification, currently only OTP, is handled by auth.resolver's
getAuthTokensFromOTP

---------

Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Jean-Baptiste Ronssin <65334819+jbronssin@users.noreply.github.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
oliver
2025-07-23 06:42:01 -06:00
committed by GitHub
parent dd5ae66449
commit 4d3124f840
106 changed files with 5103 additions and 103 deletions
@@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class DropOldTwoFactorMethodTable1752170600000
implements MigrationInterface
{
name = 'DropOldTwoFactorMethodTable1752170600000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."twoFactorMethod" DROP CONSTRAINT "FK_c1044145be65a4ee65c07e0a658"`,
);
await queryRunner.query(`DROP TABLE "core"."twoFactorMethod"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "core"."twoFactorMethod" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userWorkspaceId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_752f0250dd6824289ceddd8b054" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`ALTER TABLE "core"."twoFactorMethod" ADD CONSTRAINT "FK_c1044145be65a4ee65c07e0a658" FOREIGN KEY ("userWorkspaceId") REFERENCES "core"."userWorkspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
}
@@ -0,0 +1,49 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class TwoFactorAuthentication1752839063082
implements MigrationInterface
{
name = 'TwoFactorAuthentication1752839063082';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TYPE "core"."twoFactorAuthenticationMethod_strategy_enum" AS ENUM('TOTP')`,
);
await queryRunner.query(
`CREATE TYPE "core"."twoFactorAuthenticationMethod_status_enum" AS ENUM('PENDING', 'VERIFIED')`,
);
await queryRunner.query(
`CREATE TABLE "core"."twoFactorAuthenticationMethod" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userWorkspaceId" uuid NOT NULL, "secret" text NOT NULL, "status" "core"."twoFactorAuthenticationMethod_status_enum" NOT NULL, "strategy" "core"."twoFactorAuthenticationMethod_strategy_enum" NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_c455f6a499e7110fc95e4bea540" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_2909f5139c479e4632df03fd5e" ON "core"."twoFactorAuthenticationMethod" ("userWorkspaceId", "strategy") `,
);
await queryRunner.query(
`ALTER TABLE "core"."workspace" ADD "isTwoFactorAuthenticationEnforced" boolean NOT NULL DEFAULT false`,
);
await queryRunner.query(
`ALTER TABLE "core"."twoFactorAuthenticationMethod" ADD CONSTRAINT "FK_b0f44ffd7c794beb48cb1e1b1a9" FOREIGN KEY ("userWorkspaceId") REFERENCES "core"."userWorkspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."twoFactorAuthenticationMethod" DROP CONSTRAINT "FK_b0f44ffd7c794beb48cb1e1b1a9"`,
);
await queryRunner.query(
`ALTER TABLE "core"."workspace" DROP COLUMN "isTwoFactorAuthenticationEnforced"`,
);
await queryRunner.query(
`DROP INDEX "core"."IDX_2909f5139c479e4632df03fd5e"`,
);
await queryRunner.query(
`DROP TABLE "core"."twoFactorAuthenticationMethod"`,
);
await queryRunner.query(
`DROP TYPE "core"."twoFactorAuthenticationMethod_status_enum"`,
);
await queryRunner.query(
`DROP TYPE "core"."twoFactorAuthenticationMethod_strategy_enum"`,
);
}
}