jim800121chen 22f0837ba8 feat(visionA-backend): Phase 0 → 0.7 雲端後端(雙 binary + OIDC BFF + stage 部署)
從 edge-ai-platform POC 轉為正式產品的雲端後端,含以下整合階段:

- Phase 0:雛形骨架 — `cmd/api-server` (REST :3721) + `cmd/remote-proxy`
  (tunnel :3800 / internal :3801) 雙 binary 共用 internal/,沿用 POC 的
  WebSocket+yamux tunnel 協定但解耦 relay 與 API
- Phase 0.6:OIDC BFF 接 Innovedus Member Center
  - internal/oidc package(coreos/go-oidc + PKCE S256 + state + nonce)
  - internal/usersession package(HMAC-SHA256 cookie + RotateSessionID
    防 session fixation, OWASP ASVS V3.2.1)
  - 4 個 OIDC handler(/api/auth/login|callback|me|logout)+ AuthMiddleware
  - 完全拔除 StaticAuthProvider,OIDC 是唯一認證路徑
  - 9 個 ADR(含 ADR-010 BFF / ADR-011 取代 static auth /
    ADR-012 pending session shared cookie / ADR-013 PKCE-only public client)
- Phase 0.7:A1 改造 + security audit 修復
  - OIDC ClientSecret 變選填,支援 stage MC 的 public PKCE-only client
    (AuthStyleInParams 強制 token endpoint 不送 client_secret)
  - 預留 ServiceClient* 欄位給未來 client_credentials grant
  - 移除 13+ 處 resolveUserID(uc, StaticUserID) fallback 改 strict mode
    (Audit C1:multi-tenant 隔離破口)
  - Pairing exchange MarkUsed 失敗 abort + revoke session token(Audit M3)
  - 新增 all_endpoints_require_auth_test 整合測試(51 endpoint × 401)

驗證:go test -race -count=3 ./... 17 packages 全綠 / go vet 0 warning

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:21:20 +08:00

88 lines
3.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package oidctest — flow.go
//
// 提供「站在 caller (BFF backend) 角度」模擬完整 OIDC redirect flow 的 helper。
// 主要用於 e2e 整合測試:把「使用者打開瀏覽器、輸入帳密、按下同意」這幾個人工步驟
// 黑箱化成一個函式呼叫。
package oidctest
import (
"fmt"
"net/http"
"net/url"
"testing"
)
// SimulateAuthorizationFlow 模擬「使用者打開 /authorize → 同意登入 → IdP 302 回 redirect_uri」。
//
// 給定一個由 visionA-backend 產出的 authorize URL內含 client_id / redirect_uri /
// state / code_challenge / nonce本函式
//
// 1. 對 fake server 的 /authorize 發 GET禁止 redirect 自動跟隨
// 2. fake server 會 302 回 redirect_uri?code=<code>&state=<state>
// 3. 把 Location header 取出回傳 — 這就是 caller 接著要打的 callback URL
//
// caller 通常拿到 callback URL 之後,會「以 BFF backend client 的角色」打
// /api/auth/callback?code=...&state=...,讓 BFF 完成 token exchange + 建 session。
//
// 用 *testing.T 直接 Fatalf 而非回 error是因為 e2e test 寫法統一:
// 任何模擬步驟出錯都應該讓 test 立即停。caller 不必到處檢查 err。
func (s *Server) SimulateAuthorizationFlow(t *testing.T, authorizeURL string) string {
t.Helper()
if authorizeURL == "" {
t.Fatalf("oidctest: SimulateAuthorizationFlow: authorizeURL is empty")
}
// 用一個拒絕 redirect 的 client這樣我們才能取到 Location header。
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
req, err := http.NewRequest(http.MethodGet, authorizeURL, nil)
if err != nil {
t.Fatalf("oidctest: build authorize request: %v", err)
}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("oidctest: GET /authorize failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusFound && resp.StatusCode != http.StatusSeeOther {
t.Fatalf("oidctest: /authorize 預期 302/303得 %d", resp.StatusCode)
}
loc := resp.Header.Get("Location")
if loc == "" {
t.Fatalf("oidctest: /authorize 回應缺 Location header")
}
// sanity check確認 Location 是合法 URL 且帶 code 參數
u, err := url.Parse(loc)
if err != nil {
t.Fatalf("oidctest: callback URL 不是合法 URL: %v", err)
}
if u.Query().Get("code") == "" {
t.Fatalf("oidctest: callback URL 缺 code 參數: %s", loc)
}
return loc
}
// AuthorizeRedirectError 是 ForceAuthorizeFailure 模擬「IdP 直接拒絕授權」場景時的回傳 error。
// 不直接讓 fake server 回 4xx 是因為真 IdP 通常會 302 帶 ?error=... 回 redirect_uri
// 讓 caller (BFF) 自行處理。
type AuthorizeRedirectError struct {
Error string
ErrorDescription string
}
// String 讓 caller 容易在 test failure 訊息中顯示。
func (e AuthorizeRedirectError) String() string {
return fmt.Sprintf("authorize_error code=%q desc=%q", e.Error, e.ErrorDescription)
}