Skip to content

Conversation

@jroth1111
Copy link

@jroth1111 jroth1111 commented Dec 22, 2025

Summary

  • add shared oauthflow + oauthhttp helpers and provider adapters
  • route OAuth requests through a dedicated proxy path
  • unify auth flows for Claude/Codex/Gemini/iFlow/Qwen and add Copilot/Kiro providers

Motivation

  • standardize OAuth flows and error handling across providers to reduce drift and auth regressions
  • add new providers on a hardened, shared path instead of bespoke logic

Testing

  • go test ./internal/oauthflow ./internal/oauthhttp

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @jroth1111, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the system's OAuth capabilities by introducing a standardized and robust framework for authentication flows. It unifies the implementation details for existing providers like Claude, Codex, Gemini, iFlow, and Qwen under new shared oauthflow and oauthhttp packages. Furthermore, it expands the system's reach by integrating new providers such as GitHub Copilot and Kiro (AWS CodeWhisperer). The changes also include a proactive token refresh mechanism and improved error handling, all aimed at creating a more reliable and extensible authentication infrastructure.

Highlights

  • Unified OAuth Flows: Introduced shared oauthflow and oauthhttp packages to standardize and harden OAuth authentication processes across various providers, ensuring consistency and improved reliability.
  • New Provider Integrations: Added support for new OAuth providers, specifically GitHub Copilot and Kiro (AWS CodeWhisperer), expanding the range of services that can be authenticated through the system.
  • Refactored Existing Providers: Existing OAuth implementations for Claude, Codex, Gemini, iFlow, and Qwen have been refactored to leverage the new shared oauthflow and oauthhttp helpers, reducing code duplication and improving maintainability.
  • Hardened HTTP Requests: Implemented a new oauthhttp package that provides a hardened HTTP client wrapper with built-in retry and exponential backoff logic for transient network failures, enhancing the robustness of OAuth token exchanges and refreshes.
  • Dynamic Callback Port Handling: Improved the local callback server logic to dynamically find available ports if the default is in use, making OAuth flows more resilient to port conflicts on user machines.
  • Proactive Token Refresh Worker: Introduced a generic background worker (refresher) for proactive OAuth token refreshing, which monitors token expiry and attempts to refresh them before they become invalid, preventing service interruptions.
  • Kiro Protocol Handler: Added platform-specific logic for installing and managing a custom kiro:// protocol handler, enabling seamless social authentication (Google/GitHub) for Kiro on Linux, Windows, and macOS.
  • Enhanced Error Handling: Updated error handling across various OAuth flows, replacing log.Fatalf with log.Errorf and providing more informative JSON error responses to the client.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a significant and well-executed refactoring of the OAuth authentication flows. By creating shared oauthflow and oauthhttp helpers, you've unified the authentication logic for various providers, which greatly improves maintainability and robustness. The addition of new providers like Copilot and Kiro is well-integrated into this new framework. The change from log.Fatalf to log.Errorf in handlers is a critical improvement for server stability. The introduction of a dedicated proxy path for OAuth requests and thread-safe handling of global state are also excellent hardening measures. My review includes one suggestion to improve a polling loop for efficiency, but overall, this is a high-quality contribution.

