> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nullpass.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Two-Factor Authentication

> Enable or disable two-factor authentication

## Endpoint

```
POST /api/auth/2fa
```

## Overview

Manages two-factor authentication (2FA) using TOTP (Time-based One-Time Password). The process involves two steps for enabling: generating a QR code, then confirming with a verification code.

## Enable 2FA (Step 1: Generate QR Code)

### Request

<ParamField body="enable" type="boolean" required>
  Must be `true`
</ParamField>

### Response

<ResponseField name="qrCode" type="string">
  Base64-encoded PNG QR code image (data URI format)
</ResponseField>

<ResponseField name="secret" type="string">
  Base32-encoded secret key for manual entry
</ResponseField>

<ResponseField name="manualEntryKey" type="string">
  Same as `secret` (for compatibility)
</ResponseField>

<ResponseField name="message" type="string">
  "Scan the QR code with your authenticator app"
</ResponseField>

## Enable 2FA (Step 2: Confirm)

### Request

<ParamField body="enable" type="boolean" required>
  Must be `true`
</ParamField>

<ParamField body="secret" type="string" required>
  Secret from Step 1 response
</ParamField>

<ParamField body="verificationCode" type="string" required>
  TOTP code from authenticator app (6 digits)
</ParamField>

### Response

<ResponseField name="message" type="string">
  "2FA enabled successfully"
</ResponseField>

<ResponseField name="twoFactorEnabled" type="boolean">
  Always `true`
</ResponseField>

## Disable 2FA

### Request

<ParamField body="enable" type="boolean" required>
  Must be `false`
</ParamField>

<ParamField body="verificationCode" type="string" required>
  Current TOTP code from authenticator app
</ParamField>

### Response

<ResponseField name="message" type="string">
  "2FA disabled successfully"
</ResponseField>

<ResponseField name="twoFactorEnabled" type="boolean">
  Always `false`
</ResponseField>

## Implementation Details

### Process Flow

**Enable (Step 1):**

1. Generates secret using speakeasy (32 characters, base32)
2. Creates OTPAuth URL with issuer and label
3. Generates QR code as base64 data URI
4. Returns QR code and secret (not saved yet)

**Enable (Step 2):**

1. Verifies TOTP code with provided secret (window: 2)
2. Saves secret to user record
3. Sets `twoFactorEnabled` to true
4. Logs audit event

**Disable:**

1. Verifies 2FA is enabled
2. Verifies TOTP code with stored secret
3. Clears secret and disables 2FA
4. Logs audit event

### Code Reference

