接 DB 後真人 OIDC 登入暴露 MC OIDC provider 實作不完整,visionA 端逐項繞過, 讓登入/換帳號可用;另補真轉檔服務的整合 e2e。 OIDC 登入修復(MC 端根因另有交接檔,visionA 先繞過): - email fallback:MC id_token 不發 email claim(ASP.NET Identity 預設 factory 只發 sub/name)→ A7 email 必填擋住登入。callback email 空時用 <sub>@noemail.visiona.local placeholder,不污染 schema,MC 修好發真 email 後 ON CONFLICT 自動覆寫 - prompt=login:authorize 帶 prompt=login(config VISIONA_OIDC_PROMPT_LOGIN,預設關) - logout 連動 MC:logout 回 idp_logout(MC Web :7880 /account/logout,GET),前端用 隱藏 iframe 觸發清 MC session(Web/Api 共享 DataProtection)→ 能換帳號。 config VISIONA_OIDC_LOGOUT_URL、向下相容(未設則只清本地) 真轉檔鏈路 e2e(//go:build realconv,按需對 stage 跑、不污染主測試集): - real_converter_e2e:give 真轉檔服務 contract(init→poll→completed/promote/result) - real_chain_e2e:真轉檔→PromoteToModels→model 進 PG→冪等 全鏈路(對 stage 跑 PASS) 交接檔(給對應團隊根治): - mc-email-claim-handoff:MC 加 email claim(自訂 UserClaimsPrincipalFactory) - converter-promote-oauth-handoff:轉檔服務 OAuth 用 form body 非 Basic Auth 全程 Reviewer 審查 + 對 stage 真環境驗證。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
123 lines
5.6 KiB
Go
123 lines
5.6 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
|
||
|
||
// PromptLogin 控制 authorize request 是否帶 OIDC `prompt=login` 參數。
|
||
//
|
||
// - true → 每次登入都強制 IdP 重新認證(忽略既有 SSO session,要求重輸帳密)。
|
||
// - false → 不帶 prompt,沿用 IdP 既有 session(標準 SSO 體驗),為預設值。
|
||
//
|
||
// `prompt=login` 是 OIDC Core §3.1.2.1 標準參數,由 caller(OIDC discovery 不一定
|
||
// 在 prompt_values_supported 列出,但 Core 標準參數 IdP 多半支援)。設成 config 可控
|
||
// 是為了讓「強制重新認證」在不同部署間可開可關 — 例如 stage 要每次都問、prod 視 UX 決定。
|
||
// 對齊 VISIONA_OIDC_PROMPT_LOGIN。
|
||
PromptLogin bool
|
||
}
|
||
|
||
// 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
|
||
}
|