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.
POST /api/v1/auth/2fa/setup-init — 🔑 Bearer · Rate limit: 10 req / hour
Alias of POST /auth/2fa/setup with one cosmetic difference: the response shape includes recoveryCodes: null so the frontend can use a single response type across setup → verify without conditional fields.
Behavior is identical to POST /auth/2fa/setup — same secret generation, same encrypted persistence, same already_enabled guard. The only difference is the explicit recoveryCodes: null field. Recovery codes are still issued by POST /auth/2fa/verify, never here.
Why two endpoints
The original /setup returns { secret, qrCodeDataUrl, otpauthUrl }. After /verify succeeds, the response is { backupCodes: [...] }. The shapes don't overlap. /setup-init returns { secret, qrCodeUrl, otpauthUrl, recoveryCodes: null } so a single TS type covers both stages — recoveryCodes flips from null (setup-init) to string[] (verify), nothing else changes.
Use /setup if you want the canonical endpoint. Use /setup-init if you need the stable response shape across the two-step flow.
Request
No body. The current user is resolved from the bearer token.
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | ✓ | JWT from POST /auth/login |
Response
200 OK — ApiResponseOf<TwoFactorSetupInitResponseDto>
{
"success": true,
"data": {
"secret": "JBSWY3DPEHPK3PXP",
"qrCodeUrl": "data:image/png;base64,iVBORw0KGgo...",
"otpauthUrl": "otpauth://totp/BIO.RE:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=BIO.RE",
"recoveryCodes": null
}
}| Field | Type | Notes |
|---|---|---|
secret | string (base32) | TOTP secret for manual entry |
qrCodeUrl | string | data:image/png;base64,… — note the field name is qrCodeUrl, not qrCodeDataUrl (the only naming drift from /setup) |
otpauthUrl | string | otpauth://totp/... deep-link |
recoveryCodes | null | Always null here. Recovery codes are issued by POST /auth/2fa/verify after the user proves TOTP works. Forward-compat field so the type stays stable. |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
400 | auth.2fa.already_enabled | User.twoFactorEnabled is already true |
401 | (guard) | Missing / invalid bearer token |
404 | auth.2fa.user_not_found | Token decoded but user row missing |
429 | (throttle) | Rate limit exceeded (10 req/hour) |
Side effects
Identical to POST /auth/2fa/setup:
- Lookup
User; asserttwoFactorEnabled = false. - Generate fresh TOTP secret (
otplib.generateSecret()). - Build
otpauth://URL + render QR PNG data URL. - AES-256-GCM-encrypt the secret and persist to
User.twoFactorSecret. - No backup code generation — that's deferred to
/verify.
Code samples
curl -X POST https://api.bio.re/api/v1/auth/2fa/setup-init \
-H "Authorization: Bearer $ACCESS_TOKEN"type TwoFactorSetupInit = {
secret: string;
qrCodeUrl: string;
otpauthUrl: string;
recoveryCodes: string[] | null; // null on setup-init, string[] after verify
};
async function initTwoFactorSetup(accessToken: string): Promise<TwoFactorSetupInit> {
const res = await fetch('https://api.bio.re/api/v1/auth/2fa/setup-init', {
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-init failed'), {
code: json?.error?.code,
});
}
return json.data;
}import { useMutation } from '@tanstack/react-query';
export function useInitTwoFactorSetup() {
return useMutation({
mutationFn: async () => {
const res = await fetch('/api/v1/auth/2fa/setup-init', { method: 'POST' });
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? '2FA setup-init failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
return json.data as TwoFactorSetupInit;
},
});
}Try it
Authorization
bearer In: header
Response Body
application/json
application/json
application/json
curl -X POST "https://loading/api/v1/auth/2fa/setup-init"{
"success": true,
"data": {
"secret": "JBSWY3DPEHPK3PXP",
"qrCodeUrl": "string",
"otpauthUrl": "otpauth://totp/BIO.RE:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=BIO.RE",
"recoveryCodes": [
"string"
]
}
}{
"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
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/auth/two-factor.controller.ts | 30–50 (setupInit) |
| DTO (response) | apps/api-core/src/modules/auth/dto/two-factor-setup.dto.ts | 15–34 (TwoFactorSetupInitResponseDto) |
| Service | apps/api-core/src/modules/auth/two-factor.service.ts | 31–50 (generateSetup, shared) |
| Prisma model | packages/prisma/prisma/schema.prisma | User.twoFactorSecret, User.twoFactorEnabled |
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.
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.