跳轉到

登入(含 MFA OTP)(Login / OTP Verify)

功能群:認證|認證資料模型、登入流程、匿名態特性先讀 功能群總覽

事實基準:2026-07-05 從 FE(views/auth/Login.vue、OtpVerification.vue)/ BE(api/auth、jedi-login / jedi-mfa / jedi-captcha 套件)code 掃出;表結構取自 db_schema.json(public schema);error code 對 jedi-login / jedi-mfa / common/code/error_code.py

變更紀錄:2026-07-09 FR-047 已知坑修復 —— /otp-resend type/mfa_type key bug 修正(route 改讀 type、serializer OneOf(["email"]) fail loudly);新增 OTP 發送 server-side 節流(jedi-mfa redis,防 email bombing,MFA_429001 + retry_after);修重整直達 auth 頁 data-theme 未套、OTP 輸入框露暗色底 變更紀錄:2026-10-04 FR-126(1.24.0)—— 雙因子第二關(/otp-verify、/otp-resend)改收第一關簽發的 mfa_token、不再收使用者編號;雙因子失敗計數落 DB(public.mfa_attempts),重啟不歸零;前門對登入類端點每 IP 每分鐘 60 次限速 變更紀錄:2026-07-17 site-regression 實測回饋 —— §4/§12 補坑9:is_password_correct 密碼正確時無條件重置 status/login_failed_count 於狀態檢查前(jedi-login),停用/鎖定 gate 可能被繞過,安全等級高待修

1. 功能描述

登入頁是系統的匿名入口:帳密(或 LDAP)+ Cloudflare Turnstile captcha 登入,依 MFA_REQUIRED 環境變數與使用者 mfa_type 決定是否走 MFA OTP(轉 OTP 驗證頁),並處理首次 / 過期改密碼分流。OTP 頁(/otp-verify)輸入 6 位碼驗證後才發 JWT。本 spec 涵蓋 login 與 otp-verify 兩條 route。

主要使用者:所有未登入者。§3 權限與其他頁不同——匿名可達(見 _overview §5)。

角色速覽(匿名態,見 _overview §5):

角色 一句話
未登入者 登入 / OTP 驗證(/login、/otp-verify、/otp-qrcode 匿名可達;/totp-secret、/logout、/refresh 需 token)

1.1 功能總覽(本頁全部功能)

# 功能 說明 位置 詳述
1 帳密登入 帳號 + 密碼 → POST /login 登入頁 UC-LG-01
2 認證方式切換 密碼 / LDAP(provider) 登入頁 tab §6.2
3 Captcha Cloudflare Turnstile,登入前驗證 登入頁 §11
4 記住我 cm_persist_mode session vs local 登入頁 §12
5 登入失敗鎖定 失敗 3 次鎖 15 分鐘 BE §4
6 MFA 分流 mfa_required → 轉 OTP 頁(token 未發) 分流 UC-LG-02
7 改密碼分流 change_password → 轉重設密碼頁 分流 →忘記密碼 / 重設密碼
8 OTP 驗證 6 位碼(email / TOTP),驗證後發 JWT OTP 頁 UC-LG-02
9 OTP 重送 email 型重送(60s 冷卻) OTP 頁 §6.2
10 TOTP 啟用 QR 掃碼綁定(/totp-secret + /otp-qrcode) (登入態) UC-LG-02
11 改用 Email 驗證 TOTP 型切回 email OTP OTP 頁 §6.1
12 Token refresh / logout /refresh 換 access、/logout 撤銷 全站 §6.1

UC(§2)展開 登入分流(UC-LG-01)與 MFA/OTP(UC-LG-02)。

2. Use Case

角色速覽(匿名態,見 _overview §5):

角色 一句話
未登入者 登入 / OTP 驗證(/login、/otp-verify、/otp-qrcode 匿名可達;/totp-secret、/logout、/refresh 需 token)

流程圖視覺慣例:菱形 = 判斷、橘底 = 分流 / 例外、紅框 = 錯誤(標 error code)、綠框 = 成功終點、虛線 = 可選路徑。

UC-LG-01 登入(captcha + 鎖定 + 分流)

項目 內容
角色 未登入者
前置條件 有效帳號(status=1 啟用);captcha 通過
產出 / 後置條件 一般 → 發 JWT + 存 token;需 MFA → 轉 OTP 頁(token 未發);需改密碼 → 轉重設頁

