BIO.RE
Authentication

Generate 2FA Setup

Start TOTP enrollment. Generates a server-side TOTP secret, encrypts it on the User row, and returns the secret + QR code so the user can pair an authenticator app.

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

Generates a TOTP secret server-side, AES-256-GCM-encrypts it onto User.twoFactorSecret, and returns the secret + QR code data URL + otpauth:// URL. Does not yet activate 2FA — the user must prove they can read TOTP from their authenticator via POST /auth/2fa/verify before backup codes are issued and 2FA is flipped on.

The secret is also rendered into the QR code. Show it once and recommend the user store it via the authenticator. Don't log it. Don't echo it back over an unrelated channel.

Backup codes are not returned here — they are issued by POST /auth/2fa/verify after the user proves their TOTP works. This avoids a window where the user has backup codes for a 2FA setup they never completed.

Request

No body. The current user is resolved from the bearer token.

HeaderRequiredNotes
Authorization: Bearer <accessToken>JWT from POST /auth/login

Response

201 CreatedApiResponseOf<TwoFactorSetupResponseDto>

{
  "success": true,
  "data": {
    "secret": "JBSWY3DPEHPK3PXP",
    "qrCodeDataUrl": "data:image/png;base64,iVBORw0KGgo...",
    "otpauthUrl": "otpauth://totp/BIO.RE:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=BIO.RE"
  }
}
FieldTypeNotes
secretstring (base32)TOTP secret for manual entry into the authenticator
qrCodeDataUrlstringdata:image/png;base64,… — render directly in <img src=...>
otpauthUrlstringotpauth://totp/... — useful for native deep-links into authenticator apps

Errors

HTTPcode / i18nKeyReason
400auth.2fa.already_enabledUser.twoFactorEnabled is already true — disable first via POST /auth/2fa/disable
401(guard)Missing / invalid bearer token
404auth.2fa.user_not_foundToken decoded but user row missing (deleted account)
429(throttle)Rate limit exceeded (10 req/hour)

Side effects

  1. Lookup User by id; assert twoFactorEnabled = false (else throw already_enabled).
  2. Generate a fresh TOTP secret via otplib.generateSecret() (Google Authenticator / Authy compatible).
  3. Build otpauth:// URL with BIO.RE issuer + user email as label.
  4. Render QR code as PNG data URL via qrcode.toDataURL().
  5. Encrypt the secret with AES-256-GCM (encryptPii) and persist to User.twoFactorSecret. Until /verify, this secret exists but twoFactorEnabled = false — login is unaffected.
  6. Calling this endpoint twice replaces the secret — only the most recent /setup matches the /verify step.

Code samples

curl -X POST https://api.bio.re/api/v1/auth/2fa/setup \
  -H "Authorization: Bearer $ACCESS_TOKEN"
type TwoFactorSetup = {
  secret: string;
  qrCodeDataUrl: string;
  otpauthUrl: string;
};

async function startTwoFactorSetup(accessToken: string): Promise<TwoFactorSetup> {
  const res = await fetch('https://api.bio.re/api/v1/auth/2fa/setup', {
    method: 'POST',
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw Object.assign(new Error(json?.error?.message ?? '2FA setup failed'), {
      code: json?.error?.code,
    });
  }
  return json.data;
}
import { useMutation } from '@tanstack/react-query';

export function useStartTwoFactorSetup() {
  return useMutation({
    mutationFn: async () => {
      const res = await fetch('/api/v1/auth/2fa/setup', { method: 'POST' });
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? '2FA setup failed'), {
          code: json?.error?.code,
          i18nKey: json?.error?.i18nKey,
        });
      }
      return json.data as TwoFactorSetup;
    },
  });
}

Try it

POST
/api/v1/auth/2fa/setup
AuthorizationBearer <token>

In: header

Response Body

application/json

application/json

curl -X POST "https://loading/api/v1/auth/2fa/setup"
{
  "success": true,
  "data": {
    "secret": "JBSWY3DPEHPK3PXP",
    "qrCodeDataUrl": "string",
    "otpauthUrl": "otpauth://totp/BIO.RE:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=BIO.RE"
  }
}
{
  "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.ts20–28 (generateSetup)
DTO (response)apps/api-core/src/modules/auth/dto/response.dto.ts114–123 (TwoFactorSetupResponseDto)
Serviceapps/api-core/src/modules/auth/two-factor.service.ts31–50 (generateSetup)
PII encryptionapps/api-core/src/common/pii-encryption.tsencryptPii() (AES-256-GCM)
Prisma modelpackages/prisma/prisma/schema.prismaUser.twoFactorSecret, User.twoFactorEnabled

On this page