多因素認證 (MFA) API
概述
MFA API 提供多因素認證功能,支援 TOTP(Time-based One-Time Password)和備用碼兩種驗證方式。MFA 機制為可選功能,用戶可自行啟用或由管理員強制要求。
相關文檔:
端點列表
認證端點(修改)
| 方法 | 路徑 | 說明 | 權限 |
|---|---|---|---|
| POST | /api/v1/auth/login | 用戶登入(支援 MFA 兩階段) | 公開 |
| POST | /api/v1/auth/verify-mfa | MFA 驗證(第二階段) | 需要 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
}
欄位說明:
| 欄位 | 類型 | 說明 |
|---|---|---|
| mfaRequired | boolean | 是否需要 MFA 驗證 |
| challengeToken | string | MFA 挑戰 Token(5 分鐘有效) |
| primaryMethod | string | 主要 MFA 方法 |
| availableMethods | string[] | 可用的 MFA 方法清單 |
| expiresIn | number | Token 有效期(秒) |
響應範例 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"
}
欄位說明:
| 欄位 | 類型 | 必填 | 說明 |
|---|---|---|---|
| method | string | Yes | MFA 方法(TOTP 或 BACKUP_CODE) |
| code | string | Yes | 驗證碼(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
}
}
錯誤響應:
| 狀態碼 | 錯誤類型 | 說明 |
|---|---|---|
| 401 | MFA_INVALID_CODE | MFA 驗證碼無效 |
| 401 | MFA_CHALLENGE_EXPIRED | Challenge Token 已過期 |
| 429 | MFA_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"
}
]
}
欄位說明:
| 欄位 | 類型 | 說明 |
|---|---|---|
| mfaEnabled | boolean | MFA 是否已啟用 |
| mfaEnforcement | string | MFA 強制設定(DISABLED/OPTIONAL/REQUIRED) |
| primaryMethod | string | 主要 MFA 方法(null 表示未設定) |
| methods | array | MFA 方法列表 |
| methods[].method | string | 方法類型 |
| methods[].primary | boolean | 是否為主要方法 |
| methods[].verified | boolean | 是否已驗證 |
| methods[].remainingCodes | number | 剩餘備用碼數量(僅 BACKUP_CODE) |
| methods[].createdAt | string | 建立時間(ISO 8601) |
| methods[].lastUsedAt | string | 最後使用時間(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
}
欄位說明:
| 欄位 | 類型 | 說明 |
|---|---|---|
| secret | string | TOTP Secret(Base32 編碼,用於手動輸入) |
| qrCodeUri | string | OTP Auth URI(用於 QR Code) |
| qrCodeImage | string | QR Code 圖片(Base64 PNG,可選) |
| expiresIn | number | 設定流程有效期(秒) |
錯誤響應:
| 狀態碼 | 錯誤類型 | 說明 |
|---|---|---|
| 409 | MFA_ALREADY_ENABLED | TOTP 已啟用 |
業務規則:
- 設定流程必須在 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."
}
欄位說明:
| 欄位 | 類型 | 說明 |
|---|---|---|
| success | boolean | 是否成功 |
| message | string | 成功訊息 |
| backupCodes | string[] | 備用碼清單(首次啟用時生成,共 10 組) |
| backupCodesWarning | string | 備用碼警告訊息 |
錯誤響應:
| 狀態碼 | 錯誤類型 | 說明 |
|---|---|---|
| 400 | MFA_SETUP_NOT_FOUND | 未找到待驗證的 TOTP 設定 |
| 401 | MFA_INVALID_CODE | TOTP 驗證碼無效 |
| 409 | MFA_ALREADY_VERIFIED | TOTP 已驗證啟用 |
業務規則:
- 必須先呼叫
/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."
}
錯誤響應:
| 狀態碼 | 錯誤類型 | 說明 |
|---|---|---|
| 401 | MFA_REQUIRED | 需要 MFA 驗證(X-MFA-Code header) |
| 403 | MFA_NOT_ENABLED | MFA 未啟用 |
業務規則:
- 需要當前的 TOTP 碼確認身份
- 重新生成後,所有舊備用碼立即失效
- 備用碼僅在此響應中顯示一次
DELETE /api/v1/auth/mfa
描述: 停用 MFA
請求 Headers:
Authorization: Bearer {access_token}
X-MFA-Code: 123456
響應範例 (204 No Content):
(無響應 Body)
錯誤響應:
| 狀態碼 | 錯誤類型 | 說明 |
|---|---|---|
| 401 | MFA_REQUIRED | 需要 MFA 驗證(X-MFA-Code header) |
| 403 | MFA_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-mfa | 5 次失敗 | 15 分鐘 |
| /totp/verify | 5 次失敗 | 15 分鐘 |
| /backup-codes/regenerate | 3 次/小時 | - |
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