feat: Implement API-based Codex authentication and add team process API, while removing get_code.go from .gitignore.

This commit is contained in:
2026-02-02 10:28:30 +08:00
parent e8323671b1
commit 71eec01739
3 changed files with 35 additions and 52 deletions

2
.gitignore vendored
View File

@@ -102,4 +102,4 @@ backend/codex-pool.exe
backend/codex-pool.exe backend/codex-pool.exe
.claude/settings.local.json .claude/settings.local.json
CodexAuth CodexAuth
get_code.go

View File

@@ -757,8 +757,8 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
var code string var code string
// 根据全局配置决定授权方式 // 根据全局配置决定授权方式
if config.Global.AuthMethod == "api" { if config.Global.AuthMethod == "api" {
// 使用纯 API 模式CodexAuth // 使用纯 API 模式CodexAuth- 使用 S2A 生成的授权 URL
code, err = auth.CompleteWithCodexAPI(memberChild.Email, memberChild.Password, teamID, req.Proxy, authLogger) code, err = auth.CompleteWithCodexAPI(memberChild.Email, memberChild.Password, teamID, s2aResp.Data.AuthURL, s2aResp.Data.SessionID, req.Proxy, authLogger)
} else if req.BrowserType == "rod" { } else if req.BrowserType == "rod" {
code, err = auth.CompleteWithRodLogged(s2aResp.Data.AuthURL, memberChild.Email, memberChild.Password, teamID, req.Headless, req.Proxy, authLogger) code, err = auth.CompleteWithRodLogged(s2aResp.Data.AuthURL, memberChild.Email, memberChild.Password, teamID, req.Headless, req.Proxy, authLogger)
} else { } else {
@@ -857,8 +857,8 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
var code string var code string
// 根据全局配置决定授权方式 // 根据全局配置决定授权方式
if config.Global.AuthMethod == "api" { if config.Global.AuthMethod == "api" {
// 使用纯 API 模式CodexAuth // 使用纯 API 模式CodexAuth- 使用 S2A 生成的授权 URL
code, err = auth.CompleteWithCodexAPI(owner.Email, owner.Password, teamID, req.Proxy, authLogger) code, err = auth.CompleteWithCodexAPI(owner.Email, owner.Password, teamID, s2aResp.Data.AuthURL, s2aResp.Data.SessionID, req.Proxy, authLogger)
} else if req.BrowserType == "rod" { } else if req.BrowserType == "rod" {
code, err = auth.CompleteWithRodLogged(s2aResp.Data.AuthURL, owner.Email, owner.Password, teamID, req.Headless, req.Proxy, authLogger) code, err = auth.CompleteWithRodLogged(s2aResp.Data.AuthURL, owner.Email, owner.Password, teamID, req.Headless, req.Proxy, authLogger)
} else { } else {

View File

@@ -3,7 +3,6 @@ package auth
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/sha256"
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
@@ -21,6 +20,10 @@ import (
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )
func init() {
rand.Seed(time.Now().UnixNano())
}
// 常量 CodexClientID, CodexRedirectURI, CodexScope 已在 s2a.go 中定义 // 常量 CodexClientID, CodexRedirectURI, CodexScope 已在 s2a.go 中定义
// CodexAPIAuth 纯 API 授权 (无浏览器) - 基于 get_code.go 的实现 // CodexAPIAuth 纯 API 授权 (无浏览器) - 基于 get_code.go 的实现
@@ -29,6 +32,8 @@ type CodexAPIAuth struct {
email string email string
password string password string
workspaceID string workspaceID string
authURL string // S2A 生成的授权 URL
sessionID string // S2A 会话 ID
deviceID string deviceID string
sid string sid string
sentinelToken string sentinelToken string
@@ -125,7 +130,8 @@ func (rt *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
} }
// NewCodexAPIAuth 创建 CodexAuth 实例 // 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 var proxyURL *url.URL
if proxy != "" { if proxy != "" {
var err error var err error
@@ -150,6 +156,8 @@ func NewCodexAPIAuth(email, password, workspaceID, proxy string, logger *AuthLog
email: email, email: email,
password: password, password: password,
workspaceID: workspaceID, workspaceID: workspaceID,
authURL: authURL,
sessionID: sessionID,
deviceID: generateUUID(), deviceID: generateUUID(),
sid: 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", 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]) 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 // fnv1a32 FNV-1a 32-bit hash
func fnv1a32(data []byte) uint32 { func fnv1a32(data []byte) uint32 {
h := uint32(2166136261) 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 获取授权码 // ObtainAuthorizationCode 获取授权码
func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) { func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) {
c.logStep(StepNavigate, "开始 Codex API 授权流程...") c.logStep(StepNavigate, "开始 Codex API 授权流程...")
// 1. 生成 PKCE 参数 // 使用 S2A 生成的授权 URL不再自己生成 PKCE 参数
codeVerifier := generateCodeVerifier() if c.authURL == "" {
codeChallenge := generateCodeChallenge(codeVerifier) return "", fmt.Errorf("authURL 未设置,请先通过 S2A 生成授权 URL")
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"},
} }
authURL := "https://auth.openai.com/oauth/authorize?" + params.Encode()
headers := map[string]string{ headers := map[string]string{
"Origin": "https://auth.openai.com", "Origin": "https://auth.openai.com",
"Referer": "https://auth.openai.com/log-in", "Referer": "https://auth.openai.com/log-in",
"Content-Type": "application/json", "Content-Type": "application/json",
} }
// 3. 访问授权页面并手动跟随重定向 // 访问授权页面并手动跟随重定向
c.logStep(StepNavigate, "访问授权页面...") c.logStep(StepNavigate, "访问授权页面...")
resp, _, err := c.doRequest("GET", authURL, nil, headers) resp, _, err := c.doRequest("GET", c.authURL, nil, headers)
if err != nil { if err != nil {
c.logError(StepNavigate, "访问授权页失败: %v", err) c.logError(StepNavigate, "访问授权页失败: %v", err)
return "", fmt.Errorf("访问授权页失败: %v", err) return "", fmt.Errorf("访问授权页失败: %v", err)
} }
// 手动跟随重定向 // 手动跟随重定向
currentURL := authURL currentURL := c.authURL
for resp.StatusCode >= 300 && resp.StatusCode < 400 { for resp.StatusCode >= 300 && resp.StatusCode < 400 {
location := resp.Header.Get("Location") location := resp.Header.Get("Location")
if location == "" { if location == "" {
@@ -438,7 +415,7 @@ func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) {
} }
headers["Referer"] = currentURL headers["Referer"] = currentURL
// 4. 提交邮箱 // 提交邮箱
c.logStep(StepInputEmail, "提交邮箱: %s", c.email) c.logStep(StepInputEmail, "提交邮箱: %s", c.email)
if !c.callSentinelReq("login_web_init") { if !c.callSentinelReq("login_web_init") {
return "", fmt.Errorf("Sentinel 请求失败") 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") { if pageType == "password" || strings.Contains(string(body), "password") {
// 5. 验证密码 // 5. 验证密码
c.logStep(StepInputPassword, "验证密码...") 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))])) c.logError(StepInputPassword, "密码验证失败: %d - %s", resp.StatusCode, string(body[:min(200, len(body))]))
return "", fmt.Errorf("密码验证失败: %d", resp.StatusCode) return "", fmt.Errorf("密码验证失败: %d", resp.StatusCode)
} }
c.logStep(StepInputPassword, "密码验证成功")
} else {
c.logStep(StepInputPassword, "跳过密码验证步骤 (服务器未要求)")
} }
// 6. 选择工作区 // 6. 选择工作区
@@ -591,12 +573,13 @@ func min(a, b int) int {
} }
// CompleteWithCodexAPI 使用纯 API 方式完成授权 // 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 { if logger != nil {
logger.LogStep(StepBrowserStart, "使用 CodexAuth API 模式...") 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 err != nil {
if logger != nil { if logger != nil {
logger.LogError(StepBrowserStart, "创建 CodexAuth 失败: %v", err) logger.LogError(StepBrowserStart, "创建 CodexAuth 失败: %v", err)