Skip to content

Commit bd2ff61

Browse files
authored
Merge pull request router-for-me#1683 from dusty-du/codex/device-login-flow
Add additive Codex device-code login flow
2 parents 8a08985 + 9433ab7 commit bd2ff61

File tree

5 files changed

+372
-38
lines changed

5 files changed

+372
-38
lines changed

cmd/server/main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ func main() {
5858
// Command-line flags to control the application's behavior.
5959
var login bool
6060
var codexLogin bool
61+
var codexDeviceLogin bool
6162
var claudeLogin bool
6263
var qwenLogin bool
6364
var iflowLogin bool
@@ -76,6 +77,7 @@ func main() {
7677
// Define command-line flags for different operation modes.
7778
flag.BoolVar(&login, "login", false, "Login Google Account")
7879
flag.BoolVar(&codexLogin, "codex-login", false, "Login to Codex using OAuth")
80+
flag.BoolVar(&codexDeviceLogin, "codex-device-login", false, "Login to Codex using device code flow")
7981
flag.BoolVar(&claudeLogin, "claude-login", false, "Login to Claude using OAuth")
8082
flag.BoolVar(&qwenLogin, "qwen-login", false, "Login to Qwen using OAuth")
8183
flag.BoolVar(&iflowLogin, "iflow-login", false, "Login to iFlow using OAuth")
@@ -467,6 +469,9 @@ func main() {
467469
} else if codexLogin {
468470
// Handle Codex login
469471
cmd.DoCodexLogin(cfg, options)
472+
} else if codexDeviceLogin {
473+
// Handle Codex device-code login
474+
cmd.DoCodexDeviceLogin(cfg, options)
470475
} else if claudeLogin {
471476
// Handle Claude login
472477
cmd.DoClaudeLogin(cfg, options)

internal/auth/codex/openai_auth.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,16 +71,26 @@ func (o *CodexAuth) GenerateAuthURL(state string, pkceCodes *PKCECodes) (string,
7171
// It performs an HTTP POST request to the OpenAI token endpoint with the provided
7272
// authorization code and PKCE verifier.
7373
func (o *CodexAuth) ExchangeCodeForTokens(ctx context.Context, code string, pkceCodes *PKCECodes) (*CodexAuthBundle, error) {
74+
return o.ExchangeCodeForTokensWithRedirect(ctx, code, RedirectURI, pkceCodes)
75+
}
76+
77+
// ExchangeCodeForTokensWithRedirect exchanges an authorization code for tokens using
78+
// a caller-provided redirect URI. This supports alternate auth flows such as device
79+
// login while preserving the existing token parsing and storage behavior.
80+
func (o *CodexAuth) ExchangeCodeForTokensWithRedirect(ctx context.Context, code, redirectURI string, pkceCodes *PKCECodes) (*CodexAuthBundle, error) {
7481
if pkceCodes == nil {
7582
return nil, fmt.Errorf("PKCE codes are required for token exchange")
7683
}
84+
if strings.TrimSpace(redirectURI) == "" {
85+
return nil, fmt.Errorf("redirect URI is required for token exchange")
86+
}
7787

7888
// Prepare token exchange request
7989
data := url.Values{
8090
"grant_type": {"authorization_code"},
8191
"client_id": {ClientID},
8292
"code": {code},
83-
"redirect_uri": {RedirectURI},
93+
"redirect_uri": {strings.TrimSpace(redirectURI)},
8494
"code_verifier": {pkceCodes.CodeVerifier},
8595
}
8696

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"os"
8+
9+
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex"
10+
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
11+
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
12+
log "github.com/sirupsen/logrus"
13+
)
14+
15+
const (
16+
codexLoginModeMetadataKey = "codex_login_mode"
17+
codexLoginModeDevice = "device"
18+
)
19+
20+
// DoCodexDeviceLogin triggers the Codex device-code flow while keeping the
21+
// existing codex-login OAuth callback flow intact.
22+
func DoCodexDeviceLogin(cfg *config.Config, options *LoginOptions) {
23+
if options == nil {
24+
options = &LoginOptions{}
25+
}
26+
27+
promptFn := options.Prompt
28+
if promptFn == nil {
29+
promptFn = defaultProjectPrompt()
30+
}
31+
32+
manager := newAuthManager()
33+
34+
authOpts := &sdkAuth.LoginOptions{
35+
NoBrowser: options.NoBrowser,
36+
CallbackPort: options.CallbackPort,
37+
Metadata: map[string]string{
38+
codexLoginModeMetadataKey: codexLoginModeDevice,
39+
},
40+
Prompt: promptFn,
41+
}
42+
43+
_, savedPath, err := manager.Login(context.Background(), "codex", cfg, authOpts)
44+
if err != nil {
45+
if authErr, ok := errors.AsType[*codex.AuthenticationError](err); ok {
46+
log.Error(codex.GetUserFriendlyMessage(authErr))
47+
if authErr.Type == codex.ErrPortInUse.Type {
48+
os.Exit(codex.ErrPortInUse.Code)
49+
}
50+
return
51+
}
52+
fmt.Printf("Codex device authentication failed: %v\n", err)
53+
return
54+
}
55+
56+
if savedPath != "" {
57+
fmt.Printf("Authentication saved to %s\n", savedPath)
58+
}
59+
fmt.Println("Codex device authentication successful!")
60+
}

sdk/auth/codex.go

Lines changed: 5 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package auth
22

33
import (
44
"context"
5-
"crypto/sha256"
6-
"encoding/hex"
75
"fmt"
86
"net/http"
97
"strings"
@@ -48,6 +46,10 @@ func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
4846
opts = &LoginOptions{}
4947
}
5048

49+
if shouldUseCodexDeviceFlow(opts) {
50+
return a.loginWithDeviceFlow(ctx, cfg, opts)
51+
}
52+
5153
callbackPort := a.CallbackPort
5254
if opts.CallbackPort > 0 {
5355
callbackPort = opts.CallbackPort
@@ -186,39 +188,5 @@ waitForCallback:
186188
return nil, codex.NewAuthenticationError(codex.ErrCodeExchangeFailed, err)
187189
}
188190

189-
tokenStorage := authSvc.CreateTokenStorage(authBundle)
190-
191-
if tokenStorage == nil || tokenStorage.Email == "" {
192-
return nil, fmt.Errorf("codex token storage missing account information")
193-
}
194-
195-
planType := ""
196-
hashAccountID := ""
197-
if tokenStorage.IDToken != "" {
198-
if claims, errParse := codex.ParseJWTToken(tokenStorage.IDToken); errParse == nil && claims != nil {
199-
planType = strings.TrimSpace(claims.CodexAuthInfo.ChatgptPlanType)
200-
accountID := strings.TrimSpace(claims.CodexAuthInfo.ChatgptAccountID)
201-
if accountID != "" {
202-
digest := sha256.Sum256([]byte(accountID))
203-
hashAccountID = hex.EncodeToString(digest[:])[:8]
204-
}
205-
}
206-
}
207-
fileName := codex.CredentialFileName(tokenStorage.Email, planType, hashAccountID, true)
208-
metadata := map[string]any{
209-
"email": tokenStorage.Email,
210-
}
211-
212-
fmt.Println("Codex authentication successful")
213-
if authBundle.APIKey != "" {
214-
fmt.Println("Codex API key obtained and stored")
215-
}
216-
217-
return &coreauth.Auth{
218-
ID: fileName,
219-
Provider: a.Provider(),
220-
FileName: fileName,
221-
Storage: tokenStorage,
222-
Metadata: metadata,
223-
}, nil
191+
return a.buildAuthRecord(authSvc, authBundle)
224192
}

0 commit comments

Comments
 (0)