feat (auth): CLI OAuth supports pasting callback URLs to complete login

- Added callback URL resolution and terminal prompt logic
  - Codex/Claude/iFlow/Antigravity/Gemini login supports callback URL or local callback completion
  - Update Gemini login option signature and manager call
  - CLI default prompt function is compatible with null input to continue waiting
This commit is contained in:
Supra4E8C
2025-12-20 18:25:55 +08:00
parent 10f8c795ac
commit 93414f1baa
14 changed files with 302 additions and 33 deletions

View File

@@ -99,9 +99,18 @@ func (AntigravityAuthenticator) Login(ctx context.Context, cfg *config.Config, o
fmt.Println("Waiting for antigravity authentication callback...")
var cbRes callbackResult
manualCh, manualErrCh := promptForOAuthCallback(opts.Prompt, "antigravity")
select {
case res := <-cbChan:
cbRes = res
case manual := <-manualCh:
cbRes = callbackResult{
Code: manual.Code,
State: manual.State,
Error: manual.Error,
}
case err = <-manualErrCh:
return nil, err
case <-time.After(5 * time.Minute):
return nil, fmt.Errorf("antigravity: authentication timed out")
}

View File

@@ -98,16 +98,41 @@ func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opt
fmt.Println("Waiting for Claude authentication callback...")
result, err := oauthServer.WaitForCallback(5 * time.Minute)
if err != nil {
callbackCh := make(chan *claude.OAuthResult, 1)
callbackErrCh := make(chan error, 1)
manualCh, manualErrCh := promptForOAuthCallback(opts.Prompt, "Claude")
manualDescription := ""
go func() {
result, errWait := oauthServer.WaitForCallback(5 * time.Minute)
if errWait != nil {
callbackErrCh <- errWait
return
}
callbackCh <- result
}()
var result *claude.OAuthResult
select {
case result = <-callbackCh:
case err = <-callbackErrCh:
if strings.Contains(err.Error(), "timeout") {
return nil, claude.NewAuthenticationError(claude.ErrCallbackTimeout, err)
}
return nil, err
case manual := <-manualCh:
manualDescription = manual.ErrorDescription
result = &claude.OAuthResult{
Code: manual.Code,
State: manual.State,
Error: manual.Error,
}
case err = <-manualErrCh:
return nil, err
}
if result.Error != "" {
return nil, claude.NewOAuthError(result.Error, "", http.StatusBadRequest)
return nil, claude.NewOAuthError(result.Error, manualDescription, http.StatusBadRequest)
}
if result.State != state {

View File

@@ -97,16 +97,41 @@ func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
fmt.Println("Waiting for Codex authentication callback...")
result, err := oauthServer.WaitForCallback(5 * time.Minute)
if err != nil {
callbackCh := make(chan *codex.OAuthResult, 1)
callbackErrCh := make(chan error, 1)
manualCh, manualErrCh := promptForOAuthCallback(opts.Prompt, "Codex")
manualDescription := ""
go func() {
result, errWait := oauthServer.WaitForCallback(5 * time.Minute)
if errWait != nil {
callbackErrCh <- errWait
return
}
callbackCh <- result
}()
var result *codex.OAuthResult
select {
case result = <-callbackCh:
case err = <-callbackErrCh:
if strings.Contains(err.Error(), "timeout") {
return nil, codex.NewAuthenticationError(codex.ErrCallbackTimeout, err)
}
return nil, err
case manual := <-manualCh:
manualDescription = manual.ErrorDescription
result = &codex.OAuthResult{
Code: manual.Code,
State: manual.State,
Error: manual.Error,
}
case err = <-manualErrCh:
return nil, err
}
if result.Error != "" {
return nil, codex.NewOAuthError(result.Error, "", http.StatusBadRequest)
return nil, codex.NewOAuthError(result.Error, manualDescription, http.StatusBadRequest)
}
if result.State != state {

View File

@@ -44,7 +44,10 @@ func (a *GeminiAuthenticator) Login(ctx context.Context, cfg *config.Config, opt
}
geminiAuth := gemini.NewGeminiAuth()
_, err := geminiAuth.GetAuthenticatedClient(ctx, &ts, cfg, opts.NoBrowser)
_, err := geminiAuth.GetAuthenticatedClient(ctx, &ts, cfg, &gemini.WebLoginOptions{
NoBrowser: opts.NoBrowser,
Prompt: opts.Prompt,
})
if err != nil {
return nil, fmt.Errorf("gemini authentication failed: %w", err)
}

View File

@@ -84,9 +84,32 @@ func (a *IFlowAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
fmt.Println("Waiting for iFlow authentication callback...")
result, err := oauthServer.WaitForCallback(5 * time.Minute)
if err != nil {
callbackCh := make(chan *iflow.OAuthResult, 1)
callbackErrCh := make(chan error, 1)
manualCh, manualErrCh := promptForOAuthCallback(opts.Prompt, "iFlow")
go func() {
result, errWait := oauthServer.WaitForCallback(5 * time.Minute)
if errWait != nil {
callbackErrCh <- errWait
return
}
callbackCh <- result
}()
var result *iflow.OAuthResult
select {
case result = <-callbackCh:
case err = <-callbackErrCh:
return nil, fmt.Errorf("iflow auth: callback wait failed: %w", err)
case manual := <-manualCh:
result = &iflow.OAuthResult{
Code: manual.Code,
State: manual.State,
Error: manual.Error,
}
case err = <-manualErrCh:
return nil, err
}
if result.Error != "" {
return nil, fmt.Errorf("iflow auth: provider returned error %s", result.Error)

View File

@@ -0,0 +1,41 @@
package auth
import (
"fmt"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
)
func promptForOAuthCallback(prompt func(string) (string, error), provider string) (<-chan *misc.OAuthCallback, <-chan error) {
if prompt == nil {
return nil, nil
}
resultCh := make(chan *misc.OAuthCallback, 1)
errCh := make(chan error, 1)
go func() {
label := provider
if label == "" {
label = "OAuth"
}
input, err := prompt(fmt.Sprintf("Paste the %s callback URL (or press Enter to keep waiting): ", label))
if err != nil {
errCh <- err
return
}
parsed, err := misc.ParseOAuthCallback(input)
if err != nil {
errCh <- err
return
}
if parsed == nil {
return
}
resultCh <- parsed
}()
return resultCh, errCh
}