diff --git a/.gitignore b/.gitignore index 0074cfc..aaa6b27 100644 --- a/.gitignore +++ b/.gitignore @@ -102,4 +102,4 @@ backend/codex-pool.exe backend/codex-pool.exe .claude/settings.local.json CodexAuth -get_code.go + diff --git a/backend/internal/api/team_process.go b/backend/internal/api/team_process.go index a45b937..378c1ab 100644 --- a/backend/internal/api/team_process.go +++ b/backend/internal/api/team_process.go @@ -757,8 +757,8 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul var code string // 根据全局配置决定授权方式 if config.Global.AuthMethod == "api" { - // 使用纯 API 模式(CodexAuth) - code, err = auth.CompleteWithCodexAPI(memberChild.Email, memberChild.Password, teamID, req.Proxy, authLogger) + // 使用纯 API 模式(CodexAuth)- 使用 S2A 生成的授权 URL + code, err = auth.CompleteWithCodexAPI(memberChild.Email, memberChild.Password, teamID, s2aResp.Data.AuthURL, s2aResp.Data.SessionID, req.Proxy, authLogger) } else if req.BrowserType == "rod" { code, err = auth.CompleteWithRodLogged(s2aResp.Data.AuthURL, memberChild.Email, memberChild.Password, teamID, req.Headless, req.Proxy, authLogger) } else { @@ -857,8 +857,8 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul var code string // 根据全局配置决定授权方式 if config.Global.AuthMethod == "api" { - // 使用纯 API 模式(CodexAuth) - code, err = auth.CompleteWithCodexAPI(owner.Email, owner.Password, teamID, req.Proxy, authLogger) + // 使用纯 API 模式(CodexAuth)- 使用 S2A 生成的授权 URL + code, err = auth.CompleteWithCodexAPI(owner.Email, owner.Password, teamID, s2aResp.Data.AuthURL, s2aResp.Data.SessionID, req.Proxy, authLogger) } else if req.BrowserType == "rod" { code, err = auth.CompleteWithRodLogged(s2aResp.Data.AuthURL, owner.Email, owner.Password, teamID, req.Headless, req.Proxy, authLogger) } else { diff --git a/backend/internal/auth/codex_api.go b/backend/internal/auth/codex_api.go index c98ae7a..1e3746e 100644 --- a/backend/internal/auth/codex_api.go +++ b/backend/internal/auth/codex_api.go @@ -3,7 +3,6 @@ package auth import ( "bytes" "context" - "crypto/sha256" "crypto/tls" "encoding/base64" "encoding/json" @@ -21,6 +20,10 @@ import ( "golang.org/x/net/http2" ) +func init() { + rand.Seed(time.Now().UnixNano()) +} + // 常量 CodexClientID, CodexRedirectURI, CodexScope 已在 s2a.go 中定义 // CodexAPIAuth 纯 API 授权 (无浏览器) - 基于 get_code.go 的实现 @@ -29,6 +32,8 @@ type CodexAPIAuth struct { email string password string workspaceID string + authURL string // S2A 生成的授权 URL + sessionID string // S2A 会话 ID deviceID string sid string sentinelToken string @@ -125,7 +130,8 @@ func (rt *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) } // NewCodexAPIAuth 创建 CodexAuth 实例 -func NewCodexAPIAuth(email, password, workspaceID, proxy string, logger *AuthLogger) (*CodexAPIAuth, error) { +// authURL 和 sessionID 由 S2A 生成 +func NewCodexAPIAuth(email, password, workspaceID, authURL, sessionID, proxy string, logger *AuthLogger) (*CodexAPIAuth, error) { var proxyURL *url.URL if proxy != "" { var err error @@ -150,6 +156,8 @@ func NewCodexAPIAuth(email, password, workspaceID, proxy string, logger *AuthLog email: email, password: password, workspaceID: workspaceID, + authURL: authURL, + sessionID: sessionID, deviceID: generateUUID(), sid: generateUUID(), userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36", @@ -166,26 +174,6 @@ func generateUUID() string { b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } -// generateCodeVerifier 生成 PKCE code_verifier -func generateCodeVerifier() string { - b := make([]byte, 64) - rand.Read(b) - return base64.RawURLEncoding.EncodeToString(b) -} - -// generateCodeChallenge 生成 PKCE code_challenge (S256) -func generateCodeChallenge(verifier string) string { - hash := sha256.Sum256([]byte(verifier)) - return base64.RawURLEncoding.EncodeToString(hash[:]) -} - -// generateState 生成 state -func generateState() string { - b := make([]byte, 16) - rand.Read(b) - return base64.RawURLEncoding.EncodeToString(b) -} - // fnv1a32 FNV-1a 32-bit hash func fnv1a32(data []byte) uint32 { h := uint32(2166136261) @@ -380,47 +368,36 @@ func (c *CodexAPIAuth) logError(step AuthStep, format string, args ...interface{ } } +// GetSessionID 获取 S2A 会话 ID(用于后续入库) +func (c *CodexAPIAuth) GetSessionID() string { + return c.sessionID +} + // ObtainAuthorizationCode 获取授权码 func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) { c.logStep(StepNavigate, "开始 Codex API 授权流程...") - // 1. 生成 PKCE 参数 - codeVerifier := generateCodeVerifier() - codeChallenge := generateCodeChallenge(codeVerifier) - state := generateState() - - // 2. 构建授权 URL - params := url.Values{ - "client_id": {CodexClientID}, - "scope": {CodexScope}, - "response_type": {"code"}, - "redirect_uri": {CodexRedirectURI}, - "code_challenge": {codeChallenge}, - "code_challenge_method": {"S256"}, - "state": {state}, - "id_token_add_organizations": {"true"}, - "codex_cli_simplified_flow": {"true"}, - "originator": {"codex_cli_rs"}, + // 使用 S2A 生成的授权 URL(不再自己生成 PKCE 参数) + if c.authURL == "" { + return "", fmt.Errorf("authURL 未设置,请先通过 S2A 生成授权 URL") } - authURL := "https://auth.openai.com/oauth/authorize?" + params.Encode() - headers := map[string]string{ "Origin": "https://auth.openai.com", "Referer": "https://auth.openai.com/log-in", "Content-Type": "application/json", } - // 3. 访问授权页面并手动跟随重定向 + // 访问授权页面并手动跟随重定向 c.logStep(StepNavigate, "访问授权页面...") - resp, _, err := c.doRequest("GET", authURL, nil, headers) + resp, _, err := c.doRequest("GET", c.authURL, nil, headers) if err != nil { c.logError(StepNavigate, "访问授权页失败: %v", err) return "", fmt.Errorf("访问授权页失败: %v", err) } // 手动跟随重定向 - currentURL := authURL + currentURL := c.authURL for resp.StatusCode >= 300 && resp.StatusCode < 400 { location := resp.Header.Get("Location") if location == "" { @@ -438,7 +415,7 @@ func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) { } headers["Referer"] = currentURL - // 4. 提交邮箱 + // 提交邮箱 c.logStep(StepInputEmail, "提交邮箱: %s", c.email) if !c.callSentinelReq("login_web_init") { return "", fmt.Errorf("Sentinel 请求失败") @@ -474,6 +451,8 @@ func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) { } } + c.logStep(StepInputPassword, "邮箱提交响应 pageType=%s, 包含password=%v", pageType, strings.Contains(string(body), "password")) + if pageType == "password" || strings.Contains(string(body), "password") { // 5. 验证密码 c.logStep(StepInputPassword, "验证密码...") @@ -497,6 +476,9 @@ func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) { c.logError(StepInputPassword, "密码验证失败: %d - %s", resp.StatusCode, string(body[:min(200, len(body))])) return "", fmt.Errorf("密码验证失败: %d", resp.StatusCode) } + c.logStep(StepInputPassword, "密码验证成功") + } else { + c.logStep(StepInputPassword, "跳过密码验证步骤 (服务器未要求)") } // 6. 选择工作区 @@ -591,12 +573,13 @@ func min(a, b int) int { } // CompleteWithCodexAPI 使用纯 API 方式完成授权 -func CompleteWithCodexAPI(email, password, workspaceID, proxy string, logger *AuthLogger) (string, error) { +// authURL 和 sessionID 由 S2A 生成 +func CompleteWithCodexAPI(email, password, workspaceID, authURL, sessionID, proxy string, logger *AuthLogger) (string, error) { if logger != nil { logger.LogStep(StepBrowserStart, "使用 CodexAuth API 模式...") } - auth, err := NewCodexAPIAuth(email, password, workspaceID, proxy, logger) + auth, err := NewCodexAPIAuth(email, password, workspaceID, authURL, sessionID, proxy, logger) if err != nil { if logger != nil { logger.LogError(StepBrowserStart, "创建 CodexAuth 失败: %v", err)