---
title: TC-01 身分驗證
eyebrow: Guidant AI 資安檢視總報告 · 安全測試案例（TC）
h1: TC-01 身分驗證
subtitle: 驗 SR-01 八條，共 18 個案例
lede: 每條安全需求至少一個測試案例，有對應威脅的照駭客步驟反過來打、沒有的照需求的驗證方式設計。每個案例標明**用哪種測法**（API／pytest／設定稽核／e2e／人工）與**依矩陣的預期現況**——預期過的是回歸守門，預期不過的是已知缺口，跑出來不會把已知缺口當新 bug。
chips:
  - { text: "18 案例", kind: accent }
  - { text: "API 11", kind: plain }
  - { text: "pytest 2", kind: plain }
  - { text: "人工 5", kind: plain }
  - { text: "預期不過 4", kind: warn }
---

> 讀法見[測試案例總表](TC-00-overview.html)。案例編號 `TC-<章>.<SR 序><字母>`：`TC-01.6b` 是驗 SR-01.6 的第二個案例。「預期現況」取自[符合性矩陣](../requirements/SR-matrix.html)與連線頁現況欄，標 **預期過**／**預期不過（已知缺口）**／**部分**。

## 這一章驗什麼

[SR-01 身分驗證](../requirements/SR-01-identity.html)八條：人的登入與忘記密碼、OAuth 回呼、機器（代理程式）的報到與通行證、即時連線。測法分布：守門類走 **API**（帶對方的 token 或不帶 token 直接打端點，一秒一條）；鎖定計數走 **pytest**（要控制 Redis 狀態）；回呼頁要兩個瀏覽器與 Drive 應用程式設定、要架假 LDAP 的走**人工**。

| 案例 | 驗哪條 | 一句話 | 測法 | 預期現況 |
|---|---|---|---|---|
| TC-01.1a | SR-01.1 | 忘記密碼回應不帶鑰匙、成敗一致 | API | 預期過 |
| TC-01.1b | SR-01.1 | 全部不需登入端點的回應掃一遍不含祕密欄位 | API | 預期過（判準：名稱＋值形狀） |
| TC-01.2a | SR-01.2 | LDAP 正確帳號＋錯密碼必須 401 | 人工 | 預期過 |
| TC-01.2b | SR-01.2 | 綁定 Google 帳號不帶 id_token 必須被拒 | API | 預期過（入口已關） |
| TC-01.3a | SR-01.3 | LDAP 連續錯密碼達上限後鎖定 | 人工 | **預期不過**（LDAP 失敗不計數） |
| TC-01.3b | SR-01.3 | 雙因子驗證碼連續猜錯達上限後鎖定 | API | 預期過 |
| TC-01.4a | SR-01.4 | 回呼網址在另一個瀏覽器開必須被拒 | 人工 | 預期過 |
| TC-01.4b | SR-01.4 | 同一回呼網址第二次開必須被拒 | 人工 | 預期過 |
| TC-01.5a | SR-01.5 | Redis 重啟後雙因子失敗計數不歸零 | pytest | **預期不過**（計數只在 Redis） |
| TC-01.5b | SR-01.5 | Redis 停掉時雙因子驗證拒絕而非放行 | pytest | **預期不過**（行為未盤） |
| TC-01.6a | SR-01.6 | 心跳不帶通行證必須 401 | API | 預期過 |
| TC-01.6b | SR-01.6 | 帶 A 的通行證、自報 B 的編號，後端以 A 處理 | API | 預期過 |
| TC-01.6c | SR-01.6 | 撤銷 A 後四條通道立即全部 403 | API | 預期過 |
| TC-01.7a | SR-01.7 | 憑證申請名稱填別台的指紋，報到必須被拒 | 人工 | 預期過 |
| TC-01.7b | SR-01.7 | 註冊 token 超過有效期或次數後報到必須被拒 | API | **預期不過**（token 無效期無次數） |
| TC-01.8a | SR-01.8 | 不帶 JWT 交握必須被拒 | API | 預期過 |
| TC-01.8b | SR-01.8 | 專案 A 成員對專案 B 房號送 update（不先 join）必須被拒 | API | 預期過 |
| TC-01.8c | SR-01.8 | update 帶別人的 user，落庫署名必須是 JWT 裡的人 | API | 預期過 |