Comment on lines 2427 to 2525
for {
if time.Now().After(deadline) {
log.Error("oauth flow timed out")
setOAuthStatus(state, "OAuth flow timed out")
return
}
if data, errR := os.ReadFile(waitFile); errR == nil {
var m map[string]string
_ = json.Unmarshal(data, &m)
_ = os.Remove(waitFile)
if errStr := m["error"]; errStr != "" {
log.Errorf("Authentication failed: %s", errStr)
setOAuthStatus(state, "Authentication failed")
return
}
if m["state"] != state {
log.Errorf("State mismatch")
setOAuthStatus(state, "State mismatch")
return
}
code := m["code"]
if code == "" {
log.Error("No authorization code received")
setOAuthStatus(state, "No authorization code received")
return
}

// Exchange code for tokens
tokenReq := &kiroauth.CreateTokenRequest{
Code: code,
CodeVerifier: codeVerifier,
RedirectURI: kiroauth.KiroRedirectURI,
}

tokenResp, errToken := socialClient.CreateToken(ctx, tokenReq)
if errToken != nil {
log.Errorf("Failed to exchange code for tokens: %v", errToken)
setOAuthStatus(state, "Failed to exchange code for tokens")
return
}

// Save the token
expiresIn := tokenResp.ExpiresIn
if expiresIn <= 0 {
expiresIn = 3600
}
expiresAt := time.Now().Add(time.Duration(expiresIn) * time.Second)
email := kiroauth.ExtractEmailFromJWT(tokenResp.AccessToken)

idPart := kiroauth.SanitizeEmailForFilename(email)
if idPart == "" {
idPart = fmt.Sprintf("%d", time.Now().UnixNano()%100000)
}

now := time.Now()
fileName := fmt.Sprintf("kiro-%s-%s.json", strings.ToLower(provider), idPart)

record := &coreauth.Auth{
ID: fileName,
Provider: "kiro",
FileName: fileName,
Metadata: map[string]any{
"type": "kiro",
"access_token": tokenResp.AccessToken,
"refresh_token": tokenResp.RefreshToken,
"profile_arn": tokenResp.ProfileArn,
"expires_at": expiresAt.Format(time.RFC3339),
"auth_method": "social",
"provider": provider,
"email": email,
"last_refresh": now.Format(time.RFC3339),
},
}

savedPath, errSave := h.saveTokenRecord(ctx, record)
if errSave != nil {
log.Errorf("Failed to save authentication tokens: %v", errSave)
setOAuthStatus(state, "Failed to save authentication tokens")
return
}

fmt.Printf("Authentication successful! Token saved to %s\n", savedPath)
if email != "" {
fmt.Printf("Authenticated as: %s\n", email)
}
deleteOAuthStatus(state)
return
}
time.Sleep(500 * time.Millisecond)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This polling loop uses time.Sleep, which can be inefficient as it blocks the goroutine. For better resource usage and responsiveness to context cancellation, consider refactoring this to use a time.Ticker within a select block. This pattern is already used in the device code polling loop in the aws/builder-id case within this same function.

            ticker := time.NewTicker(500 * time.Millisecond)
            defer ticker.Stop()

            for {
                select {
                case <-ctx.Done():
                    log.Error("oauth flow cancelled")
                    setOAuthStatus(state, "OAuth flow cancelled")
                    return
                case <-ticker.C:
                    if time.Now().After(deadline) {
                        log.Error("oauth flow timed out")
                        setOAuthStatus(state, "OAuth flow timed out")
                        return
                    }
                    if data, errR := os.ReadFile(waitFile); errR == nil {
                        var m map[string]string
                        _ = json.Unmarshal(data, &m)
                        _ = os.Remove(waitFile)
                        if errStr := m["error"]; errStr != "" {
                            log.Errorf("Authentication failed: %s", errStr)
                            setOAuthStatus(state, "Authentication failed")
                            return
                        }
                        if m["state"] != state {
                            log.Errorf("State mismatch")
                            setOAuthStatus(state, "State mismatch")
                            return
                        }
                        code := m["code"]
                        if code == "" {
                            log.Error("No authorization code received")
                            setOAuthStatus(state, "No authorization code received")
                            return
                        }

                        // Exchange code for tokens
                        tokenReq := &kiroauth.CreateTokenRequest{
                            Code:         code,
                            CodeVerifier: codeVerifier,
                            RedirectURI:  kiroauth.KiroRedirectURI,
                        }

                        tokenResp, errToken := socialClient.CreateToken(ctx, tokenReq)
                        if errToken != nil {
                            log.Errorf("Failed to exchange code for tokens: %v", errToken)
                            setOAuthStatus(state, "Failed to exchange code for tokens")
                            return
                        }

                        // Save the token
                        expiresIn := tokenResp.ExpiresIn
                        if expiresIn <= 0 {
                            expiresIn = 3600
                        }
                        expiresAt := time.Now().Add(time.Duration(expiresIn) * time.Second)
                        email := kiroauth.ExtractEmailFromJWT(tokenResp.AccessToken)

                        idPart := kiroauth.SanitizeEmailForFilename(email)
                        if idPart == "" {
                            idPart = fmt.Sprintf("%d", time.Now().UnixNano()%100000)
                        }

                        now := time.Now()
                        fileName := fmt.Sprintf("kiro-%s-%s.json", strings.ToLower(provider), idPart)

                        record := &coreauth.Auth{
                            ID:       fileName,
                            Provider: "kiro",
                            FileName: fileName,
                            Metadata: map[string]any{
                                "type":          "kiro",
                                "access_token":  tokenResp.AccessToken,
                                "refresh_token": tokenResp.RefreshToken,
                                "profile_arn":   tokenResp.ProfileArn,
                                "expires_at":    expiresAt.Format(time.RFC3339),
                                "auth_method":   "social",
                                "provider":      provider,
                                "email":         email,
                                "last_refresh":  now.Format(time.RFC3339),
                            },
                        }

                        savedPath, errSave := h.saveTokenRecord(ctx, record)
                        if errSave != nil {
                            log.Errorf("Failed to save authentication tokens: %v", errSave)
                            setOAuthStatus(state, "Failed to save authentication tokens")
                            return
                        }

                        fmt.Printf("Authentication successful! Token saved to %s\n", savedPath)
                        if email != "" {
                            fmt.Printf("Authenticated as: %s\n", email)
                        }
                        deleteOAuthStatus(state)
                        return
                    }
                }
            }

@luispater
Copy link
Collaborator

Please clear the conflicts.

@jroth1111 jroth1111 force-pushed the feat/oauth-hardening-cli branch from 53f0c6e to bb1098a Compare December 22, 2025 12:53
@jroth1111
Copy link
Author

Rebased on upstream/main and resolved conflicts. OAuth hardening changes are now on top of latest main. @luispater would you mind re-reviewing?

@jroth1111
Copy link
Author

Updated polling loops per review: switched OAuth callback waits to ticker+select (ctx cancel/timeout) across providers and made Gemini onboarding sleep ctx-aware. Ready for re-review. @luispater

@luispater
Copy link
Collaborator

Updated polling loops per review: switched OAuth callback waits to ticker+select (ctx cancel/timeout) across providers and made Gemini onboarding sleep ctx-aware. Ready for re-review. @luispater

This project is not the Plus version.

This project does not accept any pull requests related to third-party provider support.

You should clean up the code pertaining to GitHub Copilot and Kiro.

After that, please wait for this PR to be merged into main before creating a new PR for the Plus version.

@jroth1111
Copy link
Author

Per your feedback, removed GitHub Copilot + Kiro provider support from CLIProxyAPI (auth packages, SDK auth, and management handler paths). Tests: ? github.com/router-for-me/CLIProxyAPI/v6/cmd/server [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/examples/custom-provider [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/examples/translator [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/access [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/access/config_access [no test files]
ok github.com/router-for-me/CLIProxyAPI/v6/internal/api (cached)
? github.com/router-for-me/CLIProxyAPI/v6/internal/api/handlers/management [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/api/middleware [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/api/modules [no test files]
ok github.com/router-for-me/CLIProxyAPI/v6/internal/api/modules/amp (cached)
? github.com/router-for-me/CLIProxyAPI/v6/internal/auth [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/auth/empty [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/auth/generic [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen [no test files]
ok github.com/router-for-me/CLIProxyAPI/v6/internal/auth/refresher (cached)
? github.com/router-for-me/CLIProxyAPI/v6/internal/auth/vertex [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/browser [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/buildinfo [no test files]
ok github.com/router-for-me/CLIProxyAPI/v6/internal/cache (cached)
? github.com/router-for-me/CLIProxyAPI/v6/internal/cmd [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/config [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/constant [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces [no test files]
ok github.com/router-for-me/CLIProxyAPI/v6/internal/logging (cached)
? github.com/router-for-me/CLIProxyAPI/v6/internal/managementasset [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/misc [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/oauthflow [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/oauthhttp [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/registry [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/geminicli [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/store [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator [no test files]
ok github.com/router-for-me/CLIProxyAPI/v6/internal/translator/antigravity/claude (cached)
ok github.com/router-for-me/CLIProxyAPI/v6/internal/translator/antigravity/gemini (cached)
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/antigravity/openai/chat-completions [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/antigravity/openai/responses [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/claude/gemini [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/claude/gemini-cli [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/claude/openai/chat-completions [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/claude/openai/responses [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/claude [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/gemini [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/gemini-cli [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/openai/chat-completions [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/openai/responses [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/claude [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/gemini [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/gemini-cli [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/chat-completions [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/responses [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini-cli/claude [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini-cli/gemini [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini-cli/openai/chat-completions [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini-cli/openai/responses [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/claude [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/gemini [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/gemini-cli [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/openai/chat-completions [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/openai/responses [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/internal/usage [no test files]
ok github.com/router-for-me/CLIProxyAPI/v6/internal/util (cached)
ok github.com/router-for-me/CLIProxyAPI/v6/internal/watcher (cached)
ok github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/diff (cached)
ok github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/synthesizer (cached)
? github.com/router-for-me/CLIProxyAPI/v6/internal/wsrelay [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/sdk/access [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/sdk/api [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/claude [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/gemini [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/openai [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/sdk/auth [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/pipeline [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/usage [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/sdk/config [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/sdk/logging [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/sdk/translator [no test files]
? github.com/router-for-me/CLIProxyAPI/v6/sdk/translator/builtin [no test files]
ok github.com/router-for-me/CLIProxyAPI/v6/test (cached). Ready for re-review. @luispater

@jroth1111
Copy link
Author

Follow-up ping: Copilot/Kiro removals are in; tests green. Ready for re-review when you have time. @luispater

@jroth1111
Copy link
Author

Also removed the last Copilot mention (comment in OpenAI->Claude stream handling). returns no matches now. @luispater

@jroth1111
Copy link
Author

Removed the internal/translator change to satisfy the translator-path-guard; opened issue #677 for maintainers to update that comment. OAuth hardening PR now avoids translator changes. All Gemini feedback (ticker+ctx cancellation) was already applied. Ready for re-review.

@jroth1111 jroth1111 force-pushed the feat/oauth-hardening-cli branch from 5b1d4ff to 19cb819 Compare December 28, 2025 03:12
@jroth1111
Copy link
Author

@luispater rebase/conflict cleanup done; removed GitHub Copilot + Kiro support and avoided translator-path-guard changes. Ready for re-review.

@jroth1111 jroth1111 changed the title feat: harden oauth flows and providers feat: harden oauth flows and improve model routing Dec 28, 2025
@jroth1111 jroth1111 changed the title feat: harden oauth flows and improve model routing feat: harden oauth flows and providers Dec 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants