Backup Codes 備用碼
MFA 最容易被忽略的不是登入時的驗證碼,而是:
如果使用者的 MFA 裝置不見了,怎麼辦?例如:
- 手機遺失。
- Authenticator app 被刪掉。
- 換手機但沒有轉移 TOTP。
- Passkey 裝置不在身邊。
- 硬體安全金鑰壞掉。
- 出國收不到 SMS。
如果沒有備援,使用者可能永遠進不去帳號。
但如果備援做得太鬆,攻擊者就會繞過 MFA。
Backup Codes 的定位就是:
在主要 MFA 失效時,用預先保存的一次性代碼恢復登入能力。它是救生繩,不是日常登入方式。
一、 一句話回答
如果面試官問:
Backup Codes 怎麼設計?有什麼安全注意事項?
可以先回答:
Backup Codes 是 MFA 的恢復用一次性代碼,通常在使用者啟用 MFA 時產生一組高熵隨機碼,讓使用者離線保存。伺服器不應保存明文,只保存 hash 或 HMAC;每個 code 只能使用一次,使用後立即失效並寄出安全通知。Backup Codes 通常只在使用者無法使用 TOTP、Passkey、SMS 或其他 MFA 時使用,使用時仍應搭配帳號密碼或既有登入上下文,並受速率限制、錯誤次數限制與稽核紀錄保護。重新產生 backup codes 時應要求近期重新驗證,並讓舊 codes 全部失效。它本質上是一種備援 authenticator,強度取決於產生方式、保存方式、使用限制與恢復流程是否嚴格。
短一點可以說:
Backup Codes 是一次性、預先保存、只顯示一次的 MFA 恢復碼;
伺服器只存 hash,使用後失效,重新產生會撤銷舊碼。二、 Backup Codes 解決什麼問題
假設使用者啟用了 TOTP。
正常登入流程:
密碼 -> TOTP -> 登入成功但手機遺失後:
密碼 -> 沒有 TOTP -> 卡住這時 backup code 可以作為備援:
密碼 -> backup code -> 登入成功 -> 要求重新設定 MFA它解決的是可用性問題:
- 裝置遺失。
- Authenticator 無法使用。
- 使用者換手機。
- 出差時沒有安全金鑰。
- 主要 MFA provider 故障。
但它也增加攻擊面:
如果 backup code 被偷,攻擊者可能繞過主要 MFA。所以 backup code 必須被當成敏感憑證管理。
三、 Backup Codes 是哪一種 authenticator
在 NIST 的語境裡,這類預先保存、登入時查出來輸入的秘密,接近 look-up secret 或 recovery code 的概念。
比較通俗地說:
它是一組事先發給使用者保存的備用秘密。特性:
| 特性 | 說明 |
|---|---|
| 預先產生 | 通常啟用 MFA 時產生 |
| 離線保存 | 使用者可以列印或存到密碼管理器 |
| 一次性 | 每組 code 只能用一次 |
| 高熵 | 不應可猜 |
| 只顯示一次 | 伺服器不再顯示明文 |
| 可撤銷 | 重新產生時舊碼失效 |
它不應該像密碼一樣長期重複使用。
四、 什麼時候產生 Backup Codes
常見時機:
1. 啟用 MFA 成功後
例如使用者啟用 TOTP:
掃 QR code -> 輸入第一組 TOTP -> 啟用成功 -> 顯示 backup codes這是最常見做法。
因為你已經確認使用者真的完成 MFA 設定。
2. 使用者主動重新產生
使用者可能覺得舊 backup codes 遺失或外洩。
這時可以提供:
重新產生 backup codes但必須要求近期重新驗證。
3. 高風險帳號要求多種備援
例如管理員、金融、醫療、企業帳號,除了 backup codes 之外,還可以要求:
- 第二個 TOTP。
- 第二個 Passkey。
- 硬體安全金鑰。
- 管理員協助恢復。
不要把唯一備援全部押在一張紙上。
五、 產生方式
Backup code 不能太短。
例如:
123456這種 6 位數太容易被猜,尤其如果沒有嚴格重試限制。
比較常見的格式:
8H7K-2D91
M4Q9-LA30
P2Z8-XC77
Q6RT-91BC或:
7f3a-b91d-2c8e設計重點:
- 使用 cryptographically secure random。
- 避免容易混淆字元,例如
O和0、I和1。 - 分組顯示,方便手動輸入。
- 保持足夠熵。
- 產生 8 到 12 組左右。
Node.js 範例:
import crypto from 'crypto'
const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
function generateBackupCode() {
let raw = ''
for (let i = 0; i < 10; i += 1) {
const index = crypto.randomInt(0, alphabet.length)
raw += alphabet[index]
}
return `${raw.slice(0, 5)}-${raw.slice(5)}`
}
function generateBackupCodes(count = 10) {
return Array.from({ length: count }, () => generateBackupCode())
}實務上可以依安全需求提高長度。
六、 儲存方式
伺服器不應保存明文 backup code。
因為 backup code 是可以通過 MFA 的秘密。
保存方式:
code_hash = HMAC(server_secret, user_id + code)或使用適合短 secret 的 keyed hash。
資料表可以像:
create table backup_codes (
id uuid primary key,
user_id uuid not null references users(id),
code_hash text not null,
used_at timestamp,
created_at timestamp not null,
batch_id uuid not null,
unique (user_id, code_hash)
);如果要支援重新產生:
create table backup_code_batches (
id uuid primary key,
user_id uuid not null references users(id),
created_at timestamp not null,
revoked_at timestamp
);重點:
- 明文只顯示一次。
- DB 只存 hash / HMAC。
- log 不記明文。
- 使用後標記
used_at。 - 重新產生時 revoke 舊 batch。
- 查詢時用 timing-safe comparison 或固定流程。
七、 顯示與保存體驗
Backup codes 只顯示一次。
頁面可以提供:
- 複製。
- 下載文字檔。
- 列印。
- 已保存確認 checkbox。
但要注意:
- 不要存在瀏覽器 localStorage。
- 下載檔名不要包含敏感帳號資訊。
- 頁面加
Cache-Control: no-store。 - 使用者返回後不再顯示明文。
- 重新查看必須重新產生,而不是解密顯示舊碼。
可以提示:
請將備用碼存放在密碼管理器或列印後放在安全位置。
每組備用碼只能使用一次。不要提示:
截圖保存到相簿就好。因為相簿可能同步到雲端,裝置也可能被他人看到。
八、 使用流程
Backup code 通常在 MFA challenge 畫面提供:
無法使用驗證器?使用備用碼流程:
使用成功後建議:
- 標記該 code 已使用。
- 寄出安全通知。
- 記錄 audit log。
- 提醒剩餘 backup codes 數量。
- 若剩餘很少,提示重新產生。
- 若是高風險帳號,可要求重新設定 MFA。
九、 使用後要不要強制重設 MFA
看場景。
如果 backup code 是因為:
手機剛好不在身邊不一定要強制重設 MFA。
但如果使用者選擇:
我無法再使用 TOTP 裝置那就應該要求:
- 使用 backup code 登入。
- 通過後進入 MFA 恢復流程。
- 重新設定 TOTP / Passkey。
- 產生新的 backup codes。
- 可能撤銷舊 sessions。
高風險產品可採取更嚴格策略:
使用 backup code 後,必須重新確認 MFA 設定。至少應該寄通知,讓使用者知道有 backup code 被使用。
十、 重新產生 Backup Codes
重新產生時要注意:
新的 codes 產生後,舊的全部失效。流程:
重新產生是高風險操作,應要求:
- 使用者已登入。
- 近期重新驗證。
- 最好通過既有 MFA。
- 寄通知。
- 寫 audit log。
十一、 速率限制與錯誤處理
Backup code 必須有嘗試限制。
否則攻擊者可以暴力猜。
限制維度:
- user。
- session。
- IP。
- device。
- MFA challenge。
例如:
同一 MFA challenge 最多 5 次。
同一 user 10 分鐘最多 10 次。
同一 IP 1 分鐘最多 30 次。錯誤訊息不要透露:
- code 是否存在。
- code 是否已使用。
- code 是否屬於這個 user。
對外統一:
備用碼無效或已使用。內部 audit log 可以記細節:
backup_code_invalid
backup_code_used
backup_code_replay_attempt
backup_code_rate_limited十二、 API 設計範例
產生 backup codes
app.post('/api/mfa/backup-codes/regenerate', requireAuth, async (req, res) => {
await requireFreshMfa(req.session)
const codes = generateBackupCodes(10)
const batchId = crypto.randomUUID()
await db.transaction(async (tx) => {
await tx.backupCodeBatch.updateMany({
where: {
userId: req.user.id,
revokedAt: null,
},
data: {
revokedAt: new Date(),
},
})
await tx.backupCodeBatch.create({
data: {
id: batchId,
userId: req.user.id,
createdAt: new Date(),
},
})
await tx.backupCode.createMany({
data: codes.map((code) => ({
id: crypto.randomUUID(),
userId: req.user.id,
batchId,
codeHash: hashBackupCode(req.user.id, code),
createdAt: new Date(),
})),
})
})
await auditLog('backup_codes_regenerated', {
userId: req.user.id,
batchId,
})
res.json({ codes })
})驗證 backup code
app.post('/api/mfa/backup-codes/verify', async (req, res) => {
const { mfaChallengeId, code } = req.body
const challenge = await getMfaChallenge(mfaChallengeId)
if (!challenge || challenge.expiresAt < new Date()) {
return res.status(400).json({ code: 'MFA_INVALID' })
}
await assertMfaAttemptLimit(challenge.userId, req.ip)
const codeHash = hashBackupCode(challenge.userId, normalizeCode(code))
const backupCode = await db.backupCode.findFirst({
where: {
userId: challenge.userId,
codeHash,
usedAt: null,
batch: {
revokedAt: null,
},
},
})
if (!backupCode) {
await recordMfaFailure(challenge.userId, 'backup_code')
return res.status(400).json({ code: 'MFA_INVALID' })
}
await db.backupCode.update({
where: { id: backupCode.id },
data: { usedAt: new Date() },
})
await markMfaChallengeVerified(mfaChallengeId)
await auditLog('backup_code_used', {
userId: challenge.userId,
backupCodeId: backupCode.id,
})
res.json({ verified: true })
})實務上要注意 transaction,避免同一 code 被並發使用兩次。
十三、 和帳號恢復的關係
Backup codes 是帳號恢復的一部分,但不等於完整帳號恢復。
如果使用者:
沒有 TOTP
也沒有 backup codes
也沒有其他 authenticator系統就要進入更高風險的恢復流程。
這可能包含:
- 已登入可信裝置。
- 第二個 email / phone。
- passkey / security key。
- 企業管理員確認。
- 高信任身份驗證。
- 人工審核。
越高風險的產品,越不能只靠客服問答恢復 MFA。
備用碼的存在,是為了減少進入人工恢復流程的機率。
十四、 常見錯誤
1. 明文保存 backup codes
錯誤:
backup_code = ABCDE-12345正確:
backup_code_hash = HMAC(server_secret, user_id + code)2. Backup code 可重複使用
錯誤:
同一組 code 可以用很多次。正確:
每組 code 使用後立即 used_at。3. 重新產生不撤銷舊碼
錯誤:
新舊 backup codes 都有效。正確:
重新產生新 batch 時,舊 batch 全部 revoked。4. 產生後可以再次查看
錯誤:
使用者可以隨時查看所有明文 backup codes。正確:
只顯示一次。忘記就重新產生,舊碼失效。5. 沒有使用通知
錯誤:
backup code 被用掉,使用者完全不知道。正確:
使用後寄安全通知,並顯示剩餘數量。6. 沒有速率限制
錯誤:
backup code 可以無限猜。正確:
限制 user、session、IP、challenge 的嘗試次數。十五、 面試追問題庫
Q1:Backup Codes 是什麼?
Backup Codes 是 MFA 的備援恢復碼,通常在啟用 MFA 時產生一組一次性代碼,讓使用者在 TOTP、Passkey、SMS 或安全金鑰不可用時,仍能恢復登入。
Q2:Backup Codes 要不要存明文?
不應該。伺服器只應保存 hash 或 HMAC 結果,明文只在產生時顯示一次。使用者如果遺失,就重新產生一批,並讓舊碼全部失效。
Q3:Backup Codes 可以重複使用嗎?
不應該。每組 code 成功使用後要立即失效,避免被重放。
Q4:重新產生 Backup Codes 要注意什麼?
要要求近期重新驗證,最好通過既有 MFA;產生新 codes 後舊 batch 應全部撤銷,並寄出通知與寫 audit log。
Q5:Backup Codes 被使用後要做什麼?
標記 used、寄安全通知、寫 audit log、顯示剩餘數量。若剩餘很少,要提示重新產生;若使用者表示主要 MFA 已遺失,應引導重新設定 MFA。
Q6:Backup Codes 算不算 MFA?
它通常是備援 authenticator。實際 assurance 取決於使用流程。例如使用者已通過密碼,再輸入預先保存的 backup code,可以作為恢復主要 MFA 的一種方式。但它的安全性取決於 code 的保存方式和系統限制。
Q7:如果使用者沒有 backup codes,也沒有 MFA 裝置怎麼辦?
這要進入帳號恢復流程。低風險產品可能用已驗證 email、可信裝置;高風險產品可能需要人工審核、企業管理員確認或重新身份驗證。不能用弱安全問題直接關閉 MFA。
十六、 實作題
題目一:設計 Backup Codes 資料表
要求:
- 一個 user 有多組 backup codes。
- 每組 code 只使用一次。
- code 不明文保存。
- 支援 batch revoke。
- 可記錄 used_at。
- 重新產生時舊 batch 全部失效。
題目二:設計 Backup Code 使用流程
情境:
使用者密碼正確,但 TOTP 裝置不見了。請設計:
- 使用者選擇使用 backup code。
- 輸入 backup code。
- 系統做速率限制。
- 比對 hash。
- 成功後標記 used。
- 建立 MFA session。
- 寄通知。
- 引導重新設定 MFA。
題目三:設計重新產生流程
要求:
- 必須登入。
- 必須近期重新驗證。
- 最好通過既有 MFA。
- 新 batch 產生後舊 batch revoked。
- 明文只顯示一次。
- 寄通知與 audit log。
十七、 資深視角
1. Backup Codes 是 MFA 的安全出口
任何強 MFA 都需要備援。
但備援不能比正門弱太多。
如果主要登入是:
密碼 + TOTP但客服可以只靠生日關閉 TOTP,那攻擊者就會走客服。
Backup codes 是把恢復流程提前設計好,避免每次都落到脆弱的人工流程。
2. 備用碼要被當成憑證
Backup code 不是提示文字,也不是單純 token。
它是可以通過 MFA 的秘密。
因此要有:
- 安全產生。
- 安全顯示。
- 安全保存。
- 使用後撤銷。
- 重試限制。
- 使用通知。
- 稽核紀錄。
3. 使用者體驗會影響安全
如果你只是顯示一串碼,使用者可能:
- 截圖。
- 傳到聊天軟體。
- 存在桌面。
- 完全沒保存。
- 保存後找不到。
好的產品會引導:
- 存到密碼管理器。
- 列印放在安全位置。
- 確認已保存。
- 告訴使用者每組只能用一次。
- 在剩餘數量很少時提醒更新。
4. 高風險系統要多備援而不是弱備援
高風險帳號不要只提供一種 backup code。
更好的做法是鼓勵:
- 第二個 Passkey。
- 第二個 TOTP。
- 硬體安全金鑰。
- recovery contact,視產品風險。
- 企業管理員恢復。
讓使用者有多個強備援,而不是把弱流程開太大。
總結
| 問題 | 回答重點 |
|---|---|
| Backup Codes | MFA 恢復用一次性代碼 |
| 產生時機 | 啟用 MFA 後或使用者主動重新產生 |
| 顯示 | 明文只顯示一次 |
| 保存 | 伺服器只存 hash/HMAC |
| 使用 | 每組 code 只能使用一次 |
| 重新產生 | 舊 batch 全部撤銷 |
| 使用後 | 標記 used、寄通知、寫 audit log |
| 風險 | 被偷可繞過主要 MFA |
| 速率限制 | 必須限制 user、IP、session、challenge |
| 好體驗 | 引導存到密碼管理器或列印安全保存 |
一句完整的面試回答可以是:
Backup Codes 是 MFA 的備援恢復碼,通常在使用者啟用 TOTP、Passkey 或其他 MFA 成功後產生一組高熵一次性代碼,讓使用者在主要 MFA 裝置遺失時仍能恢復登入。伺服器不應保存明文 code,而應保存 hash 或 HMAC;明文只顯示一次,使用者需要自己保存。每組 code 成功使用後立即標記 used,不可重放;重新產生新 codes 時應撤銷舊 batch,並要求近期重新驗證。使用 backup code 時要做速率限制、錯誤次數限制、通知與 audit log,必要時引導使用者重新設定 MFA。Backup Codes 是安全出口,但也是攻擊面,設計上要把它當成另一種 authenticator,而不是方便的後門。