feat: Implement API-based Codex authentication and add team process API, while removing get_code.go from .gitignore.
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user