從 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>
112 lines
4.9 KiB
Go
112 lines
4.9 KiB
Go
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_id(confidential 或 public 皆可)。
|
||
ClientID string
|
||
|
||
// ClientSecret 是 confidential client 的 secret;不可外洩到 frontend / log。
|
||
//
|
||
// A1(2026-05-01):ClientSecret 為**選填**:
|
||
// - 有值 → confidential client mode(client_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/callback(dev)或
|
||
// https://app.visiona.cloud/api/auth/callback(prod)。
|
||
// 必須與在 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 為基礎)可未來替換
|
||
// 而不影響 caller(OB3 / 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 session;CodeChallenge 是 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 → ErrInvalidGrant(code 用過 / 過期 / 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 session(visionA 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 email(scope=email)
|
||
Name string // OIDC name(scope=profile)
|
||
Issuer string // iss
|
||
Audience string // aud(取第一個 audience;OIDC 多 aud 時 Member Center 不使用)
|
||
IssuedAt time.Time // iat
|
||
ExpiresAt time.Time // exp
|
||
Nonce string // nonce
|
||
Raw map[string]any
|
||
}
|