# 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="https://1713127421-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F4m1xTBS7F2xndGsJd16w%2Fuploads%2FjZjdgeijbvGlzIohUXT4%2Fimage.png?alt=media&#x26;token=415b10d6-f2e6-4422-be2b-987155b94f27" 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="https://1713127421-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F4m1xTBS7F2xndGsJd16w%2Fuploads%2FZ4P5iqwOd62stqj2LqN2%2Fimage.png?alt=media&#x26;token=c2623081-8e02-4488-ae26-918429be991c" 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>
    );
}
```
