跳至主要内容

多因素認證 (MFA) API

概述

MFA API 提供多因素認證功能,支援 TOTP(Time-based One-Time Password)和備用碼兩種驗證方式。MFA 機制為可選功能,用戶可自行啟用或由管理員強制要求。

相關文檔:


端點列表

認證端點(修改)

方法路徑說明權限
POST/api/v1/auth/login用戶登入(支援 MFA 兩階段)公開
POST/api/v1/auth/verify-mfaMFA 驗證(第二階段)需要 Challenge Token

MFA 管理端點

方法路徑說明權限
GET/api/v1/auth/mfa/status取得 MFA 設定狀態需要認證
POST/api/v1/auth/mfa/totp/setup開始設定 TOTP需要認證
POST/api/v1/auth/mfa/totp/verify驗證並啟用 TOTP需要認證
POST/api/v1/auth/mfa/backup-codes/regenerate重新生成備用碼需要認證 + MFA
DELETE/api/v1/auth/mfa停用 MFA需要認證 + MFA

兩階段登入流程

用戶提交帳號密碼


┌───────────────────┐
│ POST /auth/login │
└─────────┬─────────┘

┌─────┴─────┐
│ 密碼驗證 │
└─────┬─────┘

┌─────┴─────────────────────────────────┐
│ │
▼ ▼
mfaEnabled=false mfaEnabled=true
│ │
▼ ▼
┌─────────────┐ ┌─────────────────┐
│ 返回 JWT │ │ 返回 Challenge │
│ (完成登入) │ │ Token + 方法 │
└─────────────┘ └────────┬────────┘


┌─────────────────┐
│ 用戶輸入 TOTP │
│ 或備用碼 │
└────────┬────────┘


┌─────────────────────┐
│ POST /auth/verify- │
│ mfa │
└────────┬────────────┘


┌─────────────────┐
│ 返回 JWT │
│ (完成登入) │
└─────────────────┘

端點詳細規格

POST /api/v1/auth/login(修改後)

描述: 用戶登入,根據 MFA 狀態返回不同響應

請求 Body:

{
"username": "staff@florist.com",
"password": "Password123!",
"remember": false
}

響應範例 A - 無 MFA (200 OK):

{
"accessToken": "eyJhbGciOiJSUzI1NiIs...",
"refreshToken": "eyJhbGciOiJSUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 900,
"user": {
"id": "user-001",
"email": "staff@florist.com",
"name": "王小明",
"role": "ROLE_SALES",
"roles": ["ROLE_SALES"],
"tenantId": "tenant-abc",
"mfaEnabled": false
}
}

響應範例 B - 需要 MFA 驗證 (200 OK):

{
"mfaRequired": true,
"challengeToken": "eyJhbGciOiJSUzI1NiIs...",
"primaryMethod": "TOTP",
"availableMethods": ["TOTP", "BACKUP_CODE"],
"expiresIn": 300
}

欄位說明:

欄位類型說明
mfaRequiredboolean是否需要 MFA 驗證
challengeTokenstringMFA 挑戰 Token(5 分鐘有效)
primaryMethodstring主要 MFA 方法
availableMethodsstring[]可用的 MFA 方法清單
expiresInnumberToken 有效期(秒)

響應範例 C - 需要強制設定 MFA (200 OK):

{
"mfaSetupRequired": true,
"setupToken": "eyJhbGciOiJSUzI1NiIs...",
"expiresIn": 600,
"message": "Your account requires MFA. Please set up MFA to continue."
}

當帳號設定為 mfaEnforcement=REQUIRED 但尚未啟用 MFA 時返回此響應。


POST /api/v1/auth/verify-mfa

描述: MFA 驗證(登入第二階段)

請求 Headers:

X-MFA-Challenge-Token: eyJhbGciOiJSUzI1NiIs...

請求 Body:

{
"method": "TOTP",
"code": "123456"
}

欄位說明:

欄位類型必填說明
methodstringYesMFA 方法(TOTP 或 BACKUP_CODE)
codestringYes驗證碼(TOTP: 6 位數字,備用碼: 8 位數字)

響應範例 (200 OK):

{
"accessToken": "eyJhbGciOiJSUzI1NiIs...",
"refreshToken": "eyJhbGciOiJSUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 900,
"user": {
"id": "user-001",
"email": "staff@florist.com",
"name": "王小明",
"role": "ROLE_SALES",
"roles": ["ROLE_SALES"],
"tenantId": "tenant-abc",
"mfaEnabled": true
}
}

錯誤響應:

狀態碼錯誤類型說明
401MFA_INVALID_CODEMFA 驗證碼無效
401MFA_CHALLENGE_EXPIREDChallenge Token 已過期
429MFA_TOO_MANY_ATTEMPTS嘗試次數過多,請稍後再試

錯誤響應範例 (401):

