BIO.RE
Authentication

Regenerate Backup Codes

Issue a fresh set of backup codes after the user proves their authenticator still works (TOTP code, not backup code). Old codes are deleted atomically.

POST /api/v1/auth/2fa/backup-codes/regenerate — 🔑 Bearer · Rate limit: 3 req / hour

Issues a fresh batch of backup codes (auth.backup_code_count, default 10) and atomically deletes the previous batch. Requires a valid TOTP code — backup codes are explicitly rejected here, because the operation must prove the user still has access to the authenticator.

The previous batch is deleted before the response is returned. Any unspent backup codes the user had on paper / in a vault are now dead. Show the new batch and prompt the user to replace stored copies.

Why TOTP-only (no backup-code path here)? If a stolen backup code could itself rotate the backup codes, an attacker with one code could lock the legitimate user out. Requiring a live TOTP keeps regeneration tied to authenticator possession.

Request

Body — VerifyTotpDto

FieldTypeRequiredValidationNotes
codestringexactly 6 chars (Length(6, 6))6-digit TOTP code from the authenticator. Backup codes are not accepted here.
HeaderRequiredNotes
Authorization: Bearer <accessToken>JWT from POST /auth/login

Response

200 OKApiResponseOf<BackupCodesResponseDto>

{
  "success": true,
  "data": {
    "backupCodes": [
      "ABCD-2345",
      "EFGH-6789",
      "JKLM-2345",
      "NPQR-6789",
      "STUV-2345",
      "WXYZ-6789",
      "23AB-CDEF",
      "45GH-JKLM",
      "67NP-QRST",
      "89UV-WXYZ"
    ]
  }
}
FieldTypeNotes
backupCodesstring[]New one-time codes in XXXX-XXXX format. Count = auth.backup_code_count. Server stores bcrypt hashes only.

Errors

HTTPcode / i18nKeyReason
400auth.2fa.not_enabledUser.twoFactorEnabled = false — enable 2FA first
400auth.2fa.invalid_codeTOTP code doesn't match (within auth.totp_window drift)
401(guard)Missing / invalid bearer token
429(throttle)Rate limit exceeded (3 req/hour)

Side effects

  1. Lookup User; assert twoFactorEnabled = true AND twoFactorSecret IS NOT NULL.
  2. Decrypt User.twoFactorSecret and otplib.verify() the submitted code with epochTolerance = auth.totp_window.
  3. Generate auth.backup_code_count new codes (XXXX-XXXX, no 0/O/1/I); bcrypt-hash each (auth.salt_rounds).
  4. Inside one transaction:
    • DELETE FROM BackupCode WHERE userId = :userId (drop the entire previous batch).
    • Insert the new batch.
  5. Audit log: [2fa] Backup codes regenerated for user {userId}.
  6. Sessions are NOT revoked here — the user keeps their session because the authenticator possession check just succeeded.

Code samples

curl -X POST https://api.bio.re/api/v1/auth/2fa/backup-codes/regenerate \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"code": "123456"}'
type BackupCodes = { backupCodes: string[] };

async function regenerateBackupCodes(accessToken: string, code: string): Promise<BackupCodes> {
  const res = await fetch('https://api.bio.re/api/v1/auth/2fa/backup-codes/regenerate', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ code }),
  });
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw Object.assign(new Error(json?.error?.message ?? 'Regenerate failed'), {
      code: json?.error?.code,
    });
  }
  return json.data;
}
import { useMutation } from '@tanstack/react-query';

export function useRegenerateBackupCodes() {
  return useMutation({
    mutationFn: async (code: string) => {
      const res = await fetch('/api/v1/auth/2fa/backup-codes/regenerate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ code }),
      });
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'Regenerate failed'), {
          code: json?.error?.code,
          i18nKey: json?.error?.i18nKey,
        });
      }
      return json.data as BackupCodes;
    },
  });
}

Try it

POST
/api/v1/auth/2fa/backup-codes/regenerate
AuthorizationBearer <token>

In: header

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

application/json

curl -X POST "https://loading/api/v1/auth/2fa/backup-codes/regenerate" \  -H "Content-Type: application/json" \  -d '{    "code": "123456"  }'
{
  "success": true,
  "data": {
    "backupCodes": [
      "ABC12345",
      "DEF67890"
    ]
  }
}
{
  "success": false,
  "error": {
    "code": "AUTH_UNAUTHORIZED",
    "message": "Invalid credentials",
    "i18nKey": "auth.login.invalid_credentials",
    "i18nVars": {
      "field": "email"
    },
    "details": [
      {
        "message": "email must be an email"
      }
    ],
    "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}
{
  "success": false,
  "error": {
    "code": "AUTH_UNAUTHORIZED",
    "message": "Invalid credentials",
    "i18nKey": "auth.login.invalid_credentials",
    "i18nVars": {
      "field": "email"
    },
    "details": [
      {
        "message": "email must be an email"
      }
    ],
    "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}

Source

SourcePathLines
Controllerapps/api-core/src/modules/auth/two-factor.controller.ts76–86 (regenerateBackupCodes)
DTO (request)apps/api-core/src/modules/auth/dto/two-factor.dto.ts4–7 (VerifyTotpDto)
DTO (response)apps/api-core/src/modules/auth/dto/response.dto.ts126–129 (BackupCodesResponseDto)
Serviceapps/api-core/src/modules/auth/two-factor.service.ts179–219 (regenerateBackupCodes)
Prisma modelpackages/prisma/prisma/schema.prismaBackupCode, User.twoFactorEnabled

On this page