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

View File

@ -420,7 +420,8 @@ export enum ConfigVariablesGroup {
ServerlessConfig = 'ServerlessConfig',
StorageConfig = 'StorageConfig',
SupportChatConfig = 'SupportChatConfig',
TokensDuration = 'TokensDuration'
TokensDuration = 'TokensDuration',
TwoFactorAuthentication = 'TwoFactorAuthentication'
}
export type ConfigVariablesGroupData = {
@ -616,6 +617,12 @@ export type DeleteSsoOutput = {
identityProviderId: Scalars['String'];
};
export type DeleteTwoFactorAuthenticationMethodOutput = {
__typename?: 'DeleteTwoFactorAuthenticationMethodOutput';
/** Boolean that confirms query was dispatched */
success: Scalars['Boolean'];
};
export type DeleteWebhookDto = {
id: Scalars['String'];
};
@ -704,6 +711,7 @@ export enum FeatureFlagKey {
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
IS_RELATION_CONNECT_ENABLED = 'IS_RELATION_CONNECT_ENABLED',
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
IS_TWO_FACTOR_AUTHENTICATION_ENABLED = 'IS_TWO_FACTOR_AUTHENTICATION_ENABLED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
IS_WORKFLOW_FILTERING_ENABLED = 'IS_WORKFLOW_FILTERING_ENABLED',
IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED = 'IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED'
@ -1006,6 +1014,11 @@ export enum IndexType {
GIN = 'GIN'
}
export type InitiateTwoFactorAuthenticationProvisioningOutput = {
__typename?: 'InitiateTwoFactorAuthenticationProvisioningOutput';
uri: Scalars['String'];
};
export type InvalidatePassword = {
__typename?: 'InvalidatePassword';
/** Boolean that confirms query was dispatched */
@ -1078,6 +1091,7 @@ export type Mutation = {
deleteOneRole: Scalars['String'];
deleteOneServerlessFunction: ServerlessFunction;
deleteSSOIdentityProvider: DeleteSsoOutput;
deleteTwoFactorAuthenticationMethod: DeleteTwoFactorAuthenticationMethodOutput;
deleteUser: User;
deleteWebhook: Scalars['Boolean'];
deleteWorkflowVersionStep: WorkflowAction;
@ -1091,10 +1105,13 @@ export type Mutation = {
generateApiKeyToken: ApiKeyToken;
generateTransientToken: TransientToken;
getAuthTokensFromLoginToken: AuthTokens;
getAuthTokensFromOTP: AuthTokens;
getAuthorizationUrlForSSO: GetAuthorizationUrlForSsoOutput;
getLoginTokenFromCredentials: LoginToken;
getLoginTokenFromEmailVerificationToken: GetLoginTokenFromEmailVerificationTokenOutput;
impersonate: ImpersonateOutput;
initiateOTPProvisioning: InitiateTwoFactorAuthenticationProvisioningOutput;
initiateOTPProvisioningForAuthenticatedUser: InitiateTwoFactorAuthenticationProvisioningOutput;
publishServerlessFunction: ServerlessFunction;
removeRoleFromAgent: Scalars['Boolean'];
renewToken: AuthTokens;
@ -1138,6 +1155,7 @@ export type Mutation = {
upsertSettingPermissions: Array<SettingPermission>;
userLookupAdminPanel: UserLookup;
validateApprovedAccessDomain: ApprovedAccessDomain;
verifyTwoFactorAuthenticationMethodForAuthenticatedUser: VerifyTwoFactorAuthenticationMethodOutput;
};
@ -1296,6 +1314,11 @@ export type MutationDeleteSsoIdentityProviderArgs = {
};
export type MutationDeleteTwoFactorAuthenticationMethodArgs = {
twoFactorAuthenticationMethodId: Scalars['UUID'];
};
export type MutationDeleteWebhookArgs = {
input: DeleteWebhookDto;
};
@ -1339,6 +1362,14 @@ export type MutationGetAuthTokensFromLoginTokenArgs = {
};
export type MutationGetAuthTokensFromOtpArgs = {
captchaToken?: InputMaybe<Scalars['String']>;
loginToken: Scalars['String'];
origin: Scalars['String'];
otp: Scalars['String'];
};
export type MutationGetAuthorizationUrlForSsoArgs = {
input: GetAuthorizationUrlForSsoInput;
};
@ -1366,6 +1397,12 @@ export type MutationImpersonateArgs = {
};
export type MutationInitiateOtpProvisioningArgs = {
loginToken: Scalars['String'];
origin: Scalars['String'];
};
export type MutationPublishServerlessFunctionArgs = {
input: PublishServerlessFunctionInput;
};
@ -1580,6 +1617,11 @@ export type MutationValidateApprovedAccessDomainArgs = {
input: ValidateApprovedAccessDomainInput;
};
export type MutationVerifyTwoFactorAuthenticationMethodForAuthenticatedUserArgs = {
otp: Scalars['String'];
};
export type Object = {
__typename?: 'Object';
createdAt: Scalars['DateTime'];
@ -2376,6 +2418,13 @@ export type TransientToken = {
transientToken: AuthToken;
};
export type TwoFactorAuthenticationMethodDto = {
__typename?: 'TwoFactorAuthenticationMethodDTO';
status: Scalars['String'];
strategy: Scalars['String'];
twoFactorAuthenticationMethodId: Scalars['UUID'];
};
export type UuidFilter = {
eq?: InputMaybe<Scalars['UUID']>;
gt?: InputMaybe<Scalars['UUID']>;
@ -2525,6 +2574,7 @@ export type UpdateWorkspaceInput = {
isMicrosoftAuthEnabled?: InputMaybe<Scalars['Boolean']>;
isPasswordAuthEnabled?: InputMaybe<Scalars['Boolean']>;
isPublicInviteLinkEnabled?: InputMaybe<Scalars['Boolean']>;
isTwoFactorAuthenticationEnforced?: InputMaybe<Scalars['Boolean']>;
logo?: InputMaybe<Scalars['String']>;
subdomain?: InputMaybe<Scalars['String']>;
};
@ -2610,6 +2660,7 @@ export type UserWorkspace = {
/** @deprecated Use objectPermissions instead */
objectRecordsPermissions?: Maybe<Array<PermissionsOnAllObjectRecords>>;
settingsPermissions?: Maybe<Array<SettingPermissionType>>;
twoFactorAuthenticationMethodSummary?: Maybe<Array<TwoFactorAuthenticationMethodDto>>;
updatedAt: Scalars['DateTime'];
user: User;
userId: Scalars['String'];
@ -2628,6 +2679,11 @@ export type ValidatePasswordResetToken = {
id: Scalars['String'];
};
export type VerifyTwoFactorAuthenticationMethodOutput = {
__typename?: 'VerifyTwoFactorAuthenticationMethodOutput';
success: Scalars['Boolean'];
};
export type VersionInfo = {
__typename?: 'VersionInfo';
currentVersion?: Maybe<Scalars['String']>;
@ -2703,6 +2759,7 @@ export type Workspace = {
isMicrosoftAuthEnabled: Scalars['Boolean'];
isPasswordAuthEnabled: Scalars['Boolean'];
isPublicInviteLinkEnabled: Scalars['Boolean'];
isTwoFactorAuthenticationEnforced: Scalars['Boolean'];
logo?: Maybe<Scalars['String']>;
metadataVersion: Scalars['Float'];
subdomain: Scalars['String'];