{
"type": "https://api.example.com/problems/mfa-invalid-code",
"title": "Unauthorized",
"status": 401,
"detail": "Invalid MFA code",
"remainingAttempts": 2
}

業務規則:

  • Challenge Token 有效期 5 分鐘
  • MFA 驗證失敗 5 次後,需要重新登入(密碼驗證)
  • 使用備用碼後,該備用碼立即失效
  • 備用碼剩餘數量低於 3 個時,響應中會包含警告

GET /api/v1/auth/mfa/status

描述: 取得當前用戶的 MFA 設定狀態

請求 Headers:

Authorization: Bearer {access_token}

響應範例 (200 OK):

{
"mfaEnabled": true,
"mfaEnforcement": "OPTIONAL",
"primaryMethod": "TOTP",
"methods": [
{
"method": "TOTP",
"primary": true,
"verified": true,
"createdAt": "2025-01-10T08:00:00Z",
"lastUsedAt": "2025-01-15T10:30:00Z"
},
{
"method": "BACKUP_CODE",
"primary": false,
"verified": true,
"remainingCodes": 8,
"createdAt": "2025-01-10T08:00:00Z",
"lastUsedAt": "2025-01-12T14:20:00Z"
}
]
}

欄位說明:

欄位類型說明
mfaEnabledbooleanMFA 是否已啟用
mfaEnforcementstringMFA 強制設定(DISABLED/OPTIONAL/REQUIRED)
primaryMethodstring主要 MFA 方法(null 表示未設定)
methodsarrayMFA 方法列表
methods[].methodstring方法類型
methods[].primaryboolean是否為主要方法
methods[].verifiedboolean是否已驗證
methods[].remainingCodesnumber剩餘備用碼數量(僅 BACKUP_CODE)
methods[].createdAtstring建立時間(ISO 8601)
methods[].lastUsedAtstring最後使用時間(ISO 8601)

POST /api/v1/auth/mfa/totp/setup

描述: 開始設定 TOTP,返回 Secret 和 QR Code

請求 Headers:

Authorization: Bearer {access_token}

響應範例 (200 OK):

{
"secret": "JBSWY3DPEHPK3PXP",
"qrCodeUri": "otpauth://totp/AppFuse:staff@florist.com?secret=JBSWY3DPEHPK3PXP&issuer=AppFuse&algorithm=SHA1&digits=6&period=30",
"qrCodeImage": "data:image/png;base64,iVBORw0KGgo...",
"expiresIn": 600
}

欄位說明:

欄位類型說明
secretstringTOTP Secret(Base32 編碼,用於手動輸入)
qrCodeUristringOTP Auth URI(用於 QR Code)
qrCodeImagestringQR Code 圖片(Base64 PNG,可選)
expiresInnumber設定流程有效期(秒)

錯誤響應:

狀態碼錯誤類型說明
409MFA_ALREADY_ENABLEDTOTP 已啟用

業務規則:

  • 設定流程必須在 10 分鐘內完成驗證
  • 重複呼叫會產生新的 Secret(舊的未驗證 Secret 失效)
  • Secret 僅在此響應中返回一次,不會再次顯示

POST /api/v1/auth/mfa/totp/verify

描述: 驗證 TOTP 碼並啟用 MFA

請求 Headers:

Authorization: Bearer {access_token}

請求 Body:

{
"code": "123456"
}

響應範例 (200 OK):

{
"success": true,
"message": "TOTP has been enabled successfully",
"backupCodes": [
"12345678",
"23456789",
"34567890",
"45678901",
"56789012",
"67890123",
"78901234",
"89012345",
"90123456",
"01234567"
],
"backupCodesWarning": "Please save these backup codes in a secure location. They will not be shown again."
}

欄位說明:

欄位類型說明
successboolean是否成功
messagestring成功訊息
backupCodesstring[]備用碼清單(首次啟用時生成,共 10 組)
backupCodesWarningstring備用碼警告訊息

錯誤響應:

狀態碼錯誤類型說明
400MFA_SETUP_NOT_FOUND未找到待驗證的 TOTP 設定
401MFA_INVALID_CODETOTP 驗證碼無效
409MFA_ALREADY_VERIFIEDTOTP 已驗證啟用

業務規則:

  • 必須先呼叫 /totp/setup 取得 Secret
  • 驗證成功後自動啟用 MFA
  • 首次啟用時自動生成 10 組備用碼
  • 備用碼僅在此響應中顯示一次

POST /api/v1/auth/mfa/backup-codes/regenerate

描述: 重新生成備用碼(舊備用碼全部失效)

請求 Headers:

Authorization: Bearer {access_token}
X-MFA-Code: 123456

響應範例 (200 OK):

