# visionA Phase 0.6 OIDC 手動煙測 Checklist > 適用對象:Phase 0.6 OIDC + Member Center 接入完成後,使用者首次驗收。 > > 預期時間:~30 分鐘(含 OAuth client 手動設定 ~10 分鐘) > > 上位文件:`.autoflow/04-architecture/oidc-tdd.md`、`.autoflow/04-architecture/adr/adr-010-oidc-bff.md`、`docs/DEV-SETUP.md` > > 為什麼是手動而不是自動:MC admin API 目前有兩個 bug(password grant 缺 sub claim → 500、admin API 無法回傳 client_secret),導致 OAuth client 註冊無法用 script 自動化。詳見本文「§故障排除 #1」與「§已知 limitation」。 --- ## 用法 每個 checkbox 代表一個獨立可驗證的步驟,**逐項打勾**。 有 ❌ 不通過的步驟 → 看「§故障排除」對應編號 → 修完再 retry → 才往下走。 全部 ✅ 走完 = Phase 0.6 OIDC 接入驗收通過。 --- ## 前置條件 - [ ] Docker Desktop 已開(`docker info` 不噴錯) - [ ] visionA + member_center 已 git clone 平行擺放(`ls -d ../member_center` 看得到) - [ ] 已照 `docs/DEV-SETUP.md` §4 跑過 `docker compose -f docker-compose.dev.yml --env-file .env.dev up -d --build` - [ ] frontend 依賴已裝:`cd visionA-frontend && pnpm install` - [ ] frontend dev server 已開:`cd visionA-frontend && pnpm dev`,能看到 http://localhost:3000 顯示登入頁 --- ## 階段 1:基礎服務驗證(5 分鐘) 驗證 5 個 docker service + frontend 都健在。 - [ ] **1.1** `docker compose -f docker-compose.dev.yml ps` → postgres、member-center、member-center-web、visiona-proxy、visiona-api 都顯示 `Up (healthy)`;member-center-init 顯示 `Exited (0)` - [ ] **1.2** `curl -sS http://localhost:5050/.well-known/openid-configuration | head -c 200` → 回 200 + JSON,內含 `"issuer":"http://localhost:5050/"` - [ ] **1.3** `curl -sS http://localhost:5050/jwks` → 回 200 + JSON,含 `"keys": [...]` - [ ] **1.4** `curl -sS http://localhost:3721/healthz` → 回 200(visiona-api 健康) - [ ] **1.5** `curl -sS http://localhost:3800/healthz` → 回 200(visiona-proxy 健康) - [ ] **1.6** 開 http://localhost:3000 → 看到 visionA login 頁(含「使用您的 Innovedus 帳號登入」按鈕) - [ ] **1.7** 開 http://localhost:5060 → 看到 Member Center admin 後台登入頁 任一步驟失敗 → §故障排除 #1。 --- ## 階段 2:Member Center 設定(10 分鐘) > ⚠️ 因 MC admin API 兩個 bug,此階段必須走 Web UI 手動操作。詳見 §故障排除 #1。 ### 2.1 登入 MC admin - [ ] **2.1.1** 開 http://localhost:5060 - [ ] **2.1.2** 用 `.env.dev` 中的 `MC_ADMIN_EMAIL` / `MC_ADMIN_PASSWORD` 登入(預設 `admin@visiona.local` / `Admin12345!`) - [ ] **2.1.3** 進到 admin dashboard 不噴錯 ### 2.2 建 Tenant - [ ] **2.2.1** 進「Tenants」頁 - [ ] **2.2.2** 點「Create」 - [ ] **2.2.3** 填入: - Name:`visionA` - Domains:`visiona.cloud, localhost` - Status:`active` - [ ] **2.2.4** 送出 → 列表出現 `visionA` tenant ### 2.3 建 OAuth Client - [ ] **2.3.1** 進「OAuth Clients」頁 - [ ] **2.3.2** 點「Create」 - [ ] **2.3.3** 填入: - Tenant:`visionA` - Name:`visionA Cloud` - Client Type:`confidential` - Usage:`webhook_outbound`(雛形 workaround,見 §已知 limitation #3) - Redirect URIs:`http://localhost:3721/api/auth/callback` - [ ] **2.3.4** 送出 - [ ] **2.3.5** **複製 client_id**(GUID 格式,例 `a1b2c3d4-...`)→ 暫存 - [ ] **2.3.6** **複製 client_secret**(如 UI 沒給,見 §故障排除 #1.b 的 fallback)→ 暫存 ### 2.4 建 Demo User - [ ] **2.4.1** 進「Users」頁 - [ ] **2.4.2** 點「Create」 - [ ] **2.4.3** 填入: - Email:`demo@visiona.local` - Password:`Demo12345!` - Name:`Demo User`(或自填) - Tenant:選 `visionA` - [ ] **2.4.4** 送出 → users 列表出現 demo user ### 2.5 寫回 .env.dev + 切到 OIDC mode - [ ] **2.5.1** 編輯 `.env.dev`,填入 2.3.5 / 2.3.6 拿到的值: ``` VISIONA_OIDC_CLIENT_ID= VISIONA_OIDC_CLIENT_SECRET= VISIONA_AUTH_TYPE=oidc ``` - [ ] **2.5.2** 重啟 visiona-api: ``` docker compose -f docker-compose.dev.yml --env-file .env.dev up -d visiona-api ``` - [ ] **2.5.3** 看 log: ``` docker compose -f docker-compose.dev.yml logs visiona-api --tail 20 ``` - 看到 `OIDC initialized issuer=http://localhost:5050/ client_id=<你的 guid>` - 看到 `api-server listening addr=0.0.0.0:3721` - 沒有 panic / fatal 任一步驟失敗 → §故障排除 #2。 --- ## 階段 3:完整 Login Flow(5 分鐘) 驗證 OAuth Authorization Code + PKCE 走完整 redirect chain。 - [ ] **3.1** 開瀏覽器(建議無痕視窗,避免舊 cookie 干擾)→ http://localhost:3000/login - [ ] **3.2** 看到「使用您的 Innovedus 帳號登入」按鈕 - [ ] **3.3** 開 DevTools → Network tab(保持「Preserve log」勾選,這樣才看得到 redirect chain) - [ ] **3.4** 點登入按鈕 → 第一個 request 是 `GET /api/auth/login`,response 302 - [ ] **3.5** 跟隨 302 → 第二個 request 是 `GET http://localhost:5050/oauth/authorize?...`(MC 顯示登入頁) - [ ] **3.6** 確認 authorize URL 內含: - `response_type=code` - `client_id=<你的 guid>` - `scope=openid email profile` - `state=` - `code_challenge=<...>` + `code_challenge_method=S256` - `nonce=` - `redirect_uri=http://localhost:3721/api/auth/callback` - [ ] **3.7** DevTools → Application → Cookies → http://localhost:3721 → 應已有 `visiona_pending_sid`(HttpOnly) - [ ] **3.8** MC 登入頁輸入 `demo@visiona.local` / `Demo12345!` → 送出 - [ ] **3.9**(如有同意授權頁)→ 點「同意」 - [ ] **3.10** 跟隨 302 → `GET http://localhost:3721/api/auth/callback?code=...&state=...` - [ ] **3.11** 跟隨 302 → 回到 http://localhost:3000(dashboard / workspace 首頁) - [ ] **3.12** DevTools → Cookies → http://localhost:3721 → `visiona_session` 出現(HttpOnly);`visiona_pending_sid` 已被清除 - [ ] **3.13** Header 右上角顯示 demo user 的 email 或 name(visionA-frontend `/account` 應拿得到 user info) 任一步驟失敗 → §故障排除 #3。 --- ## 階段 4:API 帶 Cookie 驗證(3 分鐘) 驗證 BFF Pattern:frontend 只用 cookie,所有 API 自動帶 session。 - [ ] **4.1** DevTools → Network → 重整任意需登入頁面(如 `/devices`) - [ ] **4.2** 找到 `GET /api/auth/me` request: - Request Headers 帶 `Cookie: visiona_session=...` - Response 200,body 含 `user_id`(OIDC sub,**不應是 `demo-user`**)、`email`、`name`、`expires_at` - [ ] **4.3** 訪 http://localhost:3000/devices → 頁面正常載入(API 拿到 cookie session) - [ ] **4.4** 訪 http://localhost:3000/models → 頁面正常載入 - [ ] **4.5** 訪 http://localhost:3000/account → 頁面正常載入並顯示 user 資訊 - [ ] **4.6** 開新 tab 直接用 curl(替換 cookie 值): ``` curl -sS -b "visiona_session=<從 DevTools 複製>" http://localhost:3721/api/auth/me ``` → 200 + JSON - [ ] **4.7** curl 不帶 cookie: ``` curl -sSi http://localhost:3721/api/auth/me ``` → 401(middleware 擋下) 任一步驟失敗 → §故障排除 #4。 --- ## 階段 5:Pairing Token 綁 OIDC user(5 分鐘) > 驗證關鍵承諾:**OIDC sub → UserContext → Pairing Token 的 user binding 正確**。 > 這是 ADR-010 + oidc-tdd §9 的核心驗證點:取代 StaticAuth 後,pairing 不會再綁到 `demo-user`。 - [ ] **5.1** 訪 http://localhost:3000/devices/pair - [ ] **5.2** 找到「產生 Pairing Token」按鈕,點下去 - [ ] **5.3** 拿到 token(格式類似 `vAc_xxxxx`) - [ ] **5.4** DevTools → Network → 看 `POST /api/pairing/token` request - Request 帶 `Cookie: visiona_session=...` - Response 200 + JSON - [ ] **5.5** 看 backend log 確認 user binding: ``` docker compose -f docker-compose.dev.yml logs visiona-api --tail 50 | grep -i pairing ``` - 應看到「PairingStore Create userID=」之類 log(user_id 是 GUID 或 MC 給的 sub,**不是** `demo-user`) - [ ] **5.6**(可選,如有實作)查 storage:訪 `/account` 或對應頁面,確認該 token 列在「我的 pairing tokens」清單下 - [ ] **5.7**(可選)拿 token 餵給 local-tool agent → agent 連上 → 確認後端 log 看到 agent 連入時 binding 的 user_id 與 5.5 一致 任一步驟失敗 → §故障排除 #5。 --- ## 階段 6:登出 + 重登(3 分鐘) - [ ] **6.1** Header → User menu → 點「登出」(或訪 `/account` 找登出按鈕) - [ ] **6.2** DevTools → Network → 看 `POST /api/auth/logout` → 204 No Content - [ ] **6.3** DevTools → Cookies → `visiona_session` 已被清除(或值改成空 + Max-Age=0) - [ ] **6.4** 自動 redirect 到 `/login`(或手動訪 `/login` 確認沒登入狀態) - [ ] **6.5** 訪 http://localhost:3000/devices → 應 redirect 到 `/login`(或顯示「請先登入」),不應看到 devices 列表 - [ ] **6.6** curl `GET /api/auth/me` 不帶 cookie → 401 - [ ] **6.7** 重新點登入按鈕 → 走完 §3 整套 flow → 又能進到 dashboard - [ ] **6.8** 確認重新登入後 `/api/auth/me` 拿到的 `user_id` 跟之前**一樣**(demo user 還是同一個 OIDC sub) 任一步驟失敗 → §故障排除 #6。 --- ## 階段 7:跨頁 Refresh / 持久化(2 分鐘) 驗證 cookie 7 天 + 24h idle 的 session 生命週期。 - [ ] **7.1** 登入狀態下重整瀏覽器 → 仍登入(cookie 持久 + `/api/auth/me` 還能拿 user) - [ ] **7.2** 關掉瀏覽器 tab(**不要清 cookie**)→ 開新 tab → 訪 http://localhost:3000 → 仍登入 - [ ] **7.3** DevTools → Cookies → `visiona_session` 的 Max-Age 約 604800(7 天) --- ## 故障排除 ### #1 階段 2 卡住:MC admin 登入失敗、tenant/client 建不起來 **a. admin 帳號登不進去** - 確認 `.env.dev` 的 `MC_ADMIN_EMAIL` / `MC_ADMIN_PASSWORD` 與 docker compose 啟動時一致 - 看 `docker compose logs member-center-init` → 應有「admin user created」訊息 - 完全重置:`docker compose -f docker-compose.dev.yml down -v` → 重新 `up -d`(會重跑 init) **b. UI 沒顯示 client_secret** - 已知 MC limitation:admin UI 建 confidential client 時可能不回傳 secret - **暫時 workaround**:把 client type 改成 `public`(`VISIONA_OIDC_CLIENT_SECRET` 留空字串),visionA-backend 應允許 empty secret 通過(雛形可接受) - 永久解:MC 補 admin API 回傳 secret 的功能 **c. 為什麼整個流程不能 script 自動化** - MC password grant 在 Identity user 上有 bug:`mandatory subject claim was missing` → 500 - → 拿不到 admin token → 無法呼叫 admin API → 必須走 Web UI - 等 MC 修這兩個 bug 後,OD2 可實作 seed script,OT2 可改全自動 e2e ### #2 visiona-api 看不到 "OIDC initialized" log,或 startup 噴錯 | log 關鍵字 | 原因 | 解法 | |------|------|------| | `discovery fetch failed` | MC 未起 / port 不對 | 確認 `docker compose ps member-center` healthy;`curl localhost:5050/.well-known/openid-configuration` 通 | | `issuer URL ... did not match` | issuer trailing slash 問題 | `.env.dev` 的 `VISIONA_OIDC_ISSUER_URL` 必須結尾 `/`,例 `http://localhost:5050/` | | `IssuerURL invalid` / `ClientID required` | env 沒帶到 | docker compose 命令必須加 `--env-file .env.dev` | | `auth type "oidc" but ClientID is CHANGE_ME` | 還沒填 client_id | 完成階段 2.5.1 | ### #3 階段 3 卡住:登入 redirect chain 失敗 | 症狀 | 原因 | 解法 | |------|------|------| | 點登入按鈕沒反應 | frontend `/api/auth/login` 沒打到 backend | 確認 `VISIONA_FRONTEND_URL=http://localhost:3000`、frontend `NEXT_PUBLIC_API_BASE_URL=http://localhost:3721`(或對應 env) | | 302 到 MC 後 MC 噴 invalid_client | client_id 不對 / tenant 沒 active | 回 MC admin UI 檢查 client 是否屬於 active tenant | | MC 登入後回 callback 噴 `state_mismatch` | `visiona_pending_sid` cookie 沒帶過去 | 確認 backend `Set-Cookie` 沒設 `Domain` 限制;瀏覽器在 callback 時應該帶 cookie(DevTools → Application → Cookies 看是否真有存進去) | | callback 噴 `pending_session_not_found` | pending session 過期(10 分鐘)或 session store 重啟丟資料 | 重新點登入按鈕從頭來;雛形 in-memory store 重啟即清空 | | callback 噴 `id_token_invalid` | id_token 驗簽失敗(issuer / aud / nonce 對不上)| 看 backend log `detail` 欄;常見:JWKS cache 過時 → 重啟 visiona-api | | callback 噴 `token_exchange_failed` | client_secret 不對 / redirect_uri 不對 | 比對 `.env.dev` 與 MC admin UI 上的 client 設定 | ### #4 階段 4 卡住:API 401 / cookie 沒帶 - DevTools → Application → Cookies → 確認 `visiona_session` 真的存在 + 沒過期 - DevTools → Network → 看 request 是否真的帶 `Cookie:` header - 如果沒帶 → frontend `fetch` 沒設 `credentials: 'include'`,看 OF2 是否真的補上 - backend log `unauthorized` 帶的 detail: - `cookie_invalid` → cookie 簽章對不上 → 通常 `VISIONA_SESSION_SECRET` 變過導致舊 cookie 失效 → 清 cookie 重登 - `session_not_found` → in-memory store 被重啟清掉 → 重登 - `session_expired` → 真的過期了,重登 ### #5 階段 5:Pairing token user_id 綁錯(綁到 `demo-user` 而非 OIDC sub) **這是 OB5 / OF2 必須驗證的關鍵點**: - 看 `docker compose logs visiona-api | grep -i 'pairing\|userID\|user_id'` - 如果 user_id 是 `demo-user` → OB5 沒拔乾淨 / `VISIONA_AUTH_TYPE` 還是 `static` → 確認階段 2.5.1 真的改成 `oidc` - 如果 user_id 是空 / `unknown` → middleware 沒注入 UserContext → 看 OB3 middleware 程式碼 - 如果 user_id 是 GUID(OIDC sub)→ ✅ 正確 - 必要時開 issue:「OB5 + OF2 完成後 pairing 仍綁到 demo-user,需 hotfix」 ### #6 階段 6:登出後仍能訪問 - DevTools 確認 `visiona_session` cookie 真的被清掉 - 如果 cookie 還在 → backend `Set-Cookie: visiona_session=; Max-Age=0` 沒設對 - 訪 `/api/auth/me` 不帶 cookie 仍 200 → middleware bypass 了某些路徑 → 看 router 設定 - frontend 沒 redirect 到 `/login` → middleware(Next.js)沒檢查 session → 看 OF2/OF3 frontend route guard --- ## 已知 Limitation(Phase 0.6 雛形) | # | Limitation | 影響 | 後續處理(Phase 1) | |---|-----------|------|--------------------| | 1 | MC admin API 不接受 client_secret 輸入 / 不回傳 client_secret | 必須走 MC Web UI 手動建 OAuth client | MC 補 admin API | | 2 | MC password grant 在 Identity user 缺 sub claim → 500 | 無法用 admin token script 自動化 seed | MC TokenController 補 Subject claim mapping | | 3 | MC 只支援 `usage=webhook_outbound` 帶 redirect_uris | 命名語意不對(visionA 借用) | MC 新增 `usage=web_app` | | 4 | visionA-backend in-memory session store 重啟即消失 | 雛形階段使用者重登 OK;不適合多 instance | Phase 1 換 Redis | | 5 | host 上的 visiona-local(local-tool)佔 3721 與 docker compose 衝突 | 兩個只能擇一跑 | 預期;docs 已說明 | | 6 | 沒有 RP-initiated logout(登出 visionA 不會把 MC session 也登出)| 同 browser 下次點登入會自動跳過 MC 登入頁 | Phase 1 補 | | 7 | 沒有 refresh token rotation | session 24h idle / 7d absolute 後必須重登 | 等 MC 開 refresh,Phase 1 接 | --- ## 自動化進度 | 階段 | 自動化狀態 | 對應測試 | |------|----------|---------| | 階段 1(基礎服務)| ✅ docker-compose healthcheck 自動 | – | | 階段 2(OAuth client 設定)| ❌ **手動**(MC bug 阻擋)| Phase 1 待 MC 修 bug | | 階段 3-7(Login flow / API / Pairing / Logout)| ✅ 用 fake OIDC 自動測(OT1)| `internal/api/handlers/oidc_e2e_test.go`(17 packages 全綠 / 6 個 OIDC e2e cases)| | 階段 3-7(with 真 MC)| ❌ **手動**(依賴階段 2 手動產 client)| Phase 1 TODO | **Phase 1 TODO 清單**: 1. MC team 修 password grant sub claim bug → unblock seed script 2. MC team 補 admin API 接受 / 回傳 client_secret → unblock OAuth client 自動化 3. MC team 新增 `usage=web_app` → 移除雛形 workaround 4. visionA OD2:寫 `make dev-seed` 把 §2 全部 script 化 5. visionA OT2 真 MC e2e:把 OT1 fake OIDC 改 testcontainers 起真 MC,跑 §3-§7 全自動 6. visionA OB?:補 RP-initiated logout 7. visionA OB?:補 refresh token(等 MC 上 refresh) 8. visionA OB?:in-memory session → Redis(Phase 1 上 staging 必做) --- ## 通過條件(Phase 0.6 OIDC 接入驗收) - [ ] 階段 1-7 全綠(每個 checkbox 都打勾) - [ ] 階段 5 確認 pairing user_id 是 OIDC sub 而非 `demo-user`(**最關鍵**) - [ ] 已知 limitation 都有 Phase 1 TODO 對應 issue / 任務 全部達成 → Phase 0.6 OIDC 接入完成 ✅,可進 Phase 1 規劃。