> For the complete documentation index, see [llms.txt](https://intercode.gitbook.io/intercode-saas-kit/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://intercode.gitbook.io/intercode-saas-kit/pages/auth/two-factor-auth.md).

# Two-Factor Auth

#### Setup:

* **Step 1:** User installs an authentication app on his prefered device.
* **Step 2:** OTP link is generated on a server side.
* **Step 3:** User scans generated on UI QR Code (via app) or follows a link to an authenticator app.
* **Step 4:** User enters code generated by authenticator app and submits its 2FA setup request.

<figure><img src="/files/40AQwKqS4GZJXLMr8RAf" alt=""><figcaption><p>2FA Setup Component</p></figcaption></figure>

#### Usage:

* **Step 1:** User logs in with their credentials (username/password).
* **Step 2:** After successful login, the user is prompted to enter a time-based one-time password (TOTP) generated by an authentication app (e.g., Google Authenticator, Authy).
* **Step 3:** The TOTP is verified on the server, and access is granted if both the password and the 2FA code are correct.

<figure><img src="/files/7dDBEpyloil6NGYid7dI" alt="" width="375"><figcaption><p>2FA Verification component</p></figcaption></figure>

## Implementation

Intercode Common uses separate UI components and a NestJS module in 2FA implementation

### Libraries and enviroment setup

First of all we need to install libraries needed to implement 2FA functionality

```bash
yarn add otplib
yarn add qrcode
```

Then we need to set up our 2FA App name on a API side

Add this variables to the following files

{% code title=".env" %}

```
TWO_FACTOR_AUTH_APP_NAME=YourAppName
```

{% endcode %}

{% code title="constants.ts" fullWidth="false" %}

```typescript
TWO_FACTOR_AUTH_APP_NAME: process.env.TWO_FACTOR_AUTH_APP_NAME
```

{% endcode %}

### API

**TwoFactorAuthController**

The `TwoFactorAuthController` handles 2FA-related HTTP requests. This controller is secured with JWT-based authentication and uses the following endpoints:

* **GET `/2fa/generate-qr`**\
  Generates a QR code and provides an OTP URI link for the user to set up 2FA in an authentication app. Requires `DemoGuard`.
* **POST `/2fa/enable-two-factor-auth`**\
  Enables 2FA for the user by verifying the provided OTP code. If the code is valid, 2FA is enabled. Requires `DemoGuard`.
* **POST `/2fa/disable-two-factor-auth`**\
  Disables 2FA for the user.
* **POST `/2fa/authenticate`**\
  Authenticates the user using their OTP code from an authentication app. If successful, it returns a signed-in token.

**Guards Used:**

* `JwtAuthenticationGuard`: Ensures that requests come from authenticated users.
* `DemoGuard`: Additional security check applied to 2FA endpoints.

{% code title="two-factor-auth.controller.ts" fullWidth="false" %}

```typescript
import { Body, Controller, Post, UseGuards, ValidationPipe, HttpException, HttpStatus, Get } from '@nestjs/common';
import { TwoFactorAuthService } from '../service/two-factor-auth.service';
import { TwoFactorAuthCodeDto } from '../dto/two-factor-auth-code.dto';
import { JwtUserPayload } from 'src/common/interfaces/jwt-user-payload.interface';
import { GetUser } from '../decorator/get-user.decorator';
import { JwtAuthenticationGuard } from 'src/common/guards/jwt-authentication.guard';
import { AdminGuard } from 'src/common/guards/admin.guard';
import { TwoFactorImpersonationDto } from '../dto/two-factor-impersonation.dto';
import { JwtTwoFactorGuard } from 'src/common/guards/jwt-two-factor.guard';
import { DemoGuard } from '../../common/guards/demo.guard';

@Controller('2fa')
@UseGuards(JwtAuthenticationGuard)
export class TwoFactorAuthController {
    constructor(private readonly twoFactorAuthService: TwoFactorAuthService) {}

    @UseGuards(DemoGuard)
    @Get('generate-qr')
    async generateQrCode(@GetUser() user: JwtUserPayload) {
        const { otpAuthUrl } = await this.twoFactorAuthService.generateTwoFactorAuthSecret(user);
        return { qrCode: await this.twoFactorAuthService.generateQrCode(otpAuthUrl), link: otpAuthUrl };
    }

    @UseGuards(DemoGuard)
    @Post('enable-two-factor-auth')
    async enableOfTwoFactorAuth(
        @GetUser() user: JwtUserPayload,
        @Body(ValidationPipe) twoFactorAuthCodeDto: TwoFactorAuthCodeDto
    ) {
        const isCodeValid = await this.twoFactorAuthService.verifyTwoFactorAuthCode(twoFactorAuthCodeDto.code, user);
        if (!isCodeValid) {
            throw new HttpException(
                {
                    message: 'Code is not valid!',
                    description: 'An incorrect code or code time has expired. Please try again.',
                },
                HttpStatus.BAD_REQUEST
            );
        }
        await this.twoFactorAuthService.activationOfTwoFactorAuth(user, true);
    }

    @UseGuards(DemoGuard)
    @Post('disable-two-factor-auth')
    async disableOfTwoFactorAuth(@GetUser() user: JwtUserPayload) {
        await this.twoFactorAuthService.activationOfTwoFactorAuth(user, false);
    }

    @Post('authenticate')
    async authenticate(
        @GetUser() user: JwtUserPayload,
        @Body(ValidationPipe) twoFactorAuthCodeDto: TwoFactorAuthCodeDto
    ) {
        const isCodeValid = await this.twoFactorAuthService.verifyTwoFactorAuthCode(twoFactorAuthCodeDto.code, user);
        if (!isCodeValid) {
            throw new HttpException(
                {
                    message: 'Code is not valid!',
                    description: 'An incorrect code or code time has expired. Please try again.',
                    code: 'INVALID_TWO_FACTOR_AUTH_CODE',
                },
                HttpStatus.BAD_REQUEST
            );
        }
        return await this.twoFactorAuthService.signIn(user, true);
    }
}
```

{% endcode %}

**TwoFactorAuthService**

The `TwoFactorAuthService` is responsible for 2FA logic, including generating secrets, creating QR codes, verifying OTP codes, and handling token generation. Key methods include:

* **`generateTwoFactorAuthSecret(user: JwtUserPayload)`**\
  Creates a 2FA secret for the user and stores it in the database. Returns the secret and an OTP URI link if the QR code hasn’t already been generated.
* **`generateQrCode(otpPathUrl: string)`**\
  Converts the OTP URI to a QR code (as a data URL) that the user can scan in their authentication app.
* **`activationOfTwoFactorAuth(user: JwtUserPayload, status: boolean)`**\
  Activates or deactivates 2FA for the user, based on the `status` parameter.
* **`verifyTwoFactorAuthCode(code: string, user: JwtUserPayload)`**\
  Verifies the provided OTP code against the user’s saved 2FA secret.
* **`signIn(user: JwtUserPayload, isTwoFactorAuthenticated: boolean): Promise<UserTokens>`**\
  Generates an access token and refresh token, including the 2FA status, for an authenticated user.

{% code title="two-factor-auth.service.ts" %}

```typescript
import { ForbiddenException, HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { authenticator } from 'otplib';
import { Repository } from 'typeorm';
import { toDataURL } from 'qrcode';
import { AuthService } from './auth.service';
import { JwtUserPayload } from 'src/common/interfaces/jwt-user-payload.interface';
import UserEntity from '../entities/user.entity';
import UserTokens from '../dto/user-tokens';
import constants from '../../constants';
import dayjs from 'dayjs';
import { Role } from 'src/common/constants/role.enum';
import { sign } from 'jsonwebtoken';
import { TokensExpiredTime } from 'src/common/constants/tokens-expired-time';

@Injectable()
export class TwoFactorAuthService {
    tokenSecret: string = constants.TOKEN_SECRET;

    constructor(
        @InjectRepository(UserEntity)
        private readonly userRepository: Repository<UserEntity>,
        private readonly authService: AuthService
    ) {}

    public async generateTwoFactorAuthSecret(user: JwtUserPayload) {
        const auth = await this.userRepository.findOne({
            where: { id: user.id },
            withDeleted: true,
        });

        if (auth) {
            if (auth.isTwoFactorEnable) {
                throw new HttpException(
                    {
                        message: 'QR code was already generated.',
                        description: 'You already have code in your Authenticator app.',
                    },
                    HttpStatus.BAD_REQUEST
                );
            }
        }

        const secret = authenticator.generateSecret();
        const appName = constants.TWO_FACTOR_AUTH_APP_NAME;
        const otpAuthUrl = authenticator.keyuri(auth.email, appName, secret);

        await this.userRepository.update({ id: auth.id }, { twoFactorAuthSecret: secret });
        return {
            secret,
            otpAuthUrl,
        };
    }

    public async generateQrCode(otpPathUrl: string) {
        return toDataURL(otpPathUrl);
    }

    public async activationOfTwoFactorAuth(user: JwtUserPayload, status: boolean) {
        try {
            return await this.userRepository.update(
                { id: user.id },
                {
                    isTwoFactorEnable: status,
                }
            );
        } catch (error) {
            throw new HttpException(
                {
                    message: 'Error occurred while updating 2FA.',
                    description: 'Please try again later.',
                },
                HttpStatus.BAD_REQUEST
            );
        }
    }

    public async verifyTwoFactorAuthCode(code: string, user: JwtUserPayload) {
        const authUser = await this.userRepository.findOne({
            where: { id: user.id },
            withDeleted: true,
        });

        if (!authUser.twoFactorAuthSecret) {
            throw new HttpException(
                {
                    message: 'Error',
                    description: 'Two factor auth is disabled on your profile',
                    code: "TWO_FACTOR_AUTH_DISABLED"
                },
                HttpStatus.BAD_REQUEST
            );
        }

        return authenticator.verify({
            token: code,
            secret: authUser.twoFactorAuthSecret,
        });
    }

    public async signIn(user: JwtUserPayload, isTwoFactorAuthenticated: boolean): Promise<UserTokens> {
        const accessToken = await this.authService.generateAccessToken({ userId: user.id, isTwoFactorAuthenticated });
        const refreshToken = await this.authService.setRefreshTokenWithExpDate(user.id);

        return {
            accessToken,
            refreshToken,
            isTwoFactorEnable: user.isTwoFactorEnable,
        };
    }
}
```

{% endcode %}

### UI

**TwoFactorAuth**

`TwoFactorAuth` manages 2FA activation and deactivation, integrating with the backend service to display and validate OTP codes and QR codes. It also includes error handling via notifications.

* **States**:
  * `loading`: Indicates whether a request is in progress.
  * `qrCode` & `otpLink`: Store the generated QR code data and OTP URI link.
* **Handlers**:
  * **getQrCode**: Fetches a QR code for 2FA setup, which the user scans with their authentication app.
  * **handleDisable**: Prompts the user to confirm 2FA deactivation, using a modal for confirmation.
  * **turnOffTwoFactorAuth**: Deactivates 2FA for the user by calling `disableTwoFactorAuth` and updating the UI.
  * **onFormSubmit**: Submits the OTP code from the `TwoFactorAuthForm` to enable 2FA.
  * **errorNotification**: Shows error messages if any request fails, using Ant Design’s notification system.
* **Display Logic**:
  * **2FA Setup** (when `currentUser.isTwoFactorEnable` is `false`): Presents three steps:
    1. **Step 1**: General instructions.
    2. **Step 2**: QR code generation.
       * Includes a button to generate the QR code and display it.
       * The QR code can be scanned to configure the 2FA app.
    3. **Step 3**: Form for OTP input.
  * **2FA Deactivation** (when `currentUser.isTwoFactorEnable` is `true`): Shows a button to turn off 2FA with a power icon and loading state, triggering `handleDisable`.

```javascript
import { useState } from 'react';
import { Divider, Button, Modal, notification, Flex } from 'antd';
import { QrcodeOutlined, PoweroffOutlined } from '@ant-design/icons';
import TwoFactorAuthForm from '../auth/TwoFactorAuthForm';
import { useAuthService } from '../../services/authService';
import { useAuth } from '../../context/AuthContext';
import { notificationType } from '../../common/constants/notificationType';
import { buttonStyle, colors, spacing } from '../../theming';
import { useTranslation } from 'react-i18next';

export default function TwoFactorAuth() {
    const [api, contextHolder] = notification.useNotification();
    const { currentUser, fetchUser } = useAuth();
    const { t } = useTranslation(['settings', 'confirmModal', 'apiResponses']);

    const { getGeneratedQr, enableTwoFactorAuth, disableTwoFactorAuth } = useAuthService();
    const [loading, setLoading] = useState(false);
    const [qrCode, setQrCode] = useState(null);
    const [otpLink, setOtpLink] = useState(null);

    // #region handlers
    function errorNotification(error) {
        api[notificationType.ERROR]({
            message: error.response?.data?.message ?? 'Error occurred while working with 2FA.',
            description: error.response?.data?.description ?? '',
            placement: 'bottomRight',
        });
    }

    async function getQrCode() {
        try {
            const { data } = await getGeneratedQr();

            if (data) {
                setQrCode(data.qrCode);
                setOtpLink(data.link);
            }
        } catch (error) {
            errorNotification(error);
        }
    }

    async function handleDisable() {
        Modal.confirm({
            title: t('confirmModal:title.disable-2fa'),
            styles: { body: { padding: spacing[4] } },
            width: 'fit-content',
            content: t('confirmModal:actions.disable-2fa'),
            centered: true,
            maskClosable: true,
            onOk: turnOffTwoFactorAuth,
            okText: t('confirmModal:buttons.yes'),
            cancelText: t('confirmModal:buttons.cancel'),
        });
    }

    async function turnOffTwoFactorAuth() {
        try {
            setLoading(true);
            const response = await disableTwoFactorAuth();

            if (response) {
                fetchUser();
            }

            setLoading(false);
        } catch (error) {
            errorNotification(error);
        } finally {
            setLoading(false);
        }
    }

    async function onFormSubmit(form) {
        try {
            setLoading(true);
            const response = await enableTwoFactorAuth(form.code);
            if (response) {
                fetchUser();
            }

            setLoading(false);
        } catch (error) {
            errorNotification(error);
        } finally {
            setLoading(false);
        }
    }
    // #endregion

    if (!currentUser.isTwoFactorEnable) {
        return (
            <Flex vertical justify="center">
                {contextHolder}
                <Divider orientation="left">{t('settings:2FA.step-1.title')}</Divider>
                <div style={{ margin: '0 50px' }}>
                    <p>{t('settings:2FA.step-1.subtitle')}</p>
                </div>
                <Divider orientation="left">{t('settings:2FA.step-2.title')}</Divider>
                <div style={{ margin: '0 50px' }}>
                    <p>{t('settings:2FA.step-2.subtitle')}</p>
                    <Button
                        type="primary"
                        icon={<QrcodeOutlined />}
                        onClick={getQrCode}
                        disabled={qrCode ? true : false}
                        style={{ ...buttonStyle.medium }}
                    >
                        {t('settings:2FA.step-2.button')}
                    </Button>
                    {qrCode && <p style={{ color: colors.colorError }}>{t('settings:2FA.step-2.help')}</p>}
                    <div style={{ margin: '10px 0' }} onClick={() => window.open(otpLink, '_blank')}>
                        {qrCode && <img src={qrCode} alt="Two-Factor Authentication QR Code" />}
                    </div>
                </div>

                <Divider orientation="left">{t('settings:2FA.step-3.title')}</Divider>
                <div style={{ margin: '0 50px' }}>
                    <p>{t('settings:2FA.step-3.subtitle')}</p>
                    <div style={{ maxWidth: 350 }}>
                        <TwoFactorAuthForm
                            loading={loading}
                            showCancel={false}
                            submitDisabled={qrCode ? false : true}
                            onFormSubmit={onFormSubmit}
                            t={t}
                        />
                    </div>
                </div>
            </Flex>
        );
    }

    if (currentUser.isTwoFactorEnable) {
        return (
            <div>
                {contextHolder}
                <Divider orientation="left">{t('settings:2FA.turn-off.title')}</Divider>
                <Button
                    type="primary"
                    icon={<PoweroffOutlined />}
                    onClick={handleDisable}
                    loading={loading}
                    danger
                    style={{ ...buttonStyle.medium }}
                >
                    {t('settings:2FA.turn-off.button')}
                </Button>
            </div>
        );
    }
}
```

**TwoFactorAuthForm**

`TwoFactorAuthForm` is a form component that accepts a 6-digit OTP code from the user, validating it against predefined rules before submission. It includes:

* **Code Input Field**: Accepts a 6-digit OTP code.
  * **Validation Rules**: The code must be exactly 6 digits and is required.
  * **Debounced Validation**: Provides feedback 1 second after the user stops typing.
* **Buttons**:
  * **Cancel Button**: Allows the user to go back or cancel the 2FA setup, visible based on `showCancel` prop.
  * **Submit Button**: Submits the form to verify and enable 2FA, with a loading indicator when `loading` is true.

```javascript
import { Button, Form, Input, Flex } from 'antd';
import { buttonStyle } from '../../theming';

export default function TwoFactorAuthForm({ loading, showCancel, submitDisabled, onBackClick, onFormSubmit, t }) {
    return (
        <Form onFinish={onFormSubmit} layout="vertical" requiredMark={false}>
            <Form.Item
                label={t('settings:2FA.form.label')}
                name="code"
                hasFeedback
                validateDebounce={1000}
                rules={[
                    {
                        min: 6,
                        max: 6,
                        message: t('settings:2FA.form.minmax'),
                    },
                    {
                        required: true,
                        message: t('settings:2FA.form.required'),
                    },
                ]}
            >
                <Input
                    size="large"
                    placeholder={t('settings:2FA.form.placeholder')}
                    disabled={submitDisabled}
                    autoComplete="off"
                />
            </Form.Item>
            <Flex justify={showCancel ? 'space-around' : 'start'} align="center" style={{ marginTop: 30 }}>
                {showCancel && (
                    <Button
                        htmlType="button"
                        type="default"
                        size={'middle'}
                        onClick={onBackClick}
                        style={{ ...buttonStyle.medium }}
                    >
                        {t('settings:2FA.form.cancel')}
                    </Button>
                )}
                <Button
                    htmlType="submit"
                    type="primary"
                    size={'middle'}
                    loading={loading}
                    disabled={submitDisabled}
                    style={{ ...buttonStyle.medium }}
                >
                    {t('settings:2FA.form.submit')}
                </Button>
            </Flex>
        </Form>
    );
}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://intercode.gitbook.io/intercode-saas-kit/pages/auth/two-factor-auth.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
