From 7304ab7d3357d861894078ba012231eccc952624 Mon Sep 17 00:00:00 2001 From: qiaoborui Date: Sat, 14 Feb 2026 12:37:49 +0800 Subject: [PATCH 1/2] fix(auth): align OpenAI OAuth authorize URL and params --- pkg/auth/oauth.go | 39 +++++++++++++++++++++++---------------- pkg/auth/oauth_test.go | 20 +++++++++++++++----- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/pkg/auth/oauth.go b/pkg/auth/oauth.go index ecd9ba2..4f26e0e 100644 --- a/pkg/auth/oauth.go +++ b/pkg/auth/oauth.go @@ -19,18 +19,20 @@ import ( ) type OAuthProviderConfig struct { - Issuer string - ClientID string - Scopes string - Port int + Issuer string + ClientID string + Scopes string + Originator string + Port int } func OpenAIOAuthConfig() OAuthProviderConfig { return OAuthProviderConfig{ - Issuer: "https://auth.openai.com", - ClientID: "app_EMoamEEZ73f0CkXaXp7hrann", - Scopes: "openid profile email offline_access", - Port: 1455, + Issuer: "https://auth.openai.com", + ClientID: "app_EMoamEEZ73f0CkXaXp7hrann", + Scopes: "openid profile email offline_access", + Originator: "codex_cli_rs", + Port: 1455, } } @@ -288,15 +290,20 @@ func BuildAuthorizeURL(cfg OAuthProviderConfig, pkce PKCECodes, state, redirectU func buildAuthorizeURL(cfg OAuthProviderConfig, pkce PKCECodes, state, redirectURI string) string { params := url.Values{ - "response_type": {"code"}, - "client_id": {cfg.ClientID}, - "redirect_uri": {redirectURI}, - "scope": {cfg.Scopes}, - "code_challenge": {pkce.CodeChallenge}, - "code_challenge_method": {"S256"}, - "state": {state}, + "response_type": {"code"}, + "client_id": {cfg.ClientID}, + "redirect_uri": {redirectURI}, + "scope": {cfg.Scopes}, + "code_challenge": {pkce.CodeChallenge}, + "code_challenge_method": {"S256"}, + "id_token_add_organizations": {"true"}, + "codex_cli_simplified_flow": {"true"}, + "state": {state}, } - return cfg.Issuer + "/authorize?" + params.Encode() + if cfg.Originator != "" { + params.Set("originator", cfg.Originator) + } + return cfg.Issuer + "/oauth/authorize?" + params.Encode() } func exchangeCodeForTokens(cfg OAuthProviderConfig, code, codeVerifier, redirectURI string) (*AuthCredential, error) { diff --git a/pkg/auth/oauth_test.go b/pkg/auth/oauth_test.go index 9f80132..2348ee2 100644 --- a/pkg/auth/oauth_test.go +++ b/pkg/auth/oauth_test.go @@ -10,10 +10,11 @@ import ( func TestBuildAuthorizeURL(t *testing.T) { cfg := OAuthProviderConfig{ - Issuer: "https://auth.example.com", - ClientID: "test-client-id", - Scopes: "openid profile", - Port: 1455, + Issuer: "https://auth.example.com", + ClientID: "test-client-id", + Scopes: "openid profile", + Originator: "codex_cli_rs", + Port: 1455, } pkce := PKCECodes{ CodeVerifier: "test-verifier", @@ -22,7 +23,7 @@ func TestBuildAuthorizeURL(t *testing.T) { u := BuildAuthorizeURL(cfg, pkce, "test-state", "http://localhost:1455/auth/callback") - if !strings.HasPrefix(u, "https://auth.example.com/authorize?") { + if !strings.HasPrefix(u, "https://auth.example.com/oauth/authorize?") { t.Errorf("URL does not start with expected prefix: %s", u) } if !strings.Contains(u, "client_id=test-client-id") { @@ -40,6 +41,15 @@ func TestBuildAuthorizeURL(t *testing.T) { if !strings.Contains(u, "response_type=code") { t.Error("URL missing response_type") } + if !strings.Contains(u, "id_token_add_organizations=true") { + t.Error("URL missing id_token_add_organizations") + } + if !strings.Contains(u, "codex_cli_simplified_flow=true") { + t.Error("URL missing codex_cli_simplified_flow") + } + if !strings.Contains(u, "originator=codex_cli_rs") { + t.Error("URL missing originator") + } } func TestParseTokenResponse(t *testing.T) { From da804a074858b07633c585f91c4a765601f142d7 Mon Sep 17 00:00:00 2001 From: qiaoborui Date: Sat, 14 Feb 2026 12:48:16 +0800 Subject: [PATCH 2/2] fix(codex): include required instructions and improve account-id extraction --- pkg/auth/oauth.go | 3 +++ pkg/auth/oauth_test.go | 27 +++++++++++++++++++++++++++ pkg/providers/codex_provider.go | 5 +++++ pkg/providers/codex_provider_test.go | 6 ++++++ 4 files changed, 41 insertions(+) diff --git a/pkg/auth/oauth.go b/pkg/auth/oauth.go index 4f26e0e..1a65896 100644 --- a/pkg/auth/oauth.go +++ b/pkg/auth/oauth.go @@ -359,6 +359,9 @@ func parseTokenResponse(body []byte, provider string) (*AuthCredential, error) { if accountID := extractAccountID(tokenResp.AccessToken); accountID != "" { cred.AccountID = accountID + } else if accountID := extractAccountID(tokenResp.IDToken); accountID != "" { + // Recent OpenAI OAuth responses may only include chatgpt_account_id in id_token claims. + cred.AccountID = accountID } return cred, nil diff --git a/pkg/auth/oauth_test.go b/pkg/auth/oauth_test.go index 2348ee2..0d2ccc9 100644 --- a/pkg/auth/oauth_test.go +++ b/pkg/auth/oauth_test.go @@ -1,6 +1,7 @@ package auth import ( + "encoding/base64" "encoding/json" "net/http" "net/http/httptest" @@ -91,6 +92,32 @@ func TestParseTokenResponseNoAccessToken(t *testing.T) { } } +func TestParseTokenResponseAccountIDFromIDToken(t *testing.T) { + idToken := makeJWTWithAccountID("acc-from-id") + resp := map[string]interface{}{ + "access_token": "not-a-jwt", + "refresh_token": "test-refresh-token", + "expires_in": 3600, + "id_token": idToken, + } + body, _ := json.Marshal(resp) + + cred, err := parseTokenResponse(body, "openai") + if err != nil { + t.Fatalf("parseTokenResponse() error: %v", err) + } + + if cred.AccountID != "acc-from-id" { + t.Errorf("AccountID = %q, want %q", cred.AccountID, "acc-from-id") + } +} + +func makeJWTWithAccountID(accountID string) string { + header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none","typ":"JWT"}`)) + payload := base64.RawURLEncoding.EncodeToString([]byte(`{"https://api.openai.com/auth":{"chatgpt_account_id":"` + accountID + `"}}`)) + return header + "." + payload + ".sig" +} + func TestExchangeCodeForTokens(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/oauth/token" { diff --git a/pkg/providers/codex_provider.go b/pkg/providers/codex_provider.go index 3463389..c0b10bd 100644 --- a/pkg/providers/codex_provider.go +++ b/pkg/providers/codex_provider.go @@ -18,6 +18,8 @@ type CodexProvider struct { tokenSource func() (string, string, error) } +const defaultCodexInstructions = "You are Codex, a coding assistant." + func NewCodexProvider(token, accountID string) *CodexProvider { opts := []option.RequestOption{ option.WithBaseURL("https://chatgpt.com/backend-api/codex"), @@ -138,6 +140,9 @@ func buildCodexParams(messages []Message, tools []ToolDefinition, model string, if instructions != "" { params.Instructions = openai.Opt(instructions) + } else { + // ChatGPT Codex backend requires instructions to be present. + params.Instructions = openai.Opt(defaultCodexInstructions) } if maxTokens, ok := options["max_tokens"].(int); ok { diff --git a/pkg/providers/codex_provider_test.go b/pkg/providers/codex_provider_test.go index 605183d..1a5a8ca 100644 --- a/pkg/providers/codex_provider_test.go +++ b/pkg/providers/codex_provider_test.go @@ -21,6 +21,12 @@ func TestBuildCodexParams_BasicMessage(t *testing.T) { if params.Model != "gpt-4o" { t.Errorf("Model = %q, want %q", params.Model, "gpt-4o") } + if !params.Instructions.Valid() { + t.Fatal("Instructions should be set") + } + if params.Instructions.Or("") != defaultCodexInstructions { + t.Errorf("Instructions = %q, want %q", params.Instructions.Or(""), defaultCodexInstructions) + } } func TestBuildCodexParams_SystemAsInstructions(t *testing.T) {