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

112 lines
4.9 KiB
Go
Raw 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 oidc
import (
"context"
"time"
)
// ProviderConfig 是建立 Provider 所需的所有設定。
//
// 全部欄位都從環境變數帶入(見 oidc-tdd.md §13.1),不在程式碼中 hardcode。
// caller 應在啟動時驗證所有必填欄位非空。
type ProviderConfig struct {
// IssuerURL 是 OIDC Identity Provider 的 issuer例如 https://member-center.dev.innovedus.com
// 結尾不帶斜線。NewProvider 會以此為 base 抓 .well-known/openid-configuration。
IssuerURL string
// ClientID 是 visionA 在 Member Center 註冊的 OAuth client_idconfidential 或 public 皆可)。
ClientID string
// ClientSecret 是 confidential client 的 secret不可外洩到 frontend / log。
//
// A12026-05-01ClientSecret 為**選填**
// - 有值 → confidential client modeclient_secret + PKCE 雙保險)
// - 留空 → public PKCE-only client mode純依靠 PKCE 防 code interception
// 兩種 mode 都符合 OAuth 2.1,由 IdP 註冊 client 時決定。
ClientSecret string
// RedirectURL 是 visionA-backend 的 callback URL例如
// http://localhost:8080/api/auth/callbackdev
// https://app.visiona.cloud/api/auth/callbackprod
// 必須與在 Member Center 註冊的 redirect_uri 完全一致。
RedirectURL string
// Scopes 是 OIDC scope 清單,預設 ["openid", "email", "profile"]。
// 若為空NewProvider 會套用預設值。
Scopes []string
}
// DefaultScopes 是 OIDC 標準 scope 集合,能取得 sub / email / name 三個 claim。
// 對齊 oidc-tdd.md §7.3 的 Claim Mapping。
var DefaultScopes = []string{"openid", "email", "profile"}
// Provider 是本 package 對外的唯一 interface封裝 OIDC Authorization Code + PKCE 流程。
//
// 設計理由:以 interface 為公開 API內部實作目前以 coreos/go-oidc/v3 為基礎)可未來替換
// 而不影響 callerOB3 / OB4 的 OIDCAuthService 與 auth handler
//
// 所有方法都應是 goroutine-safe底層 coreos provider 與 oauth2.Config 皆為 immutable
// JWKS / discovery 快取由 coreos lib 內部以 RWMutex 保護。
type Provider interface {
// AuthorizationURL 組出讓 user 跳轉到 IdP 登入畫面的 URL。
//
// 三個隨機值由 caller通常是 auth handler以 GenerateState/Nonce/CodeVerifier 產生並存
// pending sessionCodeChallenge 是 CodeVerifier 經 SHA256+base64url 後的值。
//
// 回傳的 URL 已含 response_type=code、scope、PKCE、state、nonce 參數。
AuthorizationURL(state, nonce, codeChallenge string) string
// ExchangeCode 用 authorization code + code_verifier 向 token endpoint 換 token set。
//
// 錯誤對應:
// - 401 / invalid_grant → ErrInvalidGrantcode 用過 / 過期 / verifier 不符)
// - 其他 4xx/5xx / 網路錯誤 → ErrTokenExchange包 inner error
ExchangeCode(ctx context.Context, code, codeVerifier string) (*TokenResponse, error)
// VerifyIDToken 驗 id_token 簽章與必驗 claim
// - 簽章:以 JWKS 對應 kid 的 public key 驗 RS256簽章演算法由 IdP 決定,
// coreos lib 預設信任 IdP discovery 宣告的 id_token_signing_alg_values_supported
// - iss == cfg.IssuerURL
// - aud 包含 cfg.ClientID
// - exp > now含預設 leeway
// - nonce == expectedNonce
//
// 錯誤對應:簽章/iss/aud/exp 失敗回 ErrInvalidIDToken包 inner
// nonce 不符回 ErrInvalidNonce。
VerifyIDToken(ctx context.Context, rawIDToken, expectedNonce string) (*Claims, error)
}
// TokenResponse 是 token endpoint 回傳的 token set。
//
// 對齊 RFC 6749 §5.1 + OpenID Connect Core §3.1.3.3。
// 不包含 IdToken 以外的 raw JWT已分別放欄位caller 拿到後通常會:
// 1. 把 IDToken 餵給 VerifyIDToken 拿 claims
// 2. 把 AccessToken 存進 server-side sessionvisionA BFF 模式不交給 frontend
// 3. 雛形 Phase 0.6 不用 RefreshToken見 ADR-010 §「負面影響」)
type TokenResponse struct {
AccessToken string
IDToken string
RefreshToken string
TokenType string // 預期固定 "Bearer"
ExpiresIn int // access_token 有效秒數IdP 指定)
}
// Claims 是 id_token 驗證通過後解出來的標準 + 自定 claim。
//
// Subject / Email / Name 對齊 oidc-tdd.md §7.3 的 Claim Mapping
// 後續 OB3 的 OIDCAuthService 會以這三個欄位建 user session。
//
// Raw 保留底層 lib 解出的完整 claim map未來若需要 picture / preferred_username
// 等額外欄位,可從 Raw 取出而不需要改 Claims struct。
type Claims struct {
Subject string // OIDC sub
Email string // OIDC emailscope=email
Name string // OIDC namescope=profile
Issuer string // iss
Audience string // aud取第一個 audienceOIDC 多 aud 時 Member Center 不使用)
IssuedAt time.Time // iat
ExpiresAt time.Time // exp
Nonce string // nonce
Raw map[string]any
}