跳至主要内容

ADR-005: MFA(多因素認證)實作策略

狀態

🔵 提議中 (Proposed)

日期

2025-01-15

背景

隨著系統安全需求提升,需要提供多因素認證(MFA)功能,以增強帳號安全性。MFA 作為可選功能,允許用戶自行啟用,或由管理員強制要求特定角色(如店主、管理員)啟用。

現有認證架構

目前系統已具備完整的認證基礎設施:

  • JWT 雙 Token 機制(Access Token + Refresh Token)
  • Session ID 機制(支援批量登出)
  • Token 黑名單機制
  • 登入嘗試鎖定(Login Lockout)
  • 多租戶隔離

需求

  1. 支援 TOTP(Time-based One-Time Password)作為主要 MFA 方法
  2. 提供備用碼(Backup Codes)作為恢復機制
  3. 保持與現有認證流程的相容性
  4. 支援可選啟用和強制啟用兩種模式
  5. 框架層提供可復用工具,參考實作展示具體用法

決策

1. 架構分層

採用 AppFuse 標準的「框架 + 參考實作」架構:

┌─────────────────────────────────────────────────────────────────┐
│ appfuse-server(框架層) │
│ 提供 MFA 工具和介面,不包含具體業務邏輯 │
├─────────────────────────────────────────────────────────────────┤
│ io.leandev.appfuse.mfa.api/ │
│ ├── MfaProvider # MFA 驗證介面 │
│ ├── MfaMethod # MFA 方法列舉 │
│ ├── MfaChallenge # MFA 挑戰 DTO │
│ └── MfaVerifyResult # 驗證結果 DTO │
│ │
│ io.leandev.appfuse.mfa.totp/ │
│ ├── TotpProvider # TOTP 驗證實作 │
│ ├── TotpSecretGenerator # Secret 生成器 │
│ └── TotpQrCodeGenerator # QR Code 生成器 │
│ │
│ io.leandev.appfuse.mfa.backup/ │
│ └── BackupCodeProvider # 備用碼生成與驗證 │
│ │
│ io.leandev.appfuse.security.auth/ │
│ └── JwtTokenProvider # 擴充 MFA Challenge Token 支援 │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ app-server(參考實作) │
│ 展示如何使用框架工具實作完整 MFA 功能 │
├─────────────────────────────────────────────────────────────────┤
│ entity/auth/ │
│ ├── Account # 擴充 MFA 欄位 │
│ └── AccountMfaMethod # MFA 方法實體 │
│ │
│ service/auth/ │
│ └── MfaService # MFA 業務邏輯 │
│ │
│ controller/auth/ │
│ ├── AuthController # 兩階段登入 API │
│ └── MfaController # MFA 管理 API │
└─────────────────────────────────────────────────────────────────┘

2. 兩階段登入流程

採用兩階段登入,而非在現有 JWT 中加入 MFA 狀態:

第一階段(密碼驗證):

  • 驗證 username + password
  • 成功後檢查 MFA 狀態
  • 若需 MFA,返回 Challenge Token(5 分鐘有效)

第二階段(MFA 驗證):

  • 提交 Challenge Token + MFA 碼
  • 驗證成功後返回完整 JWT

選擇理由:

  • 清楚分離密碼驗證和 MFA 驗證
  • Challenge Token 短效期,降低被盜用風險
  • 與現有 JWT 驗證流程完全相容

3. MFA 方法優先級

方法優先級說明
TOTPP1主要方法,首次實作
BACKUP_CODEP1恢復機制,與 TOTP 同時實作
EMAIL_OTPP2可選,未來擴充
SMS_OTPP3可選,需要第三方服務

4. 資料模型設計

Account 擴充

// 在 Account 實體新增
@Column(nullable = false)
private boolean mfaEnabled = false;

@Column(length = 20, nullable = false)
@Enumerated(EnumType.STRING)
private MfaEnforcement mfaEnforcement = MfaEnforcement.OPTIONAL;

@OneToMany(mappedBy = "account", cascade = CascadeType.ALL, orphanRemoval = true)
private List<AccountMfaMethod> mfaMethods = new ArrayList<>();

AccountMfaMethod 實體

@Entity
@Table(name = "account_mfa_method")
public class AccountMfaMethod extends AuditableBase {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "account_id", nullable = false)
private Account account;

@Enumerated(EnumType.STRING)
@Column(length = 20, nullable = false)
private MfaMethod method; // TOTP, BACKUP_CODE

