jim800121chen c2f0b1549e feat: OIDC 登入修復(email fallback / prompt=login / logout 連動)+ 真轉檔鏈路 e2e
接 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>
2026-06-27 03:18:59 +08:00

123 lines
5.6 KiB
Go
Raw Permalink 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
// PromptLogin 控制 authorize request 是否帶 OIDC `prompt=login` 參數。
//
// - true → 每次登入都強制 IdP 重新認證(忽略既有 SSO session要求重輸帳密
// - false → 不帶 prompt沿用 IdP 既有 session標準 SSO 體驗),為預設值。
//
// `prompt=login` 是 OIDC Core §3.1.2.1 標準參數,由 callerOIDC 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 為基礎)可未來替換
// 而不影響 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
}