```18:153:nullpass_clean/src/app/api/auth/2fa/route.ts theme={null}
export async function POST(request: NextRequest) {
  const corsResponse = handleCors(request)
  if (corsResponse) return corsResponse

  const blocked = await protectRoute(request)
  if (blocked) return blocked

  const auth = await requireAuth(request)
  if ('error' in auth) return auth.error

  try {
    const body = await request.json()
    const validated = toggle2FASchema.parse(body)

    const user = await prisma.user.findUnique({
      where: { id: auth.userId },
      select: {
        id: true,
        email: true,
        displayName: true,
        twoFactorEnabled: true,
        twoFactorSecret: true,
      },
    })

    if (!user) {
      return errorResponse('User not found', 404, request.headers.get('origin'))
    }

    if (validated.enable) {
      if (validated.verificationCode && validated.secret) {
        const verified = speakeasy.totp.verify({
          secret: validated.secret,
          encoding: 'base32',
          token: validated.verificationCode,
          window: 2,
        })

        if (!verified) {
          return errorResponse('Invalid verification code', 400, request.headers.get('origin'))
        }

        await prisma.user.update({
          where: { id: auth.userId },
          data: {
            twoFactorEnabled: true,
            twoFactorSecret: validated.secret,
          },
        })

        await createAuditLog(auth.userId, 'TWO_FACTOR_ENABLE', {})

        return jsonResponse(
          {
            message: '2FA enabled successfully',
            twoFactorEnabled: true,
          },
          200,
          request.headers.get('origin')
        )
      } else {
        const generatedSecret = speakeasy.generateSecret({
          name: `Nullpass (${user.email})`,
          issuer: 'Nullpass',
          length: 32,
        })
        const secret = generatedSecret.base32

        const otpauthUrl = speakeasy.otpauthURL({
          secret: secret,
          label: user.email,
          issuer: 'Nullpass',
          encoding: 'base32',
        })

        const qrCodeUrl = await QRCode.toDataURL(otpauthUrl)

        return jsonResponse(
          {
            qrCode: qrCodeUrl,
            secret: secret,
            manualEntryKey: secret,
            message: 'Scan the QR code with your authenticator app',
          },
          200,
          request.headers.get('origin')
        )
      }
    } else {
      if (!validated.verificationCode) {
        return errorResponse('2FA verification code is required to disable 2FA', 400, request.headers.get('origin'))
      }

      if (!user.twoFactorEnabled || !user.twoFactorSecret) {
        return errorResponse('2FA is not enabled', 400, request.headers.get('origin'))
      }

      const verified = speakeasy.totp.verify({
        secret: user.twoFactorSecret,
        encoding: 'base32',
        token: validated.verificationCode,
        window: 2,
      })

      if (!verified) {
        return errorResponse('Invalid verification code', 400, request.headers.get('origin'))
      }

      await prisma.user.update({
        where: { id: auth.userId },
        data: {
          twoFactorEnabled: false,
          twoFactorSecret: null,
        },
      })

      await createAuditLog(auth.userId, 'TWO_FACTOR_DISABLE', {})

      return jsonResponse(
        {
          message: '2FA disabled successfully',
          twoFactorEnabled: false,
        },
        200,
        request.headers.get('origin')
      )
    }
  } catch (error: any) {
    if (error.name === 'ZodError') {
      logger.warn('2FA toggle validation error:', error.errors)
      return errorResponse(error.errors[0].message, 400, request.headers.get('origin'))
    }
    logger.error('2FA toggle error:', error)
    return errorResponse('Internal server error', 500, request.headers.get('origin'))
  }
}
```

## Status Codes

<ResponseField name="200" type="OK">
  Success
</ResponseField>

<ResponseField name="400" type="Bad Request">
  Validation error, invalid verification code, or 2FA not enabled when disabling
</ResponseField>

<ResponseField name="401" type="Unauthorized">
  Missing or invalid authentication token
</ResponseField>

<ResponseField name="404" type="Not Found">
  User not found
</ResponseField>

## Example Requests

### Generate QR Code

```bash theme={null}
curl -X POST https://auth.nullpass.xyz/api/auth/2fa \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "enable": true
  }'
```

### Confirm Enable

```bash theme={null}
curl -X POST https://auth.nullpass.xyz/api/auth/2fa \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "enable": true,
    "secret": "JBSWY3DPEHPK3PXP",
    "verificationCode": "123456"
  }'
```

### Disable 2FA

```bash theme={null}
curl -X POST https://auth.nullpass.xyz/api/auth/2fa \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "enable": false,
    "verificationCode": "123456"
  }'
```

## TOTP Configuration

* **Algorithm**: SHA1
* **Digits**: 6
* **Period**: 30 seconds
* **Window**: 2 (allows codes from ±1 time step)

## Supported Authenticator Apps

* Google Authenticator
* Authy
* 1Password
* Microsoft Authenticator
* Any TOTP-compatible app

## Security Notes

* Secret is stored in database (encrypted at rest if database encryption is enabled)
* QR code contains OTPAuth URL with issuer and email label
* Verification window of 2 allows for slight clock drift
* Secret is cleared when 2FA is disabled
* All 2FA operations are logged in audit trail

## Audit Events

* **TWO\_FACTOR\_ENABLE**: 2FA enabled
* **TWO\_FACTOR\_DISABLE**: 2FA disabled


## OpenAPI

````yaml POST /auth/2fa
openapi: 3.1.0
info:
  title: Null Pass API
  description: >-
    Internal API documentation for Null Pass authentication and service
    management system
  version: 1.0.0
servers:
  - url: https://auth.nullpass.xyz/api
security: []
paths:
  /auth/2fa:
    post:
      tags:
        - Authentication
      summary: Enable 2FA
      description: Enable two-factor authentication
      responses:
        '200':
          description: 2FA enabled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/2FASetupResponse'
      security:
        - bearerAuth: []
components:
  schemas:
    2FASetupResponse:
      type: object
      properties:
        secret:
          type: string
        qrCode:
          type: string
        backupCodes:
          type: array
          items:
            type: string
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

````