-
-
Notifications
You must be signed in to change notification settings - Fork 3k
fix(codex): resolve client_id and account_id handling in token refresh and file listing #2153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -425,31 +425,49 @@ func extractCodexIDTokenClaims(auth *coreauth.Auth) gin.H { | |
| if !strings.EqualFold(strings.TrimSpace(auth.Provider), "codex") { | ||
| return nil | ||
| } | ||
| idTokenRaw, ok := auth.Metadata["id_token"].(string) | ||
| if !ok { | ||
| return nil | ||
| } | ||
| idToken := strings.TrimSpace(idTokenRaw) | ||
| if idToken == "" { | ||
| return nil | ||
| } | ||
| claims, err := codex.ParseJWTToken(idToken) | ||
| if err != nil || claims == nil { | ||
| return nil | ||
| } | ||
|
|
||
| result := gin.H{} | ||
| if v := strings.TrimSpace(claims.CodexAuthInfo.ChatgptAccountID); v != "" { | ||
| result["chatgpt_account_id"] = v | ||
| } | ||
| if v := strings.TrimSpace(claims.CodexAuthInfo.ChatgptPlanType); v != "" { | ||
| result["plan_type"] = v | ||
| } | ||
| if v := claims.CodexAuthInfo.ChatgptSubscriptionActiveStart; v != nil { | ||
| result["chatgpt_subscription_active_start"] = v | ||
|
|
||
| // Check Metadata/Attributes first before parsing id_token. | ||
| // account_id is stored under "account_id" in the JSON file (CodexTokenStorage.AccountID). | ||
| if v, ok := auth.Metadata["account_id"].(string); ok && strings.TrimSpace(v) != "" { | ||
| result["chatgpt_account_id"] = strings.TrimSpace(v) | ||
| } | ||
| if v := claims.CodexAuthInfo.ChatgptSubscriptionActiveUntil; v != nil { | ||
| result["chatgpt_subscription_active_until"] = v | ||
| // plan_type is synthesized into Attributes by the file watcher, not stored in Metadata. | ||
| if auth.Attributes != nil { | ||
| if v := strings.TrimSpace(auth.Attributes["plan_type"]); v != "" { | ||
| result["plan_type"] = v | ||
| } | ||
| } | ||
|
|
||
| // Fall back to parsing id_token for any fields still missing | ||
| needsIDToken := result["chatgpt_account_id"] == nil || result["plan_type"] == nil | ||
| if needsIDToken { | ||
| idTokenRaw, ok := auth.Metadata["id_token"].(string) | ||
| if ok { | ||
| idToken := strings.TrimSpace(idTokenRaw) | ||
| if idToken != "" { | ||
| claims, err := codex.ParseJWTToken(idToken) | ||
| if err == nil && claims != nil { | ||
| if result["chatgpt_account_id"] == nil { | ||
| if v := strings.TrimSpace(claims.CodexAuthInfo.ChatgptAccountID); v != "" { | ||
| result["chatgpt_account_id"] = v | ||
| } | ||
| } | ||
| if result["plan_type"] == nil { | ||
| if v := strings.TrimSpace(claims.CodexAuthInfo.ChatgptPlanType); v != "" { | ||
| result["plan_type"] = v | ||
| } | ||
| } | ||
| if v := claims.CodexAuthInfo.ChatgptSubscriptionActiveStart; v != nil { | ||
| result["chatgpt_subscription_active_start"] = v | ||
| } | ||
| if v := claims.CodexAuthInfo.ChatgptSubscriptionActiveUntil; v != nil { | ||
| result["chatgpt_subscription_active_until"] = v | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
||
|
|
||
| if len(result) == 0 { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,194 @@ | ||||||||||||||||
| package executor | ||||||||||||||||
|
|
||||||||||||||||
| import ( | ||||||||||||||||
| "bufio" | ||||||||||||||||
| "encoding/json" | ||||||||||||||||
| "fmt" | ||||||||||||||||
| "io" | ||||||||||||||||
| "net" | ||||||||||||||||
| "net/http" | ||||||||||||||||
| "net/url" | ||||||||||||||||
| "strings" | ||||||||||||||||
| "testing" | ||||||||||||||||
|
|
||||||||||||||||
| "github.com/google/uuid" | ||||||||||||||||
| tls "github.com/refraction-networking/utls" | ||||||||||||||||
| "golang.org/x/net/http2" | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
| // utlsTransport is a minimal Chrome-fingerprint TLS transport for test use. | ||||||||||||||||
| // Supports HTTP CONNECT proxy tunneling. | ||||||||||||||||
| type utlsTransport struct { | ||||||||||||||||
| proxyURL string | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| func newUtlsTransport(proxyURL string) *utlsTransport { | ||||||||||||||||
| return &utlsTransport{proxyURL: proxyURL} | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| func (t *utlsTransport) dial(addr string) (net.Conn, error) { | ||||||||||||||||
| if t.proxyURL == "" { | ||||||||||||||||
| return net.Dial("tcp", addr) | ||||||||||||||||
| } | ||||||||||||||||
| u, err := url.Parse(t.proxyURL) | ||||||||||||||||
| if err != nil { | ||||||||||||||||
| return nil, fmt.Errorf("parse proxy url: %w", err) | ||||||||||||||||
| } | ||||||||||||||||
| conn, err := net.Dial("tcp", u.Host) | ||||||||||||||||
| if err != nil { | ||||||||||||||||
| return nil, fmt.Errorf("connect to proxy: %w", err) | ||||||||||||||||
| } | ||||||||||||||||
| // HTTP CONNECT tunnel | ||||||||||||||||
| req, _ := http.NewRequest(http.MethodConnect, "http://"+addr, nil) | ||||||||||||||||
| req.Host = addr | ||||||||||||||||
| if err = req.Write(conn); err != nil { | ||||||||||||||||
| conn.Close() | ||||||||||||||||
| return nil, fmt.Errorf("write CONNECT: %w", err) | ||||||||||||||||
| } | ||||||||||||||||
| resp, err := http.ReadResponse(bufio.NewReader(conn), req) | ||||||||||||||||
| if err != nil { | ||||||||||||||||
| conn.Close() | ||||||||||||||||
| return nil, fmt.Errorf("read CONNECT response: %w", err) | ||||||||||||||||
| } | ||||||||||||||||
| resp.Body.Close() | ||||||||||||||||
| if resp.StatusCode != http.StatusOK { | ||||||||||||||||
| conn.Close() | ||||||||||||||||
| return nil, fmt.Errorf("proxy CONNECT failed: %s", resp.Status) | ||||||||||||||||
| } | ||||||||||||||||
| return conn, nil | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| func (t *utlsTransport) RoundTrip(req *http.Request) (*http.Response, error) { | ||||||||||||||||
| host := req.URL.Hostname() | ||||||||||||||||
| addr := host + ":443" | ||||||||||||||||
|
|
||||||||||||||||
| conn, err := t.dial(addr) | ||||||||||||||||
| if err != nil { | ||||||||||||||||
| return nil, fmt.Errorf("dial: %w", err) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| tlsConn := tls.UClient(conn, &tls.Config{ServerName: host}, tls.HelloChrome_Auto) | ||||||||||||||||
| if err = tlsConn.Handshake(); err != nil { | ||||||||||||||||
| conn.Close() | ||||||||||||||||
| return nil, fmt.Errorf("tls handshake: %w", err) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| tr := &http2.Transport{} | ||||||||||||||||
| h2Conn, err := tr.NewClientConn(tlsConn) | ||||||||||||||||
| if err != nil { | ||||||||||||||||
| tlsConn.Close() | ||||||||||||||||
| return nil, fmt.Errorf("h2 conn: %w", err) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| return h2Conn.RoundTrip(req) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // planTypePriority returns a numeric priority for a plan_type string. | ||||||||||||||||
| // Higher value means higher priority: team > plus > free > others. | ||||||||||||||||
| func planTypePriority(planType string) int { | ||||||||||||||||
| switch strings.ToLower(planType) { | ||||||||||||||||
| case "team": | ||||||||||||||||
| return 3 | ||||||||||||||||
| case "plus": | ||||||||||||||||
| return 2 | ||||||||||||||||
| case "free": | ||||||||||||||||
| return 1 | ||||||||||||||||
| default: | ||||||||||||||||
| return 0 | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // pickBestAccountID selects the best account_id from the $.accounts map returned by | ||||||||||||||||
| // the accounts/check API. Priority: team > plus > free > any other. | ||||||||||||||||
| // Returns empty string if no accounts are found. | ||||||||||||||||
| func pickBestAccountID(accounts map[string]any) string { | ||||||||||||||||
| bestID := "" | ||||||||||||||||
| bestPriority := -1 | ||||||||||||||||
| for accountID, v := range accounts { | ||||||||||||||||
| info, ok := v.(map[string]any) | ||||||||||||||||
| if !ok { | ||||||||||||||||
| continue | ||||||||||||||||
| } | ||||||||||||||||
| account, ok := info["account"].(map[string]any) | ||||||||||||||||
| if !ok { | ||||||||||||||||
| continue | ||||||||||||||||
| } | ||||||||||||||||
| planType, _ := account["plan_type"].(string) | ||||||||||||||||
| p := planTypePriority(planType) | ||||||||||||||||
| if p > bestPriority { | ||||||||||||||||
| bestPriority = p | ||||||||||||||||
| bestID = accountID | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| return bestID | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // TestCodexAccountCheck tests GET https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27 | ||||||||||||||||
| // using a real access_token. Set CODEX_ACCESS_TOKEN (and optionally CODEX_PROXY_URL) to run. | ||||||||||||||||
| // | ||||||||||||||||
| // Example: | ||||||||||||||||
| // | ||||||||||||||||
| // CODEX_ACCESS_TOKEN=eyJ... go test ./internal/runtime/executor/... -run TestCodexAccountCheck -v | ||||||||||||||||
| // CODEX_ACCESS_TOKEN=eyJ... CODEX_PROXY_URL=http://127.0.0.1:7890 go test ./internal/runtime/executor/... -run TestCodexAccountCheck -v | ||||||||||||||||
| func TestCodexAccountCheck(t *testing.T) { | ||||||||||||||||
| accessToken := "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE5MzQ0ZTY1LWJiYzktNDRkMS1hOWQwLWY5NTdiMDc5YmQwZSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS92MSJdLCJjbGllbnRfaWQiOiJhcHBfWDh6WTZ2VzJwUTl0UjNkRTduSzFqTDVnSCIsImV4cCI6MTc3NDQ1NTUyNSwiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS9hdXRoIjp7ImNoYXRncHRfYWNjb3VudF9pZCI6Ijg0YzNjZGM2LWFiZWQtNDlhNy1iY2RlLWZmMjQwMjE3NmZkYiIsImNoYXRncHRfYWNjb3VudF91c2VyX2lkIjoidXNlci00MXlCY3M0STZscEJBVEh5R2lTNG9wTUNfXzg0YzNjZGM2LWFiZWQtNDlhNy1iY2RlLWZmMjQwMjE3NmZkYiIsImNoYXRncHRfY29tcHV0ZV9yZXNpZGVuY3kiOiJub19jb25zdHJhaW50IiwiY2hhdGdwdF9wbGFuX3R5cGUiOiJmcmVlIiwiY2hhdGdwdF91c2VyX2lkIjoidXNlci00MXlCY3M0STZscEJBVEh5R2lTNG9wTUMiLCJ1c2VyX2lkIjoidXNlci00MXlCY3M0STZscEJBVEh5R2lTNG9wTUMifSwiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS9wcm9maWxlIjp7ImVtYWlsIjoiZ3VzLmxhcnNvbjJAZDQuemh1ZmFkYS5kZSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlfSwiaWF0IjoxNzczNTkxNTI1LCJpc3MiOiJodHRwczovL2F1dGgub3BlbmFpLmNvbSIsImp0aSI6IjYzZDgwY2M3LWI1NzEtNDk3Yi05ZmI2LTZiZTE2MDE1ZjI4MiIsIm5iZiI6MTc3MzU5MTUyNSwicHdkX2F1dGhfdGltZSI6MTc3MzU5MTUyNDA3NCwic2NwIjpbIm9wZW5pZCIsImVtYWlsIiwicHJvZmlsZSIsIm9mZmxpbmVfYWNjZXNzIiwibW9kZWwucmVxdWVzdCIsIm1vZGVsLnJlYWQiLCJvcmdhbml6YXRpb24ucmVhZCIsIm9yZ2FuaXphdGlvbi53cml0ZSJdLCJzZXNzaW9uX2lkIjoiYXV0aHNlc3NfWTFwQWh2VWRXQVVNb1pPVzN0OEllNjFKIiwic2wiOnRydWUsInN1YiI6ImF1dGgwfHhJcnpFSTlvV0xBUUtMMW4wR2tKenBETiJ9.f8tBrHuYqZtVpSY3cxf0NOrEgGHVZlhSQhz4_aMngNIq8_O1oY6ajyWoJpdqtf_m-luzRswZMgA-fKGiEbKu-LqqFiCHnNOFkK5ymdAoXFLsHWEX-BFS5wqTKJ6_nphrqLgVMAaA1mwuWQZ3PD2mCMJ_eErFhFPGlOCBR1TyDSMhJhvDMHB81sqJbxJBpkQNV3J1GDcvvUaNiQebAs4LNOhNaQfYTxJQqJZiGCnwjeHWql_aSSKv4y1vEXSLwH-GEqfjlpHYZqhYTHpdr_PzzOOIWq_X9ScedMOy699UYwyQa7IKcwCw6ZqaVbR_WjAdHunWi8yOl5C7JFuUA2xhrfMOQUgg86vv5oBw_OYTHzX51Dimh_SHhLaUCNC0-SPRZ-IiYz91MveiR_QCSHvU_ZXJO-FY8Xqa6NEdLZ8AbrOb81dxnV8DOABxolHVMtuxINPQuzHAEKxAyNGQkwgo7_O4TAVUycpv4b3LMIoSdCHy7F7q9Dh1UR8jTugM0Zqor6bD0XEwdFL5KxONZk-alHAo93IrDS9D8L0bjp0cMl7A1ZyjYjOMpw8Liq_b6V6uyPPJsw0DY1q-LtrQXN05W1La5bOX-J0yjtEnRrKtm5mGZ8cT2RnKYJJFFSckbMY2EvKuSEMKh0T5YoqPx-LZsXRViscbGfLBh8e2gzvXlq8" | ||||||||||||||||
|
||||||||||||||||
| proxyURL := "http://127.0.0.1:7890" | ||||||||||||||||
|
||||||||||||||||
| accessToken := "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE5MzQ0ZTY1LWJiYzktNDRkMS1hOWQwLWY5NTdiMDc5YmQwZSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS92MSJdLCJjbGllbnRfaWQiOiJhcHBfWDh6WTZ2VzJwUTl0UjNkRTduSzFqTDVnSCIsImV4cCI6MTc3NDQ1NTUyNSwiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS9hdXRoIjp7ImNoYXRncHRfYWNjb3VudF9pZCI6Ijg0YzNjZGM2LWFiZWQtNDlhNy1iY2RlLWZmMjQwMjE3NmZkYiIsImNoYXRncHRfYWNjb3VudF91c2VyX2lkIjoidXNlci00MXlCY3M0STZscEJBVEh5R2lTNG9wTUNfXzg0YzNjZGM2LWFiZWQtNDlhNy1iY2RlLWZmMjQwMjE3NmZkYiIsImNoYXRncHRfY29tcHV0ZV9yZXNpZGVuY3kiOiJub19jb25zdHJhaW50IiwiY2hhdGdwdF9wbGFuX3R5cGUiOiJmcmVlIiwiY2hhdGdwdF91c2VyX2lkIjoidXNlci00MXlCY3M0STZscEJBVEh5R2lTNG9wTUMiLCJ1c2VyX2lkIjoidXNlci00MXlCY3M0STZscEJBVEh5R2lTNG9wTUMifSwiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS9wcm9maWxlIjp7ImVtYWlsIjoiZ3VzLmxhcnNvbjJAZDQuemh1ZmFkYS5kZSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlfSwiaWF0IjoxNzczNTkxNTI1LCJpc3MiOiJodHRwczovL2F1dGgub3BlbmFpLmNvbSIsImp0aSI6IjYzZDgwY2M3LWI1NzEtNDk3Yi05ZmI2LTZiZTE2MDE1ZjI4MiIsIm5iZiI6MTc3MzU5MTUyNSwicHdkX2F1dGhfdGltZSI6MTc3MzU5MTUyNDA3NCwic2NwIjpbIm9wZW5pZCIsImVtYWlsIiwicHJvZmlsZSIsIm9mZmxpbmVfYWNjZXNzIiwibW9kZWwucmVxdWVzdCIsIm1vZGVsLnJlYWQiLCJvcmdhbml6YXRpb24ucmVhZCIsIm9yZ2FuaXphdGlvbi53cml0ZSJdLCJzZXNzaW9uX2lkIjoiYXV0aHNlc3NfWTFwQWh2VWRXQVVNb1pPVzN0OEllNjFKIiwic2wiOnRydWUsInN1YiI6ImF1dGgwfHhJcnpFSTlvV0xBUUtMMW4wR2tKenBETiJ9.f8tBrHuYqZtVpSY3cxf0NOrEgGHVZlhSQhz4_aMngNIq8_O1oY6ajyWoJpdqtf_m-luzRswZMgA-fKGiEbKu-LqqFiCHnNOFkK5ymdAoXFLsHWEX-BFS5wqTKJ6_nphrqLgVMAaA1mwuWQZ3PD2mCMJ_eErFhFPGlOCBR1TyDSMhJhvDMHB81sqJbxJBpkQNV3J1GDcvvUaNiQebAs4LNOhNaQfYTxJQqJZiGCnwjeHWql_aSSKv4y1vEXSLwH-GEqfjlpHYZqhYTHpdr_PzzOOIWq_X9ScedMOy699UYwyQa7IKcwCw6ZqaVbR_WjAdHunWi8yOl5C7JFuUA2xhrfMOQUgg86vv5oBw_OYTHzX51Dimh_SHhLaUCNC0-SPRZ-IiYz91MveiR_QCSHvU_ZXJO-FY8Xqa6NEdLZ8AbrOb81dxnV8DOABxolHVMtuxINPQuzHAEKxAyNGQkwgo7_O4TAVUycpv4b3LMIoSdCHy7F7q9Dh1UR8jTugM0Zqor6bD0XEwdFL5KxONZk-alHAo93IrDS9D8L0bjp0cMl7A1ZyjYjOMpw8Liq_b6V6uyPPJsw0DY1q-LtrQXN05W1La5bOX-J0yjtEnRrKtm5mGZ8cT2RnKYJJFFSckbMY2EvKuSEMKh0T5YoqPx-LZsXRViscbGfLBh8e2gzvXlq8" | |
| proxyURL := "http://127.0.0.1:7890" | |
| accessToken := os.Getenv("CODEX_ACCESS_TOKEN") | |
| if accessToken == "" { | |
| t.Skip("skipping test: environment variable CODEX_ACCESS_TOKEN is not set") | |
| } | |
| proxyURL := os.Getenv("CODEX_PROXY_URL") |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gate live account-check test behind explicit opt-in
TestCodexAccountCheck runs as a normal unit test but performs a real network call through a fixed local proxy (127.0.0.1:7890) and immediately Fatalfs on connection errors, so go test fails in any environment without that proxy (including typical CI). The comment says env vars should control execution, but the implementation does not check them or skip, making the test suite non-hermetic and brittle.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new
needsIDTokengate only parsesid_tokenwhenchatgpt_account_idorplan_typeis missing, butchatgpt_subscription_active_startandchatgpt_subscription_active_untilare also populated only inside that block. In cases where metadata already has account ID and attributes already has plan type, subscription dates are silently dropped even whenid_tokenis present, which regresses/auth-filesresponse completeness.Useful? React with 👍 / 👎.