{
"backupCodes": [
"12345678",
"23456789",
"34567890",
"45678901",
"56789012",
"67890123",
"78901234",
"89012345",
"90123456",
"01234567"
],
"generatedAt": "2025-01-15T10:30:00Z",
"warning": "All previous backup codes have been invalidated."
}

錯誤響應:

狀態碼錯誤類型說明
401MFA_REQUIRED需要 MFA 驗證(X-MFA-Code header)
403MFA_NOT_ENABLEDMFA 未啟用

業務規則:

  • 需要當前的 TOTP 碼確認身份
  • 重新生成後,所有舊備用碼立即失效
  • 備用碼僅在此響應中顯示一次

DELETE /api/v1/auth/mfa

描述: 停用 MFA

請求 Headers:

Authorization: Bearer {access_token}
X-MFA-Code: 123456

響應範例 (204 No Content):

(無響應 Body)

錯誤響應:

狀態碼錯誤類型說明
401MFA_REQUIRED需要 MFA 驗證(X-MFA-Code header)
403MFA_ENFORCEMENT_REQUIRED帳號設定為強制 MFA,無法停用

業務規則:

  • 需要當前的 TOTP 碼確認身份
  • 如果帳號設定為 mfaEnforcement=REQUIRED,無法停用
  • 停用後,所有 MFA 方法和備用碼都會被刪除

MFA 方法說明

TOTP (Time-based One-Time Password)

  • 演算法: HMAC-SHA1
  • 位數: 6 位數字
  • 時間步長: 30 秒
  • 時間容錯: 前後各 1 個時間窗口(共 90 秒)
  • 支援的 App: Google Authenticator、Microsoft Authenticator、Authy、1Password 等

備用碼 (Backup Codes)

  • 格式: 8 位數字
  • 數量: 10 組
  • 使用方式: 一次性使用,用過即失效
  • 用途: TOTP 裝置遺失時的緊急恢復

安全性考量

Rate Limiting

端點限制鎖定時間
/verify-mfa5 次失敗15 分鐘
/totp/verify5 次失敗15 分鐘
/backup-codes/regenerate3 次/小時-

Secret 儲存

  • TOTP Secret 使用 AES-256-GCM 加密儲存
  • 備用碼使用 BCrypt hash 儲存(無法還原)
  • 加密金鑰從環境變數讀取(MFA_ENCRYPTION_KEY

審計日誌

所有 MFA 相關操作都會記錄審計日誌:

[AUDIT] MFA setup initiated: staff@florist.com
[AUDIT] TOTP enabled: staff@florist.com
[AUDIT] MFA verified: staff@florist.com using TOTP
[AUDIT] MFA verification failed: staff@florist.com using TOTP (remaining: 2)
[AUDIT] Backup code used: staff@florist.com (remaining: 9)
[AUDIT] Backup codes regenerated: staff@florist.com
[AUDIT] MFA disabled: staff@florist.com

使用範例

1. 啟用 MFA(TOTP)

// 1. 開始設定 TOTP
const setupResponse = await fetch('/api/v1/auth/mfa/totp/setup', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
const { secret, qrCodeUri, qrCodeImage } = await setupResponse.json();

// 2. 顯示 QR Code 給用戶掃描
// 或顯示 secret 讓用戶手動輸入

// 3. 用戶掃描後,輸入 TOTP 驗證
const verifyResponse = await fetch('/api/v1/auth/mfa/totp/verify', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ code: '123456' })
});
const { backupCodes } = await verifyResponse.json();

// 4. 顯示備用碼,請用戶妥善保存
console.log('Backup codes:', backupCodes);

2. MFA 登入流程

// 1. 第一階段:密碼登入
const loginResponse = await fetch('/api/v1/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: 'staff@florist.com',
password: 'Password123!'
})
});
const loginResult = await loginResponse.json();

// 2. 檢查是否需要 MFA
if (loginResult.mfaRequired) {
// 保存 challenge token
const challengeToken = loginResult.challengeToken;

// 提示用戶輸入 TOTP
const totpCode = prompt('Please enter your TOTP code:');

// 3. 第二階段:MFA 驗證
const mfaResponse = await fetch('/api/v1/auth/verify-mfa', {
method: 'POST',
headers: {
'X-MFA-Challenge-Token': challengeToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
method: 'TOTP',
code: totpCode
})
});

const { accessToken, refreshToken, user } = await mfaResponse.json();
// 登入完成
} else {
// 無需 MFA,直接登入
const { accessToken, refreshToken, user } = loginResult;
}

3. 使用備用碼登入

// 在 MFA 驗證階段使用備用碼
const mfaResponse = await fetch('/api/v1/auth/verify-mfa', {
method: 'POST',
headers: {
'X-MFA-Challenge-Token': challengeToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
method: 'BACKUP_CODE',
code: '12345678' // 8 位備用碼
})
});

相關文檔


最後更新: 2025-01-15