@Column(columnDefinition = "TEXT")
private String encryptedSecret; // 加密儲存

private boolean primary;
private boolean verified;
private LocalDateTime verifiedAt;
private Integer remainingBackupCodes; // 僅 BACKUP_CODE 使用
private LocalDateTime lastUsedAt;
}

5. 安全措施

Secret 加密

  • 演算法: AES-256-GCM
  • 金鑰管理: 從環境變數 MFA_ENCRYPTION_KEY 讀取
  • 備用碼: 使用 BCrypt hash(不可還原)

Rate Limiting

操作限制鎖定時間
MFA 驗證失敗5 次15 分鐘
TOTP 設定驗證5 次15 分鐘
備用碼重新生成3 次/小時-

防護措施

  • Constant-time 比較: 防止 timing attack
  • TOTP 時間容錯: 允許前後各 1 個時間窗口(30 秒)
  • Challenge Token 短效期: 5 分鐘
  • 備用碼一次性使用: 使用後立即從資料庫移除

6. JwtTokenProvider 擴充

新增 MFA Challenge Token 支援:

public static final String CLAIM_MFA_REQUIRED = "mfaRequired";
public static final String CLAIM_MFA_METHODS = "mfaMethods";

public String generateMfaChallengeToken(
UserDetails userDetails,
List<MfaMethod> availableMethods,
String sessionId) {
// 生成 5 分鐘有效的臨時 token
}

public boolean isMfaChallengeToken(String token) {
// 檢查是否為 MFA Challenge Token
}

實作計畫

Phase 1:基礎架構(框架層)

  1. appfuse-server 新增 MFA 模組

    • mfa/api/ 介面定義
    • mfa/totp/ TOTP 實作
    • JwtTokenProvider 擴充
  2. 依賴:

    // 考慮的選項
    api("com.j256.java-one-time-password:java-one-time-password:1.0.0")
    // 或自行實作 RFC 6238

Phase 2:參考實作

  1. app-server 資料模型

    • AccountMfaMethod 實體
    • Account 擴充
    • 資料庫 Migration
  2. app-server 服務層

    • MfaService 業務邏輯
    • SecretEncryptor 加密服務
    • MfaAttemptTracker 嘗試追蹤
  3. app-server API 層

    • AuthController 兩階段登入
    • MfaController MFA 管理

Phase 3:完整功能

  1. 備用碼功能
  2. QR Code 生成
  3. 審計日誌
  4. 測試覆蓋

替代方案考量

方案 A:單一 Token 流程(未採用)

將 MFA 狀態嵌入 JWT,驗證時檢查 mfaVerified claim。

優點:

  • 流程簡單,不需要 Challenge Token

缺點:

  • 需要修改所有 API 的權限檢查
  • MFA 未驗證的 Token 可能被濫用
  • 與現有架構不相容

方案 B:Session-based MFA(未採用)

使用 Server-side Session 追蹤 MFA 狀態。

優點:

  • 狀態管理更直觀

缺點:

  • 違反 Stateless JWT 設計原則
  • 需要額外的 Session 儲存(Redis)
  • 增加架構複雜度

方案 C:兩階段 Token(採用)

密碼驗證返回短效期 Challenge Token,MFA 驗證返回完整 JWT。

優點:

  • 與現有 JWT 架構完全相容
  • 清楚分離認證階段
  • Challenge Token 短效期降低風險
  • 無需修改現有 API 權限檢查

缺點:

  • 前端需要處理兩種響應格式

向後相容性

對現有用戶的影響

  • 無 MFA 用戶: 登入流程不變,直接返回 JWT
  • 啟用 MFA 用戶: 需要兩階段登入
  • 現有 Token: 繼續有效,無需重新登入

API 相容性

端點變更
POST /auth/login響應格式新增 mfaRequired 欄位
POST /auth/verify-mfa新增端點
其他端點無變更

風險與緩解

風險影響緩解措施
TOTP Secret 外洩AES-256 加密儲存
暴力破解 MFARate limiting + 鎖定
Challenge Token 被盜5 分鐘短效期
用戶遺失 TOTP 裝置備用碼恢復機制
時鐘偏移導致 TOTP 失敗允許 ±30 秒容錯

參考資料


相關文檔

  • MFA API 規格
  • 認證 API 規格
  • appfuse-server 開發指引(參閱 appfuse-server/AGENTS.md)
  • app-server 開發指引(參閱 app-server/AGENTS.md)

最後更新: 2025-01-15