UC-LG-01:captcha → 鎖定檢查 → 帳密驗證 → MFA/改密碼/一般 三分流

UC-LG-02 MFA OTP 驗證 + TOTP 啟用

項目 內容
角色 需 MFA 的使用者
前置條件 POST /login 回 mfa_required=true,帶 uid + mfa_type 到 OTP 頁
產出 / 後置條件 OTP 正確 → generate_jwt_token 發 access + refresh;失敗 10s 冷卻、5 次退回登入

UC-LG-02:OTP 驗證(email/TOTP)+ TOTP 啟用(QR 掃碼)→ 發 JWT

3. 權限矩陣

本群為匿名態(見 _overview §5)。「權限」= 端點是否需認證:

操作 FE 判定 BE 強制 BE 檢查位置
登入 匿名頁 無 JWT;captcha + 帳密驗證 api/auth/routes/login_route.py::LoginRoute
OTP 驗證 匿名頁(帶 uid) 無 JWT;驗 OTP api/auth/routes/otp_route.py::MfaVerifyRoute
OTP 重送 匿名 無 JWT otp_route.py::OtpCodeResendRoute
取 TOTP QR 匿名 無 JWT(帶 uid path) otp_route.py::TotpQrcodeRoute
註冊 TOTP secret — 需 JWT(登入態) otp_route.py::TotpSecretRoute
refresh / logout — 需 JWT(refresh / access token) login_route.py::LoginRefreshRoute / LogoutRoute

4. 狀態機與前置條件

gate 效果 出處
captcha 無效 擋下(UnauthorizedError),不進帳密驗證 login_route.py(captcha_service.verify_turnstile_token,登入前)
登入失敗 users.login_failed_count++ + last_login_failed_time,即時 commit jedi-login LoginDomainService.is_password_correct
失敗 ≥ 3 且 15 分內 帳號鎖定 LOGIN_401004(MAX_LOCK_COUNT=3、USER_LOCK_TIME=900s) jedi-login is_user_locked_within_limit
帳號停用 / 未啟用 LOGIN_401011 / 401012;長期未用 401008 jedi-login status 檢查(⚠️ 密碼正確時此檢查可能被繞過,見 §12 坑)
MFA_REQUIRED=true 回 mfa_required,不發 JWT,轉 OTP 頁 login_route.py(env 全域旗標)
登入成功 login_failed_count=0 + last_login_success_time;發 JWT 寫 login_tokens jedi-login

5. UI 設計

📸 實際畫面截圖待補(一律亮色模式 + 登出態擷取;截圖另有專責 session)。預計補到 assets/img/: ① 登入頁(帳密 + captcha + 記住我) login-page.png ② OTP 驗證頁(6 格 + 重送) otp-verify.png ③ TOTP QR 綁定 totp-qrcode.png ④ 鎖定 / 錯誤提示 login-error.png

版面骨架與區塊職責:本質兩畫面——登入卡(帳密/LDAP + Captcha + 記住我 + 鎖定/分流,→ UC-LG-01)與 OTP 驗證卡(6 碼 email/TOTP + 重送 + TOTP 啟用,→ UC-LG-02)。

登入頁(登入卡:captcha/鎖定/分流)+ OTP 驗證頁(OTP 卡:驗證/重送/TOTP 啟用),可點跳轉 UC-LG-01/UC-LG-02

狀態呈現:captcha 主題隨深淺色同步;密碼欄顯示切換;OTP 6 格自動跳格 / 貼上 / 倒退、填滿自動送出;OTP 失敗 10s 冷卻、5 次退回登入;重送 60s 冷卻。無 socket。

6. API 規格

Envelope:成功 {"status": true, "data": …}、失敗 {"error_code": "…", "msg": "…"}。

6.1 總清單(本頁呼叫的全部 endpoint)

分類 Method + Path 說明 完整規格
登入 POST /login 帳密 + captcha 登入,回分流旗標 §6.2
MFA POST /otp-verify 驗 OTP,發 JWT §6.2
MFA POST /otp-resend email OTP 重送 §6.2
MFA GET /otp-qrcode/{uid} TOTP QR PNG GAI-SD-02
MFA POST /totp-secret 註冊 TOTP secret(需 JWT) GAI-SD-02
Token POST /refresh 換新 access token(需 refresh) GAI-SD-02
Token POST /logout 撤銷 token(login_tokens.is_revoked=1) GAI-SD-02
Captcha POST /verify-captcha Turnstile 驗證(captcha 模組) GAI-SD-02

