BIO.RE
Authentication

Verify TOTP & Activate 2FA

Verify the 6-digit TOTP code against the previously stored secret. On success, flip 2FA on, generate one-time backup codes, and revoke all existing sessions.

POST /api/v1/auth/2fa/verify — 🔑 Bearer · Rate limit: 5 req / hour

Verifies the 6-digit code against the secret stored by POST /auth/2fa/setup (or /setup-init). On success: flips User.twoFactorEnabled = true, generates auth.backup_code_count one-time backup codes, and revokes all existing sessions for the user (a re-login is required, this time with 2FA).

Backup codes are returned ONCE. Display them and prompt the user to download / print / save to a password manager. The server only stores bcrypt hashes — there is no way to fetch them again. To rotate, call POST /auth/2fa/backup-codes/regenerate.

All sessions for this user are revoked on success — including the one calling this endpoint. The next request with the old access token will succeed only until the JWT expires (auth.access_token_ttl_seconds); the refresh cookie is dead immediately. Plan to redirect to a 2FA-enabled login flow right after handling the response.

Request

Body — VerifyTotpDto

FieldTypeRequiredValidationNotes
codestringexactly 6 chars (Length(6, 6))6-digit TOTP code from the authenticator
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[]One-time codes in XXXX-XXXX format (alphabet skips 0/O/1/I to avoid confusion). Count = auth.backup_code_count (admin-managed config; default 10). Server stores bcrypt hashes only — these strings are never recoverable.

Errors

HTTPcode / i18nKeyReason
400auth.2fa.setup_not_initiatedNo User.twoFactorSecret — call /setup first
400auth.2fa.already_enabledUser.twoFactorEnabled is already true
400auth.2fa.invalid_codeTOTP code doesn't match (within auth.totp_window drift tolerance)
401(guard)Missing / invalid bearer token
429(throttle)Rate limit exceeded (5 req/hour)

Side effects

  1. Lookup User; assert twoFactorSecret IS NOT NULL AND twoFactorEnabled = false.
  2. Decrypt User.twoFactorSecret (decryptPii — AES-256-GCM; handles legacy plaintext too).
  3. otplib.verify() against decrypted secret with epochTolerance = auth.totp_window (default 1 step = ±30s).
  4. On success, inside one transaction:
    • Generate auth.backup_code_count codes (XXXX-XXXX format, no 0/O/1/I).
    • Bcrypt-hash each code (auth.salt_rounds) and insert into BackupCode table.
    • Set User.twoFactorEnabled = true.
    • Update all Session rows for this user → revoked = true, revokedAt = now().
  5. Audit log: [2fa] Activated for user {userId}.

Code samples

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

async function verifyTwoFactor(accessToken: string, code: string): Promise<BackupCodes> {
  const res = await fetch('https://api.bio.re/api/v1/auth/2fa/verify', {
    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 ?? '2FA verify failed'), {
      code: json?.error?.code,
    });
  }
  return json.data;
}
import { useMutation, useQueryClient } from '@tanstack/react-query';

export function useVerifyTwoFactor() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (code: string) => {
      const res = await fetch('/api/v1/auth/2fa/verify', {
        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 ?? '2FA verify failed'), {
          code: json?.error?.code,
          i18nKey: json?.error?.i18nKey,
        });
      }
      return json.data as BackupCodes;
    },
    onSuccess: () => {
      // Sessions revoked server-side — invalidate identity + force re-auth flow
      qc.invalidateQueries({ queryKey: ['auth', 'me'] });
    },
  });
}

Try it

POST
/api/v1/auth/2fa/verify
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/verify" \  -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.ts52–62 (verifyAndActivate)
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.ts55–101 (verifyAndActivate)
Prisma modelspackages/prisma/prisma/schema.prismaUser.twoFactorEnabled, BackupCode, Session.revoked

On this page