---

## TC-01.1a 忘記密碼回應不帶鑰匙、成敗一致 {#tc-01-1a}

| 欄位 | 內容 |
|---|---|
| **驗哪條** | [SR-01.1](../requirements/SR-01-identity.html#sr-01-1) 不需登入的入口，回應不得帶可直接使用的祕密 |
| **對應威脅** | C01 [T1 不需登入的入口把祕密從回程交出去](../connections/C01-browser-to-frontdoor.html) 🔴（實例 [M13-1](../STRIDE/S-spoofing.html#m13-1)） |
| **測法** | **API**——不開瀏覽器，直接對 `POST /api/1.0/forget-password` 送請求 |
| **前置** | DEV 環境；一個已存在的使用者信箱 `exists@…`；一個不存在的 `nobody@…`；遮罩設定（`FORGET_PASSWORD_MASK`）維持預設開 |
| **步驟** | ① 不帶任何登入資訊，送 `{"email": "exists@…"}`；② 記下狀態碼與完整回應 body；③ 同樣送 `{"email": "nobody@…"}`；④ 比對兩次 |
| **預期結果** | 兩次狀態碼相同、body 逐位元組相同；body 不含 `uid`、`token`、`code`、`request_uid` 任何一個 key；回應時間差 < 100ms（不讓時間差洩漏信箱存不存在） |
| **預期現況** | **預期過**——連線頁 C01 D1：回應已改固定空白、成功失敗逐位元組相同（CM-1575） |
| **自動化落點** | test repo `site-regression/api/`（Playwright request context，不開瀏覽器） |

## TC-01.1b 全部不需登入端點的回應掃一遍不含祕密欄位 {#tc-01-1b}

| 欄位 | 內容 |
|---|---|
| **驗哪條** | [SR-01.1](../requirements/SR-01-identity.html#sr-01-1) |
| **對應威脅** | 同上 🔴——這個案例驗的是「通則」，不是單一端點 |
| **測法** | **API**＋**設定稽核**——先用程式列出所有不掛身分檢查的路由（SR-02.13 要的清單），再逐一打 |
| **前置** | 路由守門清單 `scripts/deliverables/collect_route_guards.py` 產的 `route_guards.json`；免登入 POST 各準備一組打得進 handler、不改真資料的請求 |
| **步驟** | ① 產「只驗 JWT 或完全不驗」的路由清單；② 對每支送請求；③ 回應 body 遞迴掃 key 名片段含 `token`／`secret`／`password`／`key`／`uid`（排除資源本身的 uid）；④ 名稱命中的再看值形狀：純英數短代碼（`^[a-z0-9_\-.]{1,40}$`，且不是 20 字以上夾數字）不算，JWT 形狀、含 `=`／`+`／`/` 的 base64 特徵、超過 40 字的才算 |
| **預期結果** | 零命中 |
| **預期現況** | **預期過**——名稱命中的六處全是設定說明欄位（如 `key`＝`"ssh"`、`"base_url"`），值是代碼不是祕密；只看名稱會永遠紅，故判準加值形狀。範圍：免登入 14 支全掃、只驗 JWT 的 GET 無路徑參數 32 支 |
| **自動化落點** | 清單腳本 `scripts/deliverables/collect_route_guards.py`；掃描 test repo `security-compliance/features/SR-01/sr-01-1-anonymous-no-secret.feature` |

## TC-01.2a LDAP 正確帳號＋錯密碼必須 401 {#tc-01-2a}

| 欄位 | 內容 |
|---|---|
| **驗哪條** | [SR-01.2](../requirements/SR-01-identity.html#sr-01-2) 外部身分提供者登入必須真的向提供者驗證 |
| **對應威脅** | C11 [T1 只查人、不驗密碼就放行](../connections/C11-api-to-ldap.html) 🟠（實例 [M13-2](../STRIDE/S-spoofing.html#m13-2)） |
| **測法** | **人工**——要一台真的 LDAP／AD（DEV 有 192.168.50.160），且要看目錄那端的日誌 |
| **前置** | 系統設定 → 登入方式設 LDAP，指向測試 AD；一個 AD 上存在的帳號 |
| **步驟** | ① 登入頁輸入該帳號＋錯誤密碼；② 看回應；③ 到 AD 伺服器看安全日誌（事件 4625 登入失敗） |
| **預期結果** | ② 401；③ AD 日誌有一筆該帳號的失敗 bind——證明密碼真的送去驗了，不是只查到人就放行 |
| **預期現況** | **預期過**——C11 D1 ✅ 兩段式，DEV 真實 AD 手測三態 |
| **自動化落點** | 人工（可轉 API 層但要 mock LDAP，先不做） |

## TC-01.2b 綁定 Google 帳號不帶 id_token 必須被拒 {#tc-01-2b}

| 欄位 | 內容 |
|---|---|
| **驗哪條** | [SR-01.2](../requirements/SR-01-identity.html#sr-01-2) |
| **對應威脅** | C04 [T3 「用 Google 身分」的入口沒真的向 Google 驗證](../connections/C04-browser-to-api-drive-oauth.html) 🟠（實例 [M13-5](../STRIDE/S-spoofing.html#m13-5)） |
| **測法** | **API** |
| **前置** | 一般使用者的 JWT |
| **步驟** | ① 帶 JWT 對綁定外部帳號端點送 `{"provider": "google", "external_id": "任意字串"}`，不帶 `id_token`；② 看回應；③ 查該使用者的綁定紀錄 |
| **預期結果** | ② 400 或 405（入口已關）；③ 綁定紀錄沒有新增 |
| **預期現況** | **預期過**——C04 D3：決策者裁定關閉入口，登入方式白名單不含 Google。⚠️ 但是「關掉」不是「修好」，程式還在；若日後重開要先補驗證，這個案例會變成守門 |
| **自動化落點** | test repo API 層 |

## TC-01.3a LDAP 連續錯密碼達上限後鎖定 {#tc-01-3a}

| 欄位 | 內容 |
|---|---|
| **驗哪條** | [SR-01.3](../requirements/SR-01-identity.html#sr-01-3) 登入失敗鎖定涵蓋所有登入路徑 |
| **對應威脅** | 無掃描實例（標準做法） |
| **測法** | **人工**（要真 LDAP）；之後可轉 pytest mock adapter |
| **前置** | 登入方式 LDAP；`LOGIN_MAX_LOCK_COUNT`（預設 5）；一個 AD 帳號 |
| **步驟** | ① 用該帳號連續輸錯密碼 5 次；② 第 6 次輸入**正確**密碼 |
| **預期結果** | ② 必須被拒（帳號已鎖），回應與本地帳號鎖定時相同 |
| **預期現況** | **預期不過（已知缺口）**——C11 D5◇ ⚠️：失敗計數只在 `is_password_correct()`，LDAP adapter 第二段 bind 失敗直接拋 401 不碰 `login_failed_count`。第 6 次正確密碼會直接登入成功 |
| **自動化落點** | 修了之後轉 pytest（mock LDAP adapter 回 bind 失敗，斷言 `login_failed_count` 遞增） |

## TC-01.3b 雙因子驗證碼連續猜錯達上限後鎖定 {#tc-01-3b}

| 欄位 | 內容 |
|---|---|
| **驗哪條** | [SR-01.3](../requirements/SR-01-identity.html#sr-01-3) |
| **對應威脅** | 關聯 C02 T6 帳號與登入本身的洞（實例 [M13-6](../STRIDE/S-spoofing.html#m13-6) 雙因子驗證碼可以無限次猜） |
| **測法** | **API** |
| **前置** | 一個開了雙因子的帳號；完成第一階段登入拿到待驗證的 session |
| **步驟** | ① 對驗證碼端點連續送錯誤碼直到上限；② 再送一次**正確**碼 |
| **預期結果** | ② 必須被拒；回應不洩漏「還剩幾次」以外的資訊 |
| **預期現況** | **預期過**——M13-6 已修（套件 `jedi_iam.mfa` 有失敗計數）；但見 TC-01.5a，計數在 Redis |
| **自動化落點** | test repo API 層 |

## TC-01.4a 回呼網址在另一個瀏覽器開必須被拒 {#tc-01-4a}

| 欄位 | 內容 |
|---|---|
| **驗哪條** | [SR-01.4](../requirements/SR-01-identity.html#sr-01-4) OAuth 回呼同時驗連結與瀏覽器 |
| **對應威脅** | C04 [T2 授權連結轉寄給別人代按](../connections/C04-browser-to-api-drive-oauth.html) ⚪（實例 [M24-10](../STRIDE/S-spoofing.html#m24-10)） |
| **測法** | **人工**——要兩個瀏覽器 session 與已設定的 Drive 應用程式（190 未設定）；可轉 e2e |
| **前置** | Drive 整合已設定；管理員帳號；Playwright 兩個 context A、B |
| **步驟** | ① A 登入，系統設定 → 雲端硬碟 → 連結，攔截導向 Google 前的授權網址，記下 `state`；② 不真的去 Google，直接組回呼網址 `…/callback?code=假&state=<記下的>`；③ 在 B（沒有 A 的 cookie）開這個網址；④ 查 Drive 綁定紀錄 |
| **預期結果** | ③ 回呼頁顯示錯誤代碼（`GRC_400134` 類）、不換票；④ 沒有新增綁定 |
| **預期現況** | **預期過**——C04 D2 ✅ `browser_nonce` cookie 綁定，缺 cookie 一律拒 |
| **自動化落點** | 人工（步驟見[人工驗證清單](TC-manual-checklist.html#sr-01-4)）；190 填一組假的 Drive 應用程式設定後可轉 Playwright 多 context |

## TC-01.4b 同一回呼網址第二次開必須被拒 {#tc-01-4b}

| 欄位 | 內容 |
|---|---|
| **驗哪條** | [SR-01.4](../requirements/SR-01-identity.html#sr-01-4) |
| **對應威脅** | 同上 |
| **測法** | **e2e** |
| **前置** | 同 TC-01.4a |
| **步驟** | ① 在 A 完成一次正常回呼（或走 TC-01.4a 的假 code 讓它失敗一次）；② 在 A 再開同一個回呼網址 |
| **預期結果** | ② 被拒——state 一次性，用過即刪 |
| **預期現況** | **預期過**——C04 D2 ✅ `handle_callback()` 先刪 state 再比對 |
| **自動化落點** | 同上，與 TC-01.4a 同一個 feature |

## TC-01.5a Redis 重啟後雙因子失敗計數不歸零 {#tc-01-5a}

| 欄位 | 內容 |
|---|---|
| **驗哪條** | [SR-01.5](../requirements/SR-01-identity.html#sr-01-5) 安全計數不得只存易失儲存 |
| **對應威脅** | C06 [T2 拿到 Redis 帳密後歸零節流計數](../connections/C06-api-to-redis.html)（無掃描實例） |
| **測法** | **pytest**——要控制 Redis 狀態，進程式裡測比較穩 |
| **前置** | 測試用 Redis；`jedi_iam.mfa` 的失敗計數服務 |
| **步驟** | ① 對帳號 X 記 N−1 次雙因子失敗；② `FLUSHALL`（模擬重啟，持久化本來就關）；③ 再記一次失敗，查是否達鎖定 |
| **預期結果** | ③ 達鎖定（計數在 DB，或 Redis 有持久化） |
| **預期現況** | **預期不過（已知缺口）**——C06 D4◇ ⚠️：計數只在 Redis，Redis 重啟即歸零 |
| **自動化落點** | BE repo `test/`（套件層的在套件 repo） |

## TC-01.5b Redis 停掉時雙因子驗證拒絕而非放行 {#tc-01-5b}

| 欄位 | 內容 |
|---|---|
| **驗哪條** | [SR-01.5](../requirements/SR-01-identity.html#sr-01-5) |
| **對應威脅** | 同上 |
| **測法** | **pytest** |
| **前置** | 同上 |
| **步驟** | ① 把 Redis 連線設成不可用（錯的 host）；② 呼叫雙因子驗證（送正確碼） |
| **預期結果** | ② 拋錯誤、拒絕登入——不能因為計數讀不到就當成零次失敗放行 |
| **預期現況** | **預期不過（已知缺口）**——C06 D4◇ ⚠️：「Redis 連不上時的行為沒有盤」，各呼叫端自己決定。跑出來才知道是拒絕還是放行；放行就是缺口 |
| **自動化落點** | 同上 |

## TC-01.6a 心跳不帶通行證必須 401 {#tc-01-6a}

| 欄位 | 內容 |
|---|---|
| **驗哪條** | [SR-01.6](../requirements/SR-01-identity.html#sr-01-6) 機器身分只認簽章通行證 |
| **對應威脅** | C20 [T1 冒充代理程式——後端認不出對方是哪台機器](../connections/C20-agent-to-frontdoor.html) 🔴（實例 [M01-1](../STRIDE/S-spoofing.html#m01-1)） |
| **測法** | **API**——照 T1 的步驟 ①②：不需帳號，照代理程式格式打心跳 |
| **前置** | 一台已註冊的 agent 的機器編號（從管理畫面抄） |
| **步驟** | ① 不帶 `Authorization`，對 `POST /api/1.0/agents/heartbeat` 送 `{"agent_uid": "<那台的編號>"}` |
| **預期結果** | 401；回應不含任何派工內容 |
| **預期現況** | **預期過**——C20 D1 ✅ `agent_pass_required` 守門 |
| **自動化落點** | test repo API 層 |

## TC-01.6b 帶 A 的通行證、自報 B 的編號，後端以 A 處理 {#tc-01-6b}

| 欄位 | 內容 |
|---|---|
| **驗哪條** | [SR-01.6](../requirements/SR-01-identity.html#sr-01-6) |
| **對應威脅** | 同上 🔴——T1 步驟 ③「後端若只讀自報編號就相信」 |
| **測法** | **API** |
| **前置** | 兩台已註冊的 agent A、B；A 的通行證；B 名下有一張待領的派工單、A 名下沒有 |
| **步驟** | ① 帶 A 的通行證打心跳，body 寫 `{"agent_uid": "<B 的編號>"}`；② 看回應裡有沒有派工單；③ 查 B 那張單的狀態 |
| **預期結果** | ② 空（A 名下沒單）或 403；③ B 的單仍是待領，沒被 A 領走 |
| **預期現況** | **預期過**——C20 D1 ✅ 驗簽後只認 `sub` |
| **自動化落點** | test repo API 層 |

## TC-01.6c 撤銷 A 後四條通道立即全部 403 {#tc-01-6c}

| 欄位 | 內容 |
|---|---|
| **驗哪條** | [SR-01.6](../requirements/SR-01-identity.html#sr-01-6) |
| **對應威脅** | 同上 🔴——T1 步驟 ⑤「撤銷後若通道不查撤銷」 |
| **測法** | **API** |
| **前置** | 一台已註冊的 agent A 與其通行證；一張派給 A 的單 |
| **步驟** | ① 用 A 的通行證各打一次心跳、確認收到、回報結果、下載檔案，記下都成功；② 管理畫面撤銷 A；③ **不等待**，立刻用同一張通行證再打四條 |
| **預期結果** | ③ 四條全部 403（不是 401——通行證驗簽仍過，是撤銷查表擋的） |
| **預期現況** | **預期過**——C20 D1 ✅ 每請求查 `remote_agents`；DEV 實測撤銷後四通道全 403 |
| **自動化落點** | test repo API 層 |

## TC-01.7a 憑證申請名稱填別台的指紋，報到必須被拒 {#tc-01-7a}

| 欄位 | 內容 |
|---|---|
| **驗哪條** | [SR-01.7](../requirements/SR-01-identity.html#sr-01-7) 機器報到比其他端點更嚴 |
| **對應威脅** | C20 [T1 冒充代理程式](../connections/C20-agent-to-frontdoor.html) 🔴 |
| **測法** | **人工**——要自己產憑證申請檔（CSR），步驟多 |
| **前置** | 有效的註冊 token；兩台機器 A、B 的硬體指紋；openssl |
| **步驟** | ① 在 A 上產私鑰與 CSR，但 CN 填 B 的指紋；② 帶 token 對 `POST /api/1.0/agents/register` 送這份 CSR 與 A 的指紋 |
| **預期結果** | 被拒——CSR 名稱必須等於本機指紋 |
| **預期現況** | **預期過**——C20 D2 ✅ `agent_enrollment_service.py` 三件都有 |
| **自動化落點** | 人工；可轉 API（測試裡用 node `crypto` 產 CSR，接 SR-01.6 的自行報到流程） |

## TC-01.7b 註冊 token 超過有效期或次數後報到必須被拒 {#tc-01-7b}

| 欄位 | 內容 |
|---|---|
| **驗哪條** | [SR-01.7](../requirements/SR-01-identity.html#sr-01-7) |
| **對應威脅** | 同上 🔴 |
| **測法** | **API** |
| **前置** | 一組註冊 token |
| **步驟** | ① 用同一組 token 報到兩台不同的機器；② 查 token 有沒有「有效期」「剩餘次數」欄位 |
| **預期結果** | ① 第二台應被拒（或 token 有次數上限且用完）；② 欄位存在 |
| **預期現況** | **預期不過（已知缺口）**——C20 D2 ⚠️：`enroll_token` 實體只有 `enabled` 旗標，無過期無次數。第二台會成功 |
| **自動化落點** | test repo API 層（修了之後變守門） |

## TC-01.8a 不帶 JWT 交握必須被拒 {#tc-01-8a}

| 欄位 | 內容 |
|---|---|
| **驗哪條** | [SR-01.8](../requirements/SR-01-identity.html#sr-01-8) 即時連線每個事件重注入伺服器端身分 |
| **對應威脅** | C03 [T1 頻道不查房間歸屬](../connections/C03-frontdoor-to-socketio.html) 🟠（實例 [M24-9](../STRIDE/I-information-disclosure.html#m24-9)） |
| **測法** | **API**——socket.io client 不開瀏覽器 |
| **前置** | socketio 端點 `/socket.io`，namespace `/socket/fill-survey` |
| **步驟** | ① 不帶 `auth.token` 連 namespace |
| **預期結果** | 交握被拒（`connect_error`），不進入任何房間 |
| **預期現況** | **預期過**——C03 D1 ✅ 繼承 `AuthenticatedNamespace` |
| **自動化落點** | test repo API 層（socket.io-client） |

## TC-01.8b 專案 A 成員對專案 B 房號送 update（不先 join）必須被拒 {#tc-01-8b}

| 欄位 | 內容 |
|---|---|
| **驗哪條** | [SR-01.8](../requirements/SR-01-identity.html#sr-01-8) |
| **對應威脅** | 同上 🟠——T1 步驟「猜一個房號就進去」 |
| **測法** | **API** |
| **前置** | 使用者 U 是專案 A 成員、不是 B 成員；B 有一份問卷，其房號（task uid）已知 |
| **步驟** | ① 帶 U 的 JWT 連 namespace；② **不送 `join`**，直接送 `update` 事件、房號填 B 的 |
| **預期結果** | 事件被拒、只回給 U 自己錯誤；B 的問卷沒有變更；B 房間裡其他人沒收到廣播 |
| **預期現況** | **預期過**——C03 D1 ✅ 六個事件全部 `_assert_room_member` |
| **自動化落點** | test repo API 層 |

## TC-01.8c update 帶別人的 user，落庫署名必須是 JWT 裡的人 {#tc-01-8c}

| 欄位 | 內容 |
|---|---|
| **驗哪條** | [SR-01.8](../requirements/SR-01-identity.html#sr-01-8) |
| **對應威脅** | C03 [T2 署名由畫面送上來](../connections/C03-frontdoor-to-socketio.html) 🟡（實例 [M05-9](../STRIDE/R-repudiation.html#m05-9)） |
| **測法** | **API** |
| **前置** | U 是專案 A 成員；A 的問卷房號；另一個使用者 V 的 login_name |
| **步驟** | ① 帶 U 的 JWT 連線、`join` A 的房號；② 送 `update`，body 的 `user` 欄填 V；③ 查該筆答案的 `updated_user` |
| **預期結果** | ③ 是 U 不是 V |
| **預期現況** | **預期過**——C03 D2 ✅ `on_update` 署名取 `get_user_context().login_name`。⚠️ 但 `join`／`leave`／`edit`／`endedit` 四個事件的廣播 `user` 仍取畫面值——那是顯示不是落庫，另開案例驗廣播內容時再補 |
| **自動化落點** | test repo API 層 |

---

*案例從 [SR-01](../requirements/SR-01-identity.html) 的驗證方式與對應威脅的「駭客怎麼打」反推；預期現況取自[符合性矩陣](../requirements/SR-matrix.html)（2026-10-02）。測試實跑後結果回填矩陣。*