6.2 核心 endpoint(欄位對照 serializer / service,出處標段末)

[POST] /login

Request:

{
  "username": "string",   // 必
  "password": "string",   // 必
  "provider": "password", // 預設 password|ldap
  "cf_turnstile_token": "string" // 必
}

流程:取 client IP → 驗 captcha → login_service.process_login → 依 MFA_REQUIRED 決定發 token 或走 MFA。

Response data(LoginResponse):

欄位 型別 說明
access_token string null 時代表待 MFA
refresh_token string null 時代表待 MFA
user_info object {uid, login_name, nickname, email, is_admin, tenant_id, org_unit_id}
tenants array —
org_units array —
change_password bool —
change_uid — —
mfa_required bool —
mfa_type — email/totp
mfa_token string 只在 mfa_required=true 時回;第一關已通過的短期簽章憑證(5 分鐘),第二關身分只從它取

出處:api/auth/routes/login_route.py::LoginRoute、app/auth/service/login_service.py、jedi-login process_login。

[POST] /otp-verify

Request:

{
  "mfa_token": "string",  // 必(第一關回的)
  "type": "email",        // email/totp
  "code": "string"        // 6 位
}

身分只從 mfa_token 取(不收 uid,多帶會被靜默丟棄);憑證缺漏/偽造/過期/對應的人不存在一律回同一個 401 MFA_401003(「登入驗證已逾時,請重新登入」),不分開回碼以免被拿來探測使用者編號。驗 OTP(email 走 EmailMfaService、totp 走 TotpMfaService);失敗擋 MFA_401001(TOTP 為 AUTH_MFA_401001);連續失敗 5 次即鎖定該驗證方式(email:碼作廢須重送;TOTP:鎖 LOGIN_USER_LOCK_TIME,預設 15 分鐘)。失敗計數存 public.mfa_attempts(每人每種方式一列,Redis 只當快取),服務重啟不歸零;該表刻意不開 RLS(第二關尚無身分,開了寫不進去)。成功 generate_jwt_token(user.uid) 發 JWT。Response:{result, access_token, refresh_token}。出處:api/auth/routes/otp_route.py::MfaVerifyRoute、jedi-mfa。

[POST] /otp-resend ・ TOTP 啟用

/otp-resend:{mfa_token, type} → email 型重送新 OTP(身分同樣只從 mfa_token 取)。TOTP 啟用(需登入態):POST /totp-secret 註冊 secret + uri → GET /otp-qrcode/{uid} 取 QR PNG 掃碼。出處:otp_route.py::OtpCodeResendRoute / TotpSecretRoute / TotpQrcodeRoute。

⚠️ 已知 BE bug:/otp-resend serializer 宣告欄位為 type,route 讀 mfa_type kwarg(FE 送 type)→ 讀不到而靜默 default 為 email(見 §12)。

7. 前端檔案地圖(compliance-manager-fe/)

檔案 角色
src/views/auth/Login.vue 登入頁(帳密 / LDAP / captcha / 記住我 / 分流)
src/views/auth/OtpVerification.vue OTP 驗證頁(6 格 / 重送 / TOTP↔email 切換)
src/utils/authPersist.js・sessionShare.js token 持久化(cm_persist_mode)+ 心跳(cm_last_active)
src/stores/userStore.js 使用者 / tenant / org 狀態
src/config/router/index.js:671-692 route login(/・/login)、otp-verify(/・/otp-verify)
src/config/api/api.js:14-20 LOGIN / LOGOUT / REFRESH / OTP_VERIFY / OTP_RESEND / TOTP_QRCODE / TOTP_SECRET

8. 後端檔案地圖(本頁核心鏈路)

