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
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
code | string | ✓ | exactly 6 chars (Length(6, 6)) | 6-digit TOTP code from the authenticator. Backup codes are not accepted here. |
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | ✓ | JWT from POST /auth/login |
Response
200 OK — ApiResponseOf<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"
]
}
}| Field | Type | Notes |
|---|---|---|
backupCodes | string[] | New one-time codes in XXXX-XXXX format. Count = auth.backup_code_count. Server stores bcrypt hashes only. |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
400 | auth.2fa.not_enabled | User.twoFactorEnabled = false — enable 2FA first |
400 | auth.2fa.invalid_code | TOTP 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
- Lookup
User; asserttwoFactorEnabled = trueANDtwoFactorSecret IS NOT NULL. - Decrypt
User.twoFactorSecretandotplib.verify()the submitted code withepochTolerance = auth.totp_window. - Generate
auth.backup_code_countnew codes (XXXX-XXXX, no0/O/1/I); bcrypt-hash each (auth.salt_rounds). - Inside one transaction:
DELETE FROM BackupCode WHERE userId = :userId(drop the entire previous batch).- Insert the new batch.
- Audit log:
[2fa] Backup codes regenerated for user {userId}. - 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
Authorization
bearer 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
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/auth/two-factor.controller.ts | 76–86 (regenerateBackupCodes) |
| DTO (request) | apps/api-core/src/modules/auth/dto/two-factor.dto.ts | 4–7 (VerifyTotpDto) |
| DTO (response) | apps/api-core/src/modules/auth/dto/response.dto.ts | 126–129 (BackupCodesResponseDto) |
| Service | apps/api-core/src/modules/auth/two-factor.service.ts | 179–219 (regenerateBackupCodes) |
| Prisma model | packages/prisma/prisma/schema.prisma | BackupCode, User.twoFactorEnabled |