從 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>
88 lines
3.0 KiB
Go
88 lines
3.0 KiB
Go
// 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)
|
||
}
|