鏈路 Route App Service 底層
登入 api/auth/routes/login_route.py::LoginRoute app/auth/service/login_service.py::process_login jedi-login LoginService / LoginDomainService(鎖定 / 狀態)、jedi-captcha
OTP api/auth/routes/otp_route.py::MfaVerifyRoute — jedi-mfa MFAService / EmailMfaService / TotpMfaService
refresh / logout login_route.py::LoginRefreshRoute / LogoutRoute — jedi-login LoginTokenService(login_tokens jti 撤銷)

DDD 提醒:登入 / MFA 核心 domain 在 jedi-login / jedi-mfa 外部套件;主專案 route 做 HTTP 邊界 + captcha + MFA 分流編排。登入成功後由 jedi-auth user_lookup_loader 於後續請求解 JWT 注入 RLS session 變數(tenant / org paths)。

9. DB

9.1 資料表總清單(本頁讀寫的全部表)

表 讀/寫 說明 欄位詳述
public.users 讀 / 寫 帳密驗證;login_failed_count / last_login_* 更新;mfa_type / topt_secret 讀 _overview §2
public.login_tokens 寫 發 JWT 寫 jti;logout 改 is_revoked=1 _overview §2
public.totp_secrets 讀 / 寫 TOTP 註冊寫 secret+uri;驗證讀 _overview §2
public.login_logs 寫 登入稽核 _overview §2

9.2 ER 圖(本頁讀寫範圍)

見 _overview §2 認證資料模型(assets/au-auth-data-model.dot)——本頁讀寫 users / login_tokens / totp_secrets / login_logs。

9.3 核心欄位

users 認證相關欄位(password / status 0-1--1--2 / login_failed_count / last_login_success_time / last_login_failed_time / topt_secret / mfa_type)見 _overview §2;login_tokens(jti / token_type / user_identity / is_revoked / expires)、totp_secrets(secret / uri)同。

10. 頁面邏輯與資料對應

登入時序:填帳密 + captcha → POST /login → 依回應分流(一般存 token 進 dashboard / mfa_required 轉 OTP / change_password 轉重設)。OTP:填 6 碼 → POST /otp-verify → 存 token。

關鍵欄位對應(FE ↔ API):

畫面元素 FE state API 欄位
帳號 / 密碼 user.username / user.password username / password
認證方式 user.provider provider(password/ldap)
captcha token turnstileToken cf_turnstile_token
記住我 rememberMe → cm_persist_mode —(前端持久化)
OTP 6 碼 v1..v6 code
MFA 型別 / uid mfa_type / user_uid type / uid

Token 流:一般登入回 access_token + refresh_token;MFA 待 OTP 驗證後才回 token。cm_persist_mode=local(記住我)token 跨瀏覽器重啟保留;=session 心跳過期清 token(§12)。

錯誤對應:LOGIN_401001(登入失敗)/ 401004(鎖定)/ 401011(停用)/ 401012(未啟用過期)/ 401008(長期未用)/ LOGIN_404001(不存在);OTP MFA_401001 / AUTH_MFA_401001(TOTP)→ toast。

11. 背景行為與外部依賴

類型 內容
Captcha Cloudflare Turnstile;sitekey 由 VITE_TURNSTILE_SITE_KEY;BE 以 jedi-captcha 驗 token + client IP(登入前)
Email OTP email 型 MFA 由 EmailMfaService 產碼 + 非同步寄信(jedi-notification / SMTP,見 forget-password §11)
JWT jedi-login / flask-jwt-extended;access + refresh;login_tokens jti 供撤銷;/refresh 換新 access
RLS 登入本身不設 RLS 變數;後續請求由 jedi-auth user_lookup_loader 解 JWT 注入 tenant / org paths
鎖定 / 冷卻 登入失敗 3 次鎖 15 分(BE);OTP 失敗 5 次鎖定(BE,計數落 DB);OTP 失敗 10s、重送 60s(FE);前門 nginx 對 /login、/otp-verify、/otp-resend 等登入類端點每 IP 每分鐘 60 次(burst 30),超過回 429
Socket 本頁無
Anthropic / Claude 無(認證流程不涉 LLM)

