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.
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | ✓ | JWT from POST /auth/login |
Response
201 Created — ApiResponseOf<TwoFactorSetupResponseDto>
{
"success": true,
"data": {
"secret": "JBSWY3DPEHPK3PXP",
"qrCodeDataUrl": "data:image/png;base64,iVBORw0KGgo...",
"otpauthUrl": "otpauth://totp/BIO.RE:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=BIO.RE"
}
}| Field | Type | Notes |
|---|---|---|
secret | string (base32) | TOTP secret for manual entry into the authenticator |
qrCodeDataUrl | string | data:image/png;base64,… — render directly in <img src=...> |
otpauthUrl | string | otpauth://totp/... — useful for native deep-links into authenticator apps |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
400 | auth.2fa.already_enabled | User.twoFactorEnabled is already true — disable first via POST /auth/2fa/disable |
401 | (guard) | Missing / invalid bearer token |
404 | auth.2fa.user_not_found | Token decoded but user row missing (deleted account) |
429 | (throttle) | Rate limit exceeded (10 req/hour) |
Side effects
- Lookup
Userby id; asserttwoFactorEnabled = false(else throwalready_enabled). - Generate a fresh TOTP secret via
otplib.generateSecret()(Google Authenticator / Authy compatible). - Build
otpauth://URL withBIO.REissuer + user email as label. - Render QR code as PNG data URL via
qrcode.toDataURL(). - Encrypt the secret with AES-256-GCM (
encryptPii) and persist toUser.twoFactorSecret. Until/verify, this secret exists buttwoFactorEnabled = false— login is unaffected. - Calling this endpoint twice replaces the secret — only the most recent
/setupmatches the/verifystep.
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
Authorization
bearer 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
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/auth/two-factor.controller.ts | 20–28 (generateSetup) |
| DTO (response) | apps/api-core/src/modules/auth/dto/response.dto.ts | 114–123 (TwoFactorSetupResponseDto) |
| Service | apps/api-core/src/modules/auth/two-factor.service.ts | 31–50 (generateSetup) |
| PII encryption | apps/api-core/src/common/pii-encryption.ts | encryptPii() (AES-256-GCM) |
| Prisma model | packages/prisma/prisma/schema.prisma | User.twoFactorSecret, User.twoFactorEnabled |
Validate Reactivation Token
Pre-flight check for an account reactivation token from an email link. Returns account status without performing reactivation.
Init 2FA Setup (Stable Shape)
Alias of POST /auth/2fa/setup with an explicit recoveryCodes:null contract — kept thin so the frontend type stays stable across setup → verify.