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 }