12. 邊界情況與已知坑

  1. MFA 是全域旗標:MFA_REQUIRED 環境變數對所有使用者生效,無 per-tenant / per-user override;使用者 mfa_type 只決定 email vs totp 通道。
  2. /otp-resend 欄位不一致已修(2026-07-08 FR-047):原本 serializer 宣告 type、route 卻讀 mfa_type kwarg → FE 送 type 讀不到、靜默 default email(TOTP 型使用者按重送實際走 email)。已改 route 讀 kwargs.get("type")、serializer type 加 OneOf(["email"])(resend 僅支援 email、收到 totp 等值 400 fail loudly,不再靜默 fallback)。
  3. 鎖定門檻硬編:失敗 3 次 / 鎖 15 分(MAX_LOCK_COUNT / USER_LOCK_TIME config);鎖定狀態即時 commit 到 DB(叢集一致)。
  4. cm_persist_mode 心跳陷阱:=session 時,心跳(cm_last_active)過期(>8s 且無其他分頁)判定瀏覽器關閉 → 清 token;曾被誤判為「登入壞掉」(pilot 截圖教訓)。維護 session 相關邏輯注意此心跳機制。
  5. MFA 待驗證期 token 未發:mfa_required=true 時 /login 回應 token 為 null,須等 /otp-verify 成功才發——FE 已正確等待,勿在 login 回應就存 token。
  6. 欄名 topt_secret:users 表 MFA 密鑰欄拼寫為 topt_secret(非 totp_secret);另有獨立 totp_secrets 表存正式 secret+uri(見 _overview §6)。
  7. captcha 在帳密驗證前:captcha 失敗不會增加 login_failed_count(先擋 captcha 再驗帳密)——避免 captcha 錯誤污染鎖定計數。
  8. OTP 發送有 server-side 節流(2026-07-09 FR-047,防 email bombing):登入首發 + 重送共用 jedi-mfa redis 節流(key otp_cooldown:{user_id}、OTP_COOLDOWN_SECONDS 預設 60s);cooldown 內再發回 429 MFA_429001(OtpResendTooFrequentError),response data.retry_after 帶剩餘秒數供 FE 精確倒數。原本只有 FE cooldown(ref、重整即重置),重整 / 換瀏覽器 / 直打 API 都能繞——已補 BE 權威節流 + FE localStorage 持久化倒數。
  9. 密碼正確時無條件重置帳號狀態,可能繞過停用 / 鎖定 gate(安全等級高,待修):process_login() 執行順序是先 auth_adapter.authenticate()(內部呼叫 LoginDomainService.is_password_correct())、再呼叫 validate_user()(內含 is_user_enabled / is_user_locked_within_limit 等狀態檢查)。但 is_password_correct()(jedi_login/domain/service/login_domain_service.py:94-128)只要密碼比對正確,無條件執行 user.status = UserStatus.ACTIVE + user.login_failed_count = 0(122-127 行)並落地到 DB——這發生在 validate_user() 的停用 / 鎖定檢查之前。也就是說:一個被系統管理員停用(status=REVOKE/INACTIVE)或因失敗次數鎖定的帳號,只要攻擊者(或帳號本人)知道正確密碼,密碼驗證這一步就會先把帳號狀態改回 ACTIVE、失敗次數歸零,validate_user() 隨後讀到的已經是被重置過的「正常」狀態,導致停用 / 鎖定 gate 實質被繞過。此 bug 位於外部套件 jedi-login(domain service 層),非主專案程式碼;本 spec 僅記錄不修,修正建議另開 bug session 走 CLAUDE.md「外部套件異動規範」(正確順序應是先驗狀態、密碼驗證成功後才重置計數)。

13. 開發與驗證

  • 跑起來:BE python main_socketio.py(port 8000,log 在 log/app.log);FE 在 compliance-manager-fe/ 起 Vite dev server。需設 VITE_TURNSTILE_SITE_KEY(FE)+ captcha 後端 key;MFA_REQUIRED 控 MFA。BE 改 service code 後必須重啟
  • 測試帳號:dev 環境登入帳號密碼見 .env / 部署文件(memory reference_dev_login)
  • 導航路徑:/ 或 /login;MFA 時自動轉 /otp-verify
  • 前置資料:啟用帳號(status=1);TOTP 測試需先綁定驗證器
  • E2E:compliance-manager-test/ repo;登入為所有 E2E 前置(auth-states 重產見 memory reference_e2e_env_switch_and_auth)
  • 相關文件:忘記 / 重設密碼見 忘記密碼 / 重設密碼、認證模型見 功能群總覽;DB / API 全量見 GAI-SD-02/04(Security Design)