ADR-005: MFA(多因素認證)實作策略
狀態
🔵 提議中 (Proposed)
日期
2025-01-15
背景
隨著系統安全需求提升,需要提供多因素認證(MFA)功能,以增強帳號安全性。MFA 作為可選功能,允許用戶自行啟用,或由管理員強制要求特定角色(如店主、管理員)啟用。
現有認證架構
目前系統已具備完整的認證基礎設施:
- JWT 雙 Token 機制(Access Token + Refresh Token)
- Session ID 機制(支援批量登出)
- Token 黑名單機制
- 登入嘗試鎖定(Login Lockout)
- 多租戶隔離
需求
- 支援 TOTP(Time-based One-Time Password)作為主要 MFA 方法
- 提供備用碼(Backup Codes)作為恢復機制
- 保持與現有認證流程的相容性
- 支援可選啟用和強制啟用兩種模式
- 框架層提供可復用工具,參考實作展示具體用法
決策
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 方法優先級
| 方法 | 優先級 | 說明 |
|---|---|---|
| TOTP | P1 | 主要方法,首次實作 |
| BACKUP_CODE | P1 | 恢復機制,與 TOTP 同時實作 |
| EMAIL_OTP | P2 | 可選,未來擴充 |
| SMS_OTP | P3 | 可選,需要第三方服務 |
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:基礎架構(框架層)
-
appfuse-server 新增 MFA 模組
mfa/api/介面定義mfa/totp/TOTP 實作JwtTokenProvider擴充
-
依賴:
// 考慮的選項
api("com.j256.java-one-time-password:java-one-time-password:1.0.0")
// 或自行實作 RFC 6238
Phase 2:參考實作
-
app-server 資料模型
AccountMfaMethod實體Account擴充- 資料庫 Migration
-
app-server 服務層
MfaService業務邏輯SecretEncryptor加密服務MfaAttemptTracker嘗試追蹤
-
app-server API 層
AuthController兩階段登入MfaControllerMFA 管理
Phase 3:完整功能
- 備用碼功能
- QR Code 生成
- 審計日誌
- 測試覆蓋
替代方案考量
方案 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 加密儲存 |
| 暴力破解 MFA | 中 | Rate limiting + 鎖定 |
| Challenge Token 被盜 | 中 | 5 分鐘短效期 |
| 用戶遺失 TOTP 裝置 | 中 | 備用碼恢復機制 |
| 時鐘偏移導致 TOTP 失敗 | 低 | 允許 ±30 秒容錯 |
參考資料
- RFC 6238: TOTP Algorithm
- RFC 4226: HOTP Algorithm
- OWASP MFA Cheat Sheet
- Google Authenticator Key Uri Format
相關文檔
- MFA API 規格
- 認證 API 規格
- appfuse-server 開發指引(參閱 appfuse-server/AGENTS.md)
- app-server 開發指引(參閱 app-server/AGENTS.md)
最後更新: 2025-01-15