mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-13 11:24:46 +08:00
feat: 导入账号时 best-effort 从 id_token 提取用户信息
提取 DecodeIDToken(跳过过期校验)供导入场景使用, ParseIDToken 复用它并保留原有过期检查行为。 导入 OpenAI/Sora OAuth 账号时自动补充缺失的 email、 plan_type、chatgpt_account_id 等字段,不覆盖已有值。
This commit is contained in:
@@ -8,6 +8,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -292,6 +295,8 @@ func (h *AccountHandler) importData(ctx context.Context, req DataImportRequest)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enrichCredentialsFromIDToken(&item)
|
||||||
|
|
||||||
accountInput := &service.CreateAccountInput{
|
accountInput := &service.CreateAccountInput{
|
||||||
Name: item.Name,
|
Name: item.Name,
|
||||||
Notes: item.Notes,
|
Notes: item.Notes,
|
||||||
@@ -535,6 +540,57 @@ func defaultProxyName(name string) string {
|
|||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// enrichCredentialsFromIDToken performs best-effort extraction of user info fields
|
||||||
|
// (email, plan_type, chatgpt_account_id, etc.) from id_token in credentials.
|
||||||
|
// Only applies to OpenAI/Sora OAuth accounts. Skips expired token errors silently.
|
||||||
|
// Existing credential values are never overwritten — only missing fields are filled.
|
||||||
|
func enrichCredentialsFromIDToken(item *DataAccount) {
|
||||||
|
if item.Credentials == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Only enrich OpenAI/Sora OAuth accounts
|
||||||
|
platform := strings.ToLower(strings.TrimSpace(item.Platform))
|
||||||
|
if platform != service.PlatformOpenAI && platform != service.PlatformSora {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.ToLower(strings.TrimSpace(item.Type)) != service.AccountTypeOAuth {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
idToken, _ := item.Credentials["id_token"].(string)
|
||||||
|
if strings.TrimSpace(idToken) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeIDToken skips expiry validation — safe for imported data
|
||||||
|
claims, err := openai.DecodeIDToken(idToken)
|
||||||
|
if err != nil {
|
||||||
|
slog.Debug("import_enrich_id_token_decode_failed", "account", item.Name, "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfo := claims.GetUserInfo()
|
||||||
|
if userInfo == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill missing fields only (never overwrite existing values)
|
||||||
|
setIfMissing := func(key, value string) {
|
||||||
|
if value == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if existing, _ := item.Credentials[key].(string); existing == "" {
|
||||||
|
item.Credentials[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIfMissing("email", userInfo.Email)
|
||||||
|
setIfMissing("plan_type", userInfo.PlanType)
|
||||||
|
setIfMissing("chatgpt_account_id", userInfo.ChatGPTAccountID)
|
||||||
|
setIfMissing("chatgpt_user_id", userInfo.ChatGPTUserID)
|
||||||
|
setIfMissing("organization_id", userInfo.OrganizationID)
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeProxyStatus(status string) string {
|
func normalizeProxyStatus(status string) string {
|
||||||
normalized := strings.TrimSpace(strings.ToLower(status))
|
normalized := strings.TrimSpace(strings.ToLower(status))
|
||||||
switch normalized {
|
switch normalized {
|
||||||
|
|||||||
@@ -326,12 +326,9 @@ func (r *RefreshTokenRequest) ToFormData() string {
|
|||||||
return params.Encode()
|
return params.Encode()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseIDToken parses the ID Token JWT and extracts claims.
|
// DecodeIDToken decodes the ID Token JWT payload without validating expiration.
|
||||||
// 注意:当前仅解码 payload 并校验 exp,未验证 JWT 签名。
|
// Use this for best-effort extraction (e.g., during data import) where the token may be expired.
|
||||||
// 生产环境如需用 ID Token 做授权决策,应通过 OpenAI 的 JWKS 端点验证签名:
|
func DecodeIDToken(idToken string) (*IDTokenClaims, error) {
|
||||||
//
|
|
||||||
// https://auth.openai.com/.well-known/jwks.json
|
|
||||||
func ParseIDToken(idToken string) (*IDTokenClaims, error) {
|
|
||||||
parts := strings.Split(idToken, ".")
|
parts := strings.Split(idToken, ".")
|
||||||
if len(parts) != 3 {
|
if len(parts) != 3 {
|
||||||
return nil, fmt.Errorf("invalid JWT format: expected 3 parts, got %d", len(parts))
|
return nil, fmt.Errorf("invalid JWT format: expected 3 parts, got %d", len(parts))
|
||||||
@@ -361,6 +358,20 @@ func ParseIDToken(idToken string) (*IDTokenClaims, error) {
|
|||||||
return nil, fmt.Errorf("failed to parse JWT claims: %w", err)
|
return nil, fmt.Errorf("failed to parse JWT claims: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return &claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseIDToken parses the ID Token JWT and extracts claims.
|
||||||
|
// 注意:当前仅解码 payload 并校验 exp,未验证 JWT 签名。
|
||||||
|
// 生产环境如需用 ID Token 做授权决策,应通过 OpenAI 的 JWKS 端点验证签名:
|
||||||
|
//
|
||||||
|
// https://auth.openai.com/.well-known/jwks.json
|
||||||
|
func ParseIDToken(idToken string) (*IDTokenClaims, error) {
|
||||||
|
claims, err := DecodeIDToken(idToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// 校验 ID Token 是否已过期(允许 2 分钟时钟偏差,防止因服务器时钟略有差异误判刚颁发的令牌)
|
// 校验 ID Token 是否已过期(允许 2 分钟时钟偏差,防止因服务器时钟略有差异误判刚颁发的令牌)
|
||||||
const clockSkewTolerance = 120 // 秒
|
const clockSkewTolerance = 120 // 秒
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
@@ -368,7 +379,7 @@ func ParseIDToken(idToken string) (*IDTokenClaims, error) {
|
|||||||
return nil, fmt.Errorf("id_token has expired (exp: %d, now: %d, skew_tolerance: %ds)", claims.Exp, now, clockSkewTolerance)
|
return nil, fmt.Errorf("id_token has expired (exp: %d, now: %d, skew_tolerance: %ds)", claims.Exp, now, clockSkewTolerance)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &claims, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserInfo represents user information extracted from ID Token claims.
|
// UserInfo represents user information extracted from ID Token claims.
|
||||||
|
|||||||
Reference in New Issue
Block a user