跳至主要內容
Skip to content

Backup Codes 備用碼

MFA 最容易被忽略的不是登入時的驗證碼,而是:

text
如果使用者的 MFA 裝置不見了,怎麼辦?

例如:

  • 手機遺失。
  • Authenticator app 被刪掉。
  • 換手機但沒有轉移 TOTP。
  • Passkey 裝置不在身邊。
  • 硬體安全金鑰壞掉。
  • 出國收不到 SMS。

如果沒有備援,使用者可能永遠進不去帳號。

但如果備援做得太鬆,攻擊者就會繞過 MFA。

Backup Codes 的定位就是:

text
在主要 MFA 失效時,用預先保存的一次性代碼恢復登入能力。

它是救生繩,不是日常登入方式。

一、 一句話回答

如果面試官問:

Backup Codes 怎麼設計?有什麼安全注意事項?

可以先回答:

Backup Codes 是 MFA 的恢復用一次性代碼,通常在使用者啟用 MFA 時產生一組高熵隨機碼,讓使用者離線保存。伺服器不應保存明文,只保存 hash 或 HMAC;每個 code 只能使用一次,使用後立即失效並寄出安全通知。Backup Codes 通常只在使用者無法使用 TOTP、Passkey、SMS 或其他 MFA 時使用,使用時仍應搭配帳號密碼或既有登入上下文,並受速率限制、錯誤次數限制與稽核紀錄保護。重新產生 backup codes 時應要求近期重新驗證,並讓舊 codes 全部失效。它本質上是一種備援 authenticator,強度取決於產生方式、保存方式、使用限制與恢復流程是否嚴格。

短一點可以說:

text
Backup Codes 是一次性、預先保存、只顯示一次的 MFA 恢復碼;
伺服器只存 hash,使用後失效,重新產生會撤銷舊碼。

二、 Backup Codes 解決什麼問題

假設使用者啟用了 TOTP。

正常登入流程:

text
密碼 -> TOTP -> 登入成功

但手機遺失後:

text
密碼 -> 沒有 TOTP -> 卡住

這時 backup code 可以作為備援:

text
密碼 -> backup code -> 登入成功 -> 要求重新設定 MFA

它解決的是可用性問題:

  • 裝置遺失。
  • Authenticator 無法使用。
  • 使用者換手機。
  • 出差時沒有安全金鑰。
  • 主要 MFA provider 故障。

但它也增加攻擊面:

text
如果 backup code 被偷,攻擊者可能繞過主要 MFA。

所以 backup code 必須被當成敏感憑證管理。

三、 Backup Codes 是哪一種 authenticator

在 NIST 的語境裡,這類預先保存、登入時查出來輸入的秘密,接近 look-up secret 或 recovery code 的概念。

比較通俗地說:

text
它是一組事先發給使用者保存的備用秘密。

特性:

特性說明
預先產生通常啟用 MFA 時產生
離線保存使用者可以列印或存到密碼管理器
一次性每組 code 只能用一次
高熵不應可猜
只顯示一次伺服器不再顯示明文
可撤銷重新產生時舊碼失效

它不應該像密碼一樣長期重複使用。

四、 什麼時候產生 Backup Codes

常見時機:

1. 啟用 MFA 成功後

例如使用者啟用 TOTP:

text
掃 QR code -> 輸入第一組 TOTP -> 啟用成功 -> 顯示 backup codes

這是最常見做法。

因為你已經確認使用者真的完成 MFA 設定。

2. 使用者主動重新產生

使用者可能覺得舊 backup codes 遺失或外洩。

這時可以提供:

text
重新產生 backup codes

但必須要求近期重新驗證。

3. 高風險帳號要求多種備援

例如管理員、金融、醫療、企業帳號,除了 backup codes 之外,還可以要求:

  • 第二個 TOTP。
  • 第二個 Passkey。
  • 硬體安全金鑰。
  • 管理員協助恢復。

不要把唯一備援全部押在一張紙上。

五、 產生方式

Backup code 不能太短。

例如:

text
123456

這種 6 位數太容易被猜,尤其如果沒有嚴格重試限制。

比較常見的格式:

text
8H7K-2D91
M4Q9-LA30
P2Z8-XC77
Q6RT-91BC

或:

text
7f3a-b91d-2c8e

設計重點:

  • 使用 cryptographically secure random。
  • 避免容易混淆字元,例如 O0I1
  • 分組顯示,方便手動輸入。
  • 保持足夠熵。
  • 產生 8 到 12 組左右。

Node.js 範例:

ts
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 的秘密。

保存方式:

text
code_hash = HMAC(server_secret, user_id + code)

或使用適合短 secret 的 keyed hash。

資料表可以像:

sql
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)
);

如果要支援重新產生:

sql
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
  • 使用者返回後不再顯示明文。
  • 重新查看必須重新產生,而不是解密顯示舊碼。

可以提示:

text
請將備用碼存放在密碼管理器或列印後放在安全位置。
每組備用碼只能使用一次。

不要提示:

text
截圖保存到相簿就好。

因為相簿可能同步到雲端,裝置也可能被他人看到。

八、 使用流程

Backup code 通常在 MFA challenge 畫面提供:

text
無法使用驗證器?使用備用碼

流程:

使用成功後建議:

  • 標記該 code 已使用。
  • 寄出安全通知。
  • 記錄 audit log。
  • 提醒剩餘 backup codes 數量。
  • 若剩餘很少,提示重新產生。
  • 若是高風險帳號,可要求重新設定 MFA。

九、 使用後要不要強制重設 MFA

看場景。

如果 backup code 是因為:

text
手機剛好不在身邊

不一定要強制重設 MFA。

但如果使用者選擇:

text
我無法再使用 TOTP 裝置

那就應該要求:

  • 使用 backup code 登入。
  • 通過後進入 MFA 恢復流程。
  • 重新設定 TOTP / Passkey。
  • 產生新的 backup codes。
  • 可能撤銷舊 sessions。

高風險產品可採取更嚴格策略:

text
使用 backup code 後,必須重新確認 MFA 設定。

至少應該寄通知,讓使用者知道有 backup code 被使用。

十、 重新產生 Backup Codes

重新產生時要注意:

text
新的 codes 產生後,舊的全部失效。

流程:

重新產生是高風險操作,應要求:

  • 使用者已登入。
  • 近期重新驗證。
  • 最好通過既有 MFA。
  • 寄通知。
  • 寫 audit log。

十一、 速率限制與錯誤處理

Backup code 必須有嘗試限制。

否則攻擊者可以暴力猜。

限制維度:

  • user。
  • session。
  • IP。
  • device。
  • MFA challenge。

例如:

text
同一 MFA challenge 最多 5 次。
同一 user 10 分鐘最多 10 次。
同一 IP 1 分鐘最多 30 次。

錯誤訊息不要透露:

  • code 是否存在。
  • code 是否已使用。
  • code 是否屬於這個 user。

對外統一:

text
備用碼無效或已使用。

內部 audit log 可以記細節:

text
backup_code_invalid
backup_code_used
backup_code_replay_attempt
backup_code_rate_limited

十二、 API 設計範例

產生 backup codes

ts
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

ts
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 是帳號恢復的一部分,但不等於完整帳號恢復。

如果使用者:

text
沒有 TOTP
也沒有 backup codes
也沒有其他 authenticator

系統就要進入更高風險的恢復流程。

這可能包含:

  • 已登入可信裝置。
  • 第二個 email / phone。
  • passkey / security key。
  • 企業管理員確認。
  • 高信任身份驗證。
  • 人工審核。

越高風險的產品,越不能只靠客服問答恢復 MFA。

備用碼的存在,是為了減少進入人工恢復流程的機率。

十四、 常見錯誤

1. 明文保存 backup codes

錯誤:

text
backup_code = ABCDE-12345

正確:

text
backup_code_hash = HMAC(server_secret, user_id + code)

2. Backup code 可重複使用

錯誤:

text
同一組 code 可以用很多次。

正確:

text
每組 code 使用後立即 used_at。

3. 重新產生不撤銷舊碼

錯誤:

text
新舊 backup codes 都有效。

正確:

text
重新產生新 batch 時,舊 batch 全部 revoked。

4. 產生後可以再次查看

錯誤:

text
使用者可以隨時查看所有明文 backup codes。

正確:

text
只顯示一次。忘記就重新產生,舊碼失效。

5. 沒有使用通知

錯誤:

text
backup code 被用掉,使用者完全不知道。

正確:

text
使用後寄安全通知,並顯示剩餘數量。

6. 沒有速率限制

錯誤:

text
backup code 可以無限猜。

正確:

text
限制 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 使用流程

情境:

text
使用者密碼正確,但 TOTP 裝置不見了。

請設計:

  1. 使用者選擇使用 backup code。
  2. 輸入 backup code。
  3. 系統做速率限制。
  4. 比對 hash。
  5. 成功後標記 used。
  6. 建立 MFA session。
  7. 寄通知。
  8. 引導重新設定 MFA。

題目三:設計重新產生流程

要求:

  • 必須登入。
  • 必須近期重新驗證。
  • 最好通過既有 MFA。
  • 新 batch 產生後舊 batch revoked。
  • 明文只顯示一次。
  • 寄通知與 audit log。

十七、 資深視角

1. Backup Codes 是 MFA 的安全出口

任何強 MFA 都需要備援。

但備援不能比正門弱太多。

如果主要登入是:

text
密碼 + TOTP

但客服可以只靠生日關閉 TOTP,那攻擊者就會走客服。

Backup codes 是把恢復流程提前設計好,避免每次都落到脆弱的人工流程。

2. 備用碼要被當成憑證

Backup code 不是提示文字,也不是單純 token。

它是可以通過 MFA 的秘密。

因此要有:

  • 安全產生。
  • 安全顯示。
  • 安全保存。
  • 使用後撤銷。
  • 重試限制。
  • 使用通知。
  • 稽核紀錄。

3. 使用者體驗會影響安全

如果你只是顯示一串碼,使用者可能:

  • 截圖。
  • 傳到聊天軟體。
  • 存在桌面。
  • 完全沒保存。
  • 保存後找不到。

好的產品會引導:

  • 存到密碼管理器。
  • 列印放在安全位置。
  • 確認已保存。
  • 告訴使用者每組只能用一次。
  • 在剩餘數量很少時提醒更新。

4. 高風險系統要多備援而不是弱備援

高風險帳號不要只提供一種 backup code。

更好的做法是鼓勵:

  • 第二個 Passkey。
  • 第二個 TOTP。
  • 硬體安全金鑰。
  • recovery contact,視產品風險。
  • 企業管理員恢復。

讓使用者有多個強備援,而不是把弱流程開太大。


總結

問題回答重點
Backup CodesMFA 恢復用一次性代碼
產生時機啟用 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,而不是方便的後門。

延伸閱讀