Refactor codebase
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
// Package claude provides OAuth2 authentication functionality for Anthropic's Claude API.
|
||||
// This package implements the complete OAuth2 flow with PKCE (Proof Key for Code Exchange)
|
||||
// for secure authentication with Claude API, including token exchange, refresh, and storage.
|
||||
package claude
|
||||
|
||||
import (
|
||||
@@ -22,7 +25,8 @@ const (
|
||||
redirectURI = "http://localhost:54545/callback"
|
||||
)
|
||||
|
||||
// Parse token response
|
||||
// tokenResponse represents the response structure from Anthropic's OAuth token endpoint.
|
||||
// It contains access token, refresh token, and associated user/organization information.
|
||||
type tokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
@@ -38,19 +42,39 @@ type tokenResponse struct {
|
||||
} `json:"account"`
|
||||
}
|
||||
|
||||
// ClaudeAuth handles Anthropic OAuth2 authentication flow
|
||||
// ClaudeAuth handles Anthropic OAuth2 authentication flow.
|
||||
// It provides methods for generating authorization URLs, exchanging codes for tokens,
|
||||
// and refreshing expired tokens using PKCE for enhanced security.
|
||||
type ClaudeAuth struct {
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewClaudeAuth creates a new Anthropic authentication service
|
||||
// NewClaudeAuth creates a new Anthropic authentication service.
|
||||
// It initializes the HTTP client with proxy settings from the configuration.
|
||||
//
|
||||
// Parameters:
|
||||
// - cfg: The application configuration containing proxy settings
|
||||
//
|
||||
// Returns:
|
||||
// - *ClaudeAuth: A new Claude authentication service instance
|
||||
func NewClaudeAuth(cfg *config.Config) *ClaudeAuth {
|
||||
return &ClaudeAuth{
|
||||
httpClient: util.SetProxy(cfg, &http.Client{}),
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateAuthURL creates the OAuth authorization URL with PKCE
|
||||
// GenerateAuthURL creates the OAuth authorization URL with PKCE.
|
||||
// This method generates a secure authorization URL including PKCE challenge codes
|
||||
// for the OAuth2 flow with Anthropic's API.
|
||||
//
|
||||
// Parameters:
|
||||
// - state: A random state parameter for CSRF protection
|
||||
// - pkceCodes: The PKCE codes for secure code exchange
|
||||
//
|
||||
// Returns:
|
||||
// - string: The complete authorization URL
|
||||
// - string: The state parameter for verification
|
||||
// - error: An error if PKCE codes are missing or URL generation fails
|
||||
func (o *ClaudeAuth) GenerateAuthURL(state string, pkceCodes *PKCECodes) (string, string, error) {
|
||||
if pkceCodes == nil {
|
||||
return "", "", fmt.Errorf("PKCE codes are required")
|
||||
@@ -71,6 +95,15 @@ func (o *ClaudeAuth) GenerateAuthURL(state string, pkceCodes *PKCECodes) (string
|
||||
return authURL, state, nil
|
||||
}
|
||||
|
||||
// parseCodeAndState extracts the authorization code and state from the callback response.
|
||||
// It handles the parsing of the code parameter which may contain additional fragments.
|
||||
//
|
||||
// Parameters:
|
||||
// - code: The raw code parameter from the OAuth callback
|
||||
//
|
||||
// Returns:
|
||||
// - parsedCode: The extracted authorization code
|
||||
// - parsedState: The extracted state parameter if present
|
||||
func (c *ClaudeAuth) parseCodeAndState(code string) (parsedCode, parsedState string) {
|
||||
splits := strings.Split(code, "#")
|
||||
parsedCode = splits[0]
|
||||
@@ -80,7 +113,19 @@ func (c *ClaudeAuth) parseCodeAndState(code string) (parsedCode, parsedState str
|
||||
return
|
||||
}
|
||||
|
||||
// ExchangeCodeForTokens exchanges authorization code for access tokens
|
||||
// ExchangeCodeForTokens exchanges authorization code for access tokens.
|
||||
// This method implements the OAuth2 token exchange flow using PKCE for security.
|
||||
// It sends the authorization code along with PKCE verifier to get access and refresh tokens.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: The context for the request
|
||||
// - code: The authorization code received from OAuth callback
|
||||
// - state: The state parameter for verification
|
||||
// - pkceCodes: The PKCE codes for secure verification
|
||||
//
|
||||
// Returns:
|
||||
// - *ClaudeAuthBundle: The complete authentication bundle with tokens
|
||||
// - error: An error if token exchange fails
|
||||
func (o *ClaudeAuth) ExchangeCodeForTokens(ctx context.Context, code, state string, pkceCodes *PKCECodes) (*ClaudeAuthBundle, error) {
|
||||
if pkceCodes == nil {
|
||||
return nil, fmt.Errorf("PKCE codes are required for token exchange")
|
||||
@@ -121,7 +166,9 @@ func (o *ClaudeAuth) ExchangeCodeForTokens(ctx context.Context, code, state stri
|
||||
return nil, fmt.Errorf("token exchange request failed: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
if errClose := resp.Body.Close(); errClose != nil {
|
||||
log.Errorf("failed to close response body: %v", errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
@@ -157,7 +204,17 @@ func (o *ClaudeAuth) ExchangeCodeForTokens(ctx context.Context, code, state stri
|
||||
return bundle, nil
|
||||
}
|
||||
|
||||
// RefreshTokens refreshes the access token using the refresh token
|
||||
// RefreshTokens refreshes the access token using the refresh token.
|
||||
// This method exchanges a valid refresh token for a new access token,
|
||||
// extending the user's authenticated session.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: The context for the request
|
||||
// - refreshToken: The refresh token to use for getting new access token
|
||||
//
|
||||
// Returns:
|
||||
// - *ClaudeTokenData: The new token data with updated access token
|
||||
// - error: An error if token refresh fails
|
||||
func (o *ClaudeAuth) RefreshTokens(ctx context.Context, refreshToken string) (*ClaudeTokenData, error) {
|
||||
if refreshToken == "" {
|
||||
return nil, fmt.Errorf("refresh token is required")
|
||||
@@ -215,7 +272,15 @@ func (o *ClaudeAuth) RefreshTokens(ctx context.Context, refreshToken string) (*C
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateTokenStorage creates a new ClaudeTokenStorage from auth bundle and user info
|
||||
// CreateTokenStorage creates a new ClaudeTokenStorage from auth bundle and user info.
|
||||
// This method converts the authentication bundle into a token storage structure
|
||||
// suitable for persistence and later use.
|
||||
//
|
||||
// Parameters:
|
||||
// - bundle: The authentication bundle containing token data
|
||||
//
|
||||
// Returns:
|
||||
// - *ClaudeTokenStorage: A new token storage instance
|
||||
func (o *ClaudeAuth) CreateTokenStorage(bundle *ClaudeAuthBundle) *ClaudeTokenStorage {
|
||||
storage := &ClaudeTokenStorage{
|
||||
AccessToken: bundle.TokenData.AccessToken,
|
||||
@@ -228,7 +293,18 @@ func (o *ClaudeAuth) CreateTokenStorage(bundle *ClaudeAuthBundle) *ClaudeTokenSt
|
||||
return storage
|
||||
}
|
||||
|
||||
// RefreshTokensWithRetry refreshes tokens with automatic retry logic
|
||||
// RefreshTokensWithRetry refreshes tokens with automatic retry logic.
|
||||
// This method implements exponential backoff retry logic for token refresh operations,
|
||||
// providing resilience against temporary network or service issues.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: The context for the request
|
||||
// - refreshToken: The refresh token to use
|
||||
// - maxRetries: The maximum number of retry attempts
|
||||
//
|
||||
// Returns:
|
||||
// - *ClaudeTokenData: The refreshed token data
|
||||
// - error: An error if all retry attempts fail
|
||||
func (o *ClaudeAuth) RefreshTokensWithRetry(ctx context.Context, refreshToken string, maxRetries int) (*ClaudeTokenData, error) {
|
||||
var lastErr error
|
||||
|
||||
@@ -254,7 +330,13 @@ func (o *ClaudeAuth) RefreshTokensWithRetry(ctx context.Context, refreshToken st
|
||||
return nil, fmt.Errorf("token refresh failed after %d attempts: %w", maxRetries, lastErr)
|
||||
}
|
||||
|
||||
// UpdateTokenStorage updates an existing token storage with new token data
|
||||
// UpdateTokenStorage updates an existing token storage with new token data.
|
||||
// This method refreshes the token storage with newly obtained access and refresh tokens,
|
||||
// updating timestamps and expiration information.
|
||||
//
|
||||
// Parameters:
|
||||
// - storage: The existing token storage to update
|
||||
// - tokenData: The new token data to apply
|
||||
func (o *ClaudeAuth) UpdateTokenStorage(storage *ClaudeTokenStorage, tokenData *ClaudeTokenData) {
|
||||
storage.AccessToken = tokenData.AccessToken
|
||||
storage.RefreshToken = tokenData.RefreshToken
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Package claude provides authentication and token management functionality
|
||||
// for Anthropic's Claude AI services. It handles OAuth2 token storage, serialization,
|
||||
// and retrieval for maintaining authenticated sessions with the Claude API.
|
||||
package claude
|
||||
|
||||
import (
|
||||
@@ -6,14 +9,19 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// OAuthError represents an OAuth-specific error
|
||||
// OAuthError represents an OAuth-specific error.
|
||||
type OAuthError struct {
|
||||
Code string `json:"error"`
|
||||
// Code is the OAuth error code.
|
||||
Code string `json:"error"`
|
||||
// Description is a human-readable description of the error.
|
||||
Description string `json:"error_description,omitempty"`
|
||||
URI string `json:"error_uri,omitempty"`
|
||||
StatusCode int `json:"-"`
|
||||
// URI is a URI identifying a human-readable web page with information about the error.
|
||||
URI string `json:"error_uri,omitempty"`
|
||||
// StatusCode is the HTTP status code associated with the error.
|
||||
StatusCode int `json:"-"`
|
||||
}
|
||||
|
||||
// Error returns a string representation of the OAuth error.
|
||||
func (e *OAuthError) Error() string {
|
||||
if e.Description != "" {
|
||||
return fmt.Sprintf("OAuth error %s: %s", e.Code, e.Description)
|
||||
@@ -21,7 +29,7 @@ func (e *OAuthError) Error() string {
|
||||
return fmt.Sprintf("OAuth error: %s", e.Code)
|
||||
}
|
||||
|
||||
// NewOAuthError creates a new OAuth error
|
||||
// NewOAuthError creates a new OAuth error with the specified code, description, and status code.
|
||||
func NewOAuthError(code, description string, statusCode int) *OAuthError {
|
||||
return &OAuthError{
|
||||
Code: code,
|
||||
@@ -30,14 +38,19 @@ func NewOAuthError(code, description string, statusCode int) *OAuthError {
|
||||
}
|
||||
}
|
||||
|
||||
// AuthenticationError represents authentication-related errors
|
||||
// AuthenticationError represents authentication-related errors.
|
||||
type AuthenticationError struct {
|
||||
Type string `json:"type"`
|
||||
// Type is the type of authentication error.
|
||||
Type string `json:"type"`
|
||||
// Message is a human-readable message describing the error.
|
||||
Message string `json:"message"`
|
||||
Code int `json:"code"`
|
||||
Cause error `json:"-"`
|
||||
// Code is the HTTP status code associated with the error.
|
||||
Code int `json:"code"`
|
||||
// Cause is the underlying error that caused this authentication error.
|
||||
Cause error `json:"-"`
|
||||
}
|
||||
|
||||
// Error returns a string representation of the authentication error.
|
||||
func (e *AuthenticationError) Error() string {
|
||||
if e.Cause != nil {
|
||||
return fmt.Sprintf("%s: %s (caused by: %v)", e.Type, e.Message, e.Cause)
|
||||
@@ -45,44 +58,50 @@ func (e *AuthenticationError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.Type, e.Message)
|
||||
}
|
||||
|
||||
// Common authentication error types
|
||||
// Common authentication error types.
|
||||
var (
|
||||
ErrTokenExpired = &AuthenticationError{
|
||||
Type: "token_expired",
|
||||
Message: "Access token has expired",
|
||||
Code: http.StatusUnauthorized,
|
||||
}
|
||||
// ErrTokenExpired = &AuthenticationError{
|
||||
// Type: "token_expired",
|
||||
// Message: "Access token has expired",
|
||||
// Code: http.StatusUnauthorized,
|
||||
// }
|
||||
|
||||
// ErrInvalidState represents an error for invalid OAuth state parameter.
|
||||
ErrInvalidState = &AuthenticationError{
|
||||
Type: "invalid_state",
|
||||
Message: "OAuth state parameter is invalid",
|
||||
Code: http.StatusBadRequest,
|
||||
}
|
||||
|
||||
// ErrCodeExchangeFailed represents an error when exchanging authorization code for tokens fails.
|
||||
ErrCodeExchangeFailed = &AuthenticationError{
|
||||
Type: "code_exchange_failed",
|
||||
Message: "Failed to exchange authorization code for tokens",
|
||||
Code: http.StatusBadRequest,
|
||||
}
|
||||
|
||||
// ErrServerStartFailed represents an error when starting the OAuth callback server fails.
|
||||
ErrServerStartFailed = &AuthenticationError{
|
||||
Type: "server_start_failed",
|
||||
Message: "Failed to start OAuth callback server",
|
||||
Code: http.StatusInternalServerError,
|
||||
}
|
||||
|
||||
// ErrPortInUse represents an error when the OAuth callback port is already in use.
|
||||
ErrPortInUse = &AuthenticationError{
|
||||
Type: "port_in_use",
|
||||
Message: "OAuth callback port is already in use",
|
||||
Code: 13, // Special exit code for port-in-use
|
||||
}
|
||||
|
||||
// ErrCallbackTimeout represents an error when waiting for OAuth callback times out.
|
||||
ErrCallbackTimeout = &AuthenticationError{
|
||||
Type: "callback_timeout",
|
||||
Message: "Timeout waiting for OAuth callback",
|
||||
Code: http.StatusRequestTimeout,
|
||||
}
|
||||
|
||||
// ErrBrowserOpenFailed represents an error when opening the browser for authentication fails.
|
||||
ErrBrowserOpenFailed = &AuthenticationError{
|
||||
Type: "browser_open_failed",
|
||||
Message: "Failed to open browser for authentication",
|
||||
@@ -90,7 +109,7 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
// NewAuthenticationError creates a new authentication error with a cause
|
||||
// NewAuthenticationError creates a new authentication error with a cause based on a base error.
|
||||
func NewAuthenticationError(baseErr *AuthenticationError, cause error) *AuthenticationError {
|
||||
return &AuthenticationError{
|
||||
Type: baseErr.Type,
|
||||
@@ -100,21 +119,21 @@ func NewAuthenticationError(baseErr *AuthenticationError, cause error) *Authenti
|
||||
}
|
||||
}
|
||||
|
||||
// IsAuthenticationError checks if an error is an authentication error
|
||||
// IsAuthenticationError checks if an error is an authentication error.
|
||||
func IsAuthenticationError(err error) bool {
|
||||
var authenticationError *AuthenticationError
|
||||
ok := errors.As(err, &authenticationError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsOAuthError checks if an error is an OAuth error
|
||||
// IsOAuthError checks if an error is an OAuth error.
|
||||
func IsOAuthError(err error) bool {
|
||||
var oAuthError *OAuthError
|
||||
ok := errors.As(err, &oAuthError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetUserFriendlyMessage returns a user-friendly error message
|
||||
// GetUserFriendlyMessage returns a user-friendly error message based on the error type.
|
||||
func GetUserFriendlyMessage(err error) string {
|
||||
switch {
|
||||
case IsAuthenticationError(err):
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
// Package claude provides authentication and token management functionality
|
||||
// for Anthropic's Claude AI services. It handles OAuth2 token storage, serialization,
|
||||
// and retrieval for maintaining authenticated sessions with the Claude API.
|
||||
package claude
|
||||
|
||||
// LoginSuccessHtml is the template for the OAuth success page
|
||||
// LoginSuccessHtml is the HTML template displayed to users after successful OAuth authentication.
|
||||
// This template provides a user-friendly success page with options to close the window
|
||||
// or navigate to the Claude platform. It includes automatic window closing functionality
|
||||
// and keyboard accessibility features.
|
||||
const LoginSuccessHtml = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -202,7 +208,9 @@ const LoginSuccessHtml = `<!DOCTYPE html>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
// SetupNoticeHtml is the template for the setup notice section
|
||||
// SetupNoticeHtml is the HTML template for the setup notice section.
|
||||
// This template is embedded within the success page to inform users about
|
||||
// additional setup steps required to complete their Claude account configuration.
|
||||
const SetupNoticeHtml = `
|
||||
<div class="setup-notice">
|
||||
<h3>Additional Setup Required</h3>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Package claude provides authentication and token management functionality
|
||||
// for Anthropic's Claude AI services. It handles OAuth2 token storage, serialization,
|
||||
// and retrieval for maintaining authenticated sessions with the Claude API.
|
||||
package claude
|
||||
|
||||
import (
|
||||
@@ -13,24 +16,45 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// OAuthServer handles the local HTTP server for OAuth callbacks
|
||||
// OAuthServer handles the local HTTP server for OAuth callbacks.
|
||||
// It listens for the authorization code response from the OAuth provider
|
||||
// and captures the necessary parameters to complete the authentication flow.
|
||||
type OAuthServer struct {
|
||||
server *http.Server
|
||||
port int
|
||||
// server is the underlying HTTP server instance
|
||||
server *http.Server
|
||||
// port is the port number on which the server listens
|
||||
port int
|
||||
// resultChan is a channel for sending OAuth results
|
||||
resultChan chan *OAuthResult
|
||||
errorChan chan error
|
||||
mu sync.Mutex
|
||||
running bool
|
||||
// errorChan is a channel for sending OAuth errors
|
||||
errorChan chan error
|
||||
// mu is a mutex for protecting server state
|
||||
mu sync.Mutex
|
||||
// running indicates whether the server is currently running
|
||||
running bool
|
||||
}
|
||||
|
||||
// OAuthResult contains the result of the OAuth callback
|
||||
// OAuthResult contains the result of the OAuth callback.
|
||||
// It holds either the authorization code and state for successful authentication
|
||||
// or an error message if the authentication failed.
|
||||
type OAuthResult struct {
|
||||
Code string
|
||||
// Code is the authorization code received from the OAuth provider
|
||||
Code string
|
||||
// State is the state parameter used to prevent CSRF attacks
|
||||
State string
|
||||
// Error contains any error message if the OAuth flow failed
|
||||
Error string
|
||||
}
|
||||
|
||||
// NewOAuthServer creates a new OAuth callback server
|
||||
// NewOAuthServer creates a new OAuth callback server.
|
||||
// It initializes the server with the specified port and creates channels
|
||||
// for handling OAuth results and errors.
|
||||
//
|
||||
// Parameters:
|
||||
// - port: The port number on which the server should listen
|
||||
//
|
||||
// Returns:
|
||||
// - *OAuthServer: A new OAuthServer instance
|
||||
func NewOAuthServer(port int) *OAuthServer {
|
||||
return &OAuthServer{
|
||||
port: port,
|
||||
@@ -39,8 +63,13 @@ func NewOAuthServer(port int) *OAuthServer {
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the OAuth callback server
|
||||
func (s *OAuthServer) Start(ctx context.Context) error {
|
||||
// Start starts the OAuth callback server.
|
||||
// It sets up the HTTP handlers for the callback and success endpoints,
|
||||
// and begins listening on the specified port.
|
||||
//
|
||||
// Returns:
|
||||
// - error: An error if the server fails to start
|
||||
func (s *OAuthServer) Start() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
@@ -79,7 +108,14 @@ func (s *OAuthServer) Start(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop gracefully stops the OAuth callback server
|
||||
// Stop gracefully stops the OAuth callback server.
|
||||
// It performs a graceful shutdown of the HTTP server with a timeout.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: The context for controlling the shutdown process
|
||||
//
|
||||
// Returns:
|
||||
// - error: An error if the server fails to stop gracefully
|
||||
func (s *OAuthServer) Stop(ctx context.Context) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -101,7 +137,16 @@ func (s *OAuthServer) Stop(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// WaitForCallback waits for the OAuth callback with a timeout
|
||||
// WaitForCallback waits for the OAuth callback with a timeout.
|
||||
// It blocks until either an OAuth result is received, an error occurs,
|
||||
// or the specified timeout is reached.
|
||||
//
|
||||
// Parameters:
|
||||
// - timeout: The maximum time to wait for the callback
|
||||
//
|
||||
// Returns:
|
||||
// - *OAuthResult: The OAuth result if successful
|
||||
// - error: An error if the callback times out or an error occurs
|
||||
func (s *OAuthServer) WaitForCallback(timeout time.Duration) (*OAuthResult, error) {
|
||||
select {
|
||||
case result := <-s.resultChan:
|
||||
@@ -113,7 +158,13 @@ func (s *OAuthServer) WaitForCallback(timeout time.Duration) (*OAuthResult, erro
|
||||
}
|
||||
}
|
||||
|
||||
// handleCallback handles the OAuth callback endpoint
|
||||
// handleCallback handles the OAuth callback endpoint.
|
||||
// It extracts the authorization code and state from the callback URL,
|
||||
// validates the parameters, and sends the result to the waiting channel.
|
||||
//
|
||||
// Parameters:
|
||||
// - w: The HTTP response writer
|
||||
// - r: The HTTP request
|
||||
func (s *OAuthServer) handleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
log.Debug("Received OAuth callback")
|
||||
|
||||
@@ -171,7 +222,12 @@ func (s *OAuthServer) handleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/success", http.StatusFound)
|
||||
}
|
||||
|
||||
// handleSuccess handles the success page endpoint
|
||||
// handleSuccess handles the success page endpoint.
|
||||
// It serves a user-friendly HTML page indicating that authentication was successful.
|
||||
//
|
||||
// Parameters:
|
||||
// - w: The HTTP response writer
|
||||
// - r: The HTTP request
|
||||
func (s *OAuthServer) handleSuccess(w http.ResponseWriter, r *http.Request) {
|
||||
log.Debug("Serving success page")
|
||||
|
||||
@@ -195,7 +251,16 @@ func (s *OAuthServer) handleSuccess(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// generateSuccessHTML creates the HTML content for the success page
|
||||
// generateSuccessHTML creates the HTML content for the success page.
|
||||
// It customizes the page based on whether additional setup is required
|
||||
// and includes a link to the platform.
|
||||
//
|
||||
// Parameters:
|
||||
// - setupRequired: Whether additional setup is required after authentication
|
||||
// - platformURL: The URL to the platform for additional setup
|
||||
//
|
||||
// Returns:
|
||||
// - string: The HTML content for the success page
|
||||
func (s *OAuthServer) generateSuccessHTML(setupRequired bool, platformURL string) string {
|
||||
html := LoginSuccessHtml
|
||||
|
||||
@@ -213,7 +278,11 @@ func (s *OAuthServer) generateSuccessHTML(setupRequired bool, platformURL string
|
||||
return html
|
||||
}
|
||||
|
||||
// sendResult sends the OAuth result to the waiting channel
|
||||
// sendResult sends the OAuth result to the waiting channel.
|
||||
// It ensures that the result is sent without blocking the handler.
|
||||
//
|
||||
// Parameters:
|
||||
// - result: The OAuth result to send
|
||||
func (s *OAuthServer) sendResult(result *OAuthResult) {
|
||||
select {
|
||||
case s.resultChan <- result:
|
||||
@@ -223,7 +292,11 @@ func (s *OAuthServer) sendResult(result *OAuthResult) {
|
||||
}
|
||||
}
|
||||
|
||||
// isPortAvailable checks if the specified port is available
|
||||
// isPortAvailable checks if the specified port is available.
|
||||
// It attempts to listen on the port to determine availability.
|
||||
//
|
||||
// Returns:
|
||||
// - bool: True if the port is available, false otherwise
|
||||
func (s *OAuthServer) isPortAvailable() bool {
|
||||
addr := fmt.Sprintf(":%d", s.port)
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
@@ -236,7 +309,10 @@ func (s *OAuthServer) isPortAvailable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsRunning returns whether the server is currently running
|
||||
// IsRunning returns whether the server is currently running.
|
||||
//
|
||||
// Returns:
|
||||
// - bool: True if the server is running, false otherwise
|
||||
func (s *OAuthServer) IsRunning() bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Package claude provides authentication and token management functionality
|
||||
// for Anthropic's Claude AI services. It handles OAuth2 token storage, serialization,
|
||||
// and retrieval for maintaining authenticated sessions with the Claude API.
|
||||
package claude
|
||||
|
||||
import (
|
||||
@@ -8,7 +11,13 @@ import (
|
||||
)
|
||||
|
||||
// GeneratePKCECodes generates a PKCE code verifier and challenge pair
|
||||
// following RFC 7636 specifications for OAuth 2.0 PKCE extension
|
||||
// following RFC 7636 specifications for OAuth 2.0 PKCE extension.
|
||||
// This provides additional security for the OAuth flow by ensuring that
|
||||
// only the client that initiated the request can exchange the authorization code.
|
||||
//
|
||||
// Returns:
|
||||
// - *PKCECodes: A struct containing the code verifier and challenge
|
||||
// - error: An error if the generation fails, nil otherwise
|
||||
func GeneratePKCECodes() (*PKCECodes, error) {
|
||||
// Generate code verifier: 43-128 characters, URL-safe
|
||||
codeVerifier, err := generateCodeVerifier()
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Package claude provides authentication and token management functionality
|
||||
// for Anthropic's Claude AI services. It handles OAuth2 token storage, serialization,
|
||||
// and retrieval for maintaining authenticated sessions with the Claude API.
|
||||
package claude
|
||||
|
||||
import (
|
||||
@@ -7,32 +10,50 @@ import (
|
||||
"path"
|
||||
)
|
||||
|
||||
// ClaudeTokenStorage extends the existing GeminiTokenStorage for Anthropic-specific data
|
||||
// It maintains compatibility with the existing auth system while adding Anthropic-specific fields
|
||||
// ClaudeTokenStorage stores OAuth2 token information for Anthropic Claude API authentication.
|
||||
// It maintains compatibility with the existing auth system while adding Claude-specific fields
|
||||
// for managing access tokens, refresh tokens, and user account information.
|
||||
type ClaudeTokenStorage struct {
|
||||
// IDToken is the JWT ID token containing user claims
|
||||
// IDToken is the JWT ID token containing user claims and identity information.
|
||||
IDToken string `json:"id_token"`
|
||||
// AccessToken is the OAuth2 access token for API access
|
||||
|
||||
// AccessToken is the OAuth2 access token used for authenticating API requests.
|
||||
AccessToken string `json:"access_token"`
|
||||
// RefreshToken is used to obtain new access tokens
|
||||
|
||||
// RefreshToken is used to obtain new access tokens when the current one expires.
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
// LastRefresh is the timestamp of the last token refresh
|
||||
|
||||
// LastRefresh is the timestamp of the last token refresh operation.
|
||||
LastRefresh string `json:"last_refresh"`
|
||||
// Email is the Anthropic account email
|
||||
|
||||
// Email is the Anthropic account email address associated with this token.
|
||||
Email string `json:"email"`
|
||||
// Type indicates the type (gemini, chatgpt, claude) of token storage.
|
||||
|
||||
// Type indicates the authentication provider type, always "claude" for this storage.
|
||||
Type string `json:"type"`
|
||||
// Expire is the timestamp of the token expire
|
||||
|
||||
// Expire is the timestamp when the current access token expires.
|
||||
Expire string `json:"expired"`
|
||||
}
|
||||
|
||||
// SaveTokenToFile serializes the token storage to a JSON file.
|
||||
// SaveTokenToFile serializes the Claude token storage to a JSON file.
|
||||
// This method creates the necessary directory structure and writes the token
|
||||
// data in JSON format to the specified file path for persistent storage.
|
||||
//
|
||||
// Parameters:
|
||||
// - authFilePath: The full path where the token file should be saved
|
||||
//
|
||||
// Returns:
|
||||
// - error: An error if the operation fails, nil otherwise
|
||||
func (ts *ClaudeTokenStorage) SaveTokenToFile(authFilePath string) error {
|
||||
ts.Type = "claude"
|
||||
|
||||
// Create directory structure if it doesn't exist
|
||||
if err := os.MkdirAll(path.Dir(authFilePath), 0700); err != nil {
|
||||
return fmt.Errorf("failed to create directory: %v", err)
|
||||
}
|
||||
|
||||
// Create the token file
|
||||
f, err := os.Create(authFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create token file: %w", err)
|
||||
@@ -41,9 +62,9 @@ func (ts *ClaudeTokenStorage) SaveTokenToFile(authFilePath string) error {
|
||||
_ = f.Close()
|
||||
}()
|
||||
|
||||
// Encode and write the token data as JSON
|
||||
if err = json.NewEncoder(f).Encode(ts); err != nil {
|
||||
return fmt.Errorf("failed to write token to file: %w", err)
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
@@ -6,14 +6,19 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// OAuthError represents an OAuth-specific error
|
||||
// OAuthError represents an OAuth-specific error.
|
||||
type OAuthError struct {
|
||||
Code string `json:"error"`
|
||||
// Code is the OAuth error code.
|
||||
Code string `json:"error"`
|
||||
// Description is a human-readable description of the error.
|
||||
Description string `json:"error_description,omitempty"`
|
||||
URI string `json:"error_uri,omitempty"`
|
||||
StatusCode int `json:"-"`
|
||||
// URI is a URI identifying a human-readable web page with information about the error.
|
||||
URI string `json:"error_uri,omitempty"`
|
||||
// StatusCode is the HTTP status code associated with the error.
|
||||
StatusCode int `json:"-"`
|
||||
}
|
||||
|
||||
// Error returns a string representation of the OAuth error.
|
||||
func (e *OAuthError) Error() string {
|
||||
if e.Description != "" {
|
||||
return fmt.Sprintf("OAuth error %s: %s", e.Code, e.Description)
|
||||
@@ -21,7 +26,7 @@ func (e *OAuthError) Error() string {
|
||||
return fmt.Sprintf("OAuth error: %s", e.Code)
|
||||
}
|
||||
|
||||
// NewOAuthError creates a new OAuth error
|
||||
// NewOAuthError creates a new OAuth error with the specified code, description, and status code.
|
||||
func NewOAuthError(code, description string, statusCode int) *OAuthError {
|
||||
return &OAuthError{
|
||||
Code: code,
|
||||
@@ -30,14 +35,19 @@ func NewOAuthError(code, description string, statusCode int) *OAuthError {
|
||||
}
|
||||
}
|
||||
|
||||
// AuthenticationError represents authentication-related errors
|
||||
// AuthenticationError represents authentication-related errors.
|
||||
type AuthenticationError struct {
|
||||
Type string `json:"type"`
|
||||
// Type is the type of authentication error.
|
||||
Type string `json:"type"`
|
||||
// Message is a human-readable message describing the error.
|
||||
Message string `json:"message"`
|
||||
Code int `json:"code"`
|
||||
Cause error `json:"-"`
|
||||
// Code is the HTTP status code associated with the error.
|
||||
Code int `json:"code"`
|
||||
// Cause is the underlying error that caused this authentication error.
|
||||
Cause error `json:"-"`
|
||||
}
|
||||
|
||||
// Error returns a string representation of the authentication error.
|
||||
func (e *AuthenticationError) Error() string {
|
||||
if e.Cause != nil {
|
||||
return fmt.Sprintf("%s: %s (caused by: %v)", e.Type, e.Message, e.Cause)
|
||||
@@ -45,44 +55,50 @@ func (e *AuthenticationError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.Type, e.Message)
|
||||
}
|
||||
|
||||
// Common authentication error types
|
||||
// Common authentication error types.
|
||||
var (
|
||||
ErrTokenExpired = &AuthenticationError{
|
||||
Type: "token_expired",
|
||||
Message: "Access token has expired",
|
||||
Code: http.StatusUnauthorized,
|
||||
}
|
||||
// ErrTokenExpired = &AuthenticationError{
|
||||
// Type: "token_expired",
|
||||
// Message: "Access token has expired",
|
||||
// Code: http.StatusUnauthorized,
|
||||
// }
|
||||
|
||||
// ErrInvalidState represents an error for invalid OAuth state parameter.
|
||||
ErrInvalidState = &AuthenticationError{
|
||||
Type: "invalid_state",
|
||||
Message: "OAuth state parameter is invalid",
|
||||
Code: http.StatusBadRequest,
|
||||
}
|
||||
|
||||
// ErrCodeExchangeFailed represents an error when exchanging authorization code for tokens fails.
|
||||
ErrCodeExchangeFailed = &AuthenticationError{
|
||||
Type: "code_exchange_failed",
|
||||
Message: "Failed to exchange authorization code for tokens",
|
||||
Code: http.StatusBadRequest,
|
||||
}
|
||||
|
||||
// ErrServerStartFailed represents an error when starting the OAuth callback server fails.
|
||||
ErrServerStartFailed = &AuthenticationError{
|
||||
Type: "server_start_failed",
|
||||
Message: "Failed to start OAuth callback server",
|
||||
Code: http.StatusInternalServerError,
|
||||
}
|
||||
|
||||
// ErrPortInUse represents an error when the OAuth callback port is already in use.
|
||||
ErrPortInUse = &AuthenticationError{
|
||||
Type: "port_in_use",
|
||||
Message: "OAuth callback port is already in use",
|
||||
Code: 13, // Special exit code for port-in-use
|
||||
}
|
||||
|
||||
// ErrCallbackTimeout represents an error when waiting for OAuth callback times out.
|
||||
ErrCallbackTimeout = &AuthenticationError{
|
||||
Type: "callback_timeout",
|
||||
Message: "Timeout waiting for OAuth callback",
|
||||
Code: http.StatusRequestTimeout,
|
||||
}
|
||||
|
||||
// ErrBrowserOpenFailed represents an error when opening the browser for authentication fails.
|
||||
ErrBrowserOpenFailed = &AuthenticationError{
|
||||
Type: "browser_open_failed",
|
||||
Message: "Failed to open browser for authentication",
|
||||
@@ -90,7 +106,7 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
// NewAuthenticationError creates a new authentication error with a cause
|
||||
// NewAuthenticationError creates a new authentication error with a cause based on a base error.
|
||||
func NewAuthenticationError(baseErr *AuthenticationError, cause error) *AuthenticationError {
|
||||
return &AuthenticationError{
|
||||
Type: baseErr.Type,
|
||||
@@ -100,21 +116,21 @@ func NewAuthenticationError(baseErr *AuthenticationError, cause error) *Authenti
|
||||
}
|
||||
}
|
||||
|
||||
// IsAuthenticationError checks if an error is an authentication error
|
||||
// IsAuthenticationError checks if an error is an authentication error.
|
||||
func IsAuthenticationError(err error) bool {
|
||||
var authenticationError *AuthenticationError
|
||||
ok := errors.As(err, &authenticationError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsOAuthError checks if an error is an OAuth error
|
||||
// IsOAuthError checks if an error is an OAuth error.
|
||||
func IsOAuthError(err error) bool {
|
||||
var oAuthError *OAuthError
|
||||
ok := errors.As(err, &oAuthError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetUserFriendlyMessage returns a user-friendly error message
|
||||
// GetUserFriendlyMessage returns a user-friendly error message based on the error type.
|
||||
func GetUserFriendlyMessage(err error) string {
|
||||
switch {
|
||||
case IsAuthenticationError(err):
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package codex
|
||||
|
||||
// LoginSuccessHtml is the template for the OAuth success page
|
||||
// LoginSuccessHTML is the HTML template for the page shown after a successful
|
||||
// OAuth2 authentication with Codex. It informs the user that the authentication
|
||||
// was successful and provides a countdown timer to automatically close the window.
|
||||
const LoginSuccessHtml = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -202,7 +204,9 @@ const LoginSuccessHtml = `<!DOCTYPE html>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
// SetupNoticeHtml is the template for the setup notice section
|
||||
// SetupNoticeHTML is the HTML template for the section that provides instructions
|
||||
// for additional setup. This is displayed on the success page when further actions
|
||||
// are required from the user.
|
||||
const SetupNoticeHtml = `
|
||||
<div class="setup-notice">
|
||||
<h3>Additional Setup Required</h3>
|
||||
|
||||
@@ -8,7 +8,9 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// JWTClaims represents the claims section of a JWT token
|
||||
// JWTClaims represents the claims section of a JSON Web Token (JWT).
|
||||
// It includes standard claims like issuer, subject, and expiration time, as well as
|
||||
// custom claims specific to OpenAI's authentication.
|
||||
type JWTClaims struct {
|
||||
AtHash string `json:"at_hash"`
|
||||
Aud []string `json:"aud"`
|
||||
@@ -25,12 +27,18 @@ type JWTClaims struct {
|
||||
Sid string `json:"sid"`
|
||||
Sub string `json:"sub"`
|
||||
}
|
||||
|
||||
// Organizations defines the structure for organization details within the JWT claims.
|
||||
// It holds information about the user's organization, such as ID, role, and title.
|
||||
type Organizations struct {
|
||||
ID string `json:"id"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
Role string `json:"role"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
// CodexAuthInfo contains authentication-related details specific to Codex.
|
||||
// This includes ChatGPT account information, subscription status, and user/organization IDs.
|
||||
type CodexAuthInfo struct {
|
||||
ChatgptAccountID string `json:"chatgpt_account_id"`
|
||||
ChatgptPlanType string `json:"chatgpt_plan_type"`
|
||||
@@ -43,8 +51,10 @@ type CodexAuthInfo struct {
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
// ParseJWTToken parses a JWT token and extracts the claims without verification
|
||||
// This is used for extracting user information from ID tokens
|
||||
// ParseJWTToken parses a JWT token string and extracts its claims without performing
|
||||
// cryptographic signature verification. This is useful for introspecting the token's
|
||||
// contents to retrieve user information from an ID token after it has been validated
|
||||
// by the authentication server.
|
||||
func ParseJWTToken(token string) (*JWTClaims, error) {
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 3 {
|
||||
@@ -65,7 +75,9 @@ func ParseJWTToken(token string) (*JWTClaims, error) {
|
||||
return &claims, nil
|
||||
}
|
||||
|
||||
// base64URLDecode decodes a base64 URL-encoded string with proper padding
|
||||
// base64URLDecode decodes a Base64 URL-encoded string, adding padding if necessary.
|
||||
// JWTs use a URL-safe Base64 alphabet and omit padding, so this function ensures
|
||||
// correct decoding by re-adding the padding before decoding.
|
||||
func base64URLDecode(data string) ([]byte, error) {
|
||||
// Add padding if necessary
|
||||
switch len(data) % 4 {
|
||||
@@ -78,12 +90,13 @@ func base64URLDecode(data string) ([]byte, error) {
|
||||
return base64.URLEncoding.DecodeString(data)
|
||||
}
|
||||
|
||||
// GetUserEmail extracts the user email from JWT claims
|
||||
// GetUserEmail extracts the user's email address from the JWT claims.
|
||||
func (c *JWTClaims) GetUserEmail() string {
|
||||
return c.Email
|
||||
}
|
||||
|
||||
// GetAccountID extracts the user ID from JWT claims (subject)
|
||||
// GetAccountID extracts the user's account ID (subject) from the JWT claims.
|
||||
// It retrieves the unique identifier for the user's ChatGPT account.
|
||||
func (c *JWTClaims) GetAccountID() string {
|
||||
return c.CodexAuthInfo.ChatgptAccountID
|
||||
}
|
||||
|
||||
@@ -13,24 +13,45 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// OAuthServer handles the local HTTP server for OAuth callbacks
|
||||
// OAuthServer handles the local HTTP server for OAuth callbacks.
|
||||
// It listens for the authorization code response from the OAuth provider
|
||||
// and captures the necessary parameters to complete the authentication flow.
|
||||
type OAuthServer struct {
|
||||
server *http.Server
|
||||
port int
|
||||
// server is the underlying HTTP server instance
|
||||
server *http.Server
|
||||
// port is the port number on which the server listens
|
||||
port int
|
||||
// resultChan is a channel for sending OAuth results
|
||||
resultChan chan *OAuthResult
|
||||
errorChan chan error
|
||||
mu sync.Mutex
|
||||
running bool
|
||||
// errorChan is a channel for sending OAuth errors
|
||||
errorChan chan error
|
||||
// mu is a mutex for protecting server state
|
||||
mu sync.Mutex
|
||||
// running indicates whether the server is currently running
|
||||
running bool
|
||||
}
|
||||
|
||||
// OAuthResult contains the result of the OAuth callback
|
||||
// OAuthResult contains the result of the OAuth callback.
|
||||
// It holds either the authorization code and state for successful authentication
|
||||
// or an error message if the authentication failed.
|
||||
type OAuthResult struct {
|
||||
Code string
|
||||
// Code is the authorization code received from the OAuth provider
|
||||
Code string
|
||||
// State is the state parameter used to prevent CSRF attacks
|
||||
State string
|
||||
// Error contains any error message if the OAuth flow failed
|
||||
Error string
|
||||
}
|
||||
|
||||
// NewOAuthServer creates a new OAuth callback server
|
||||
// NewOAuthServer creates a new OAuth callback server.
|
||||
// It initializes the server with the specified port and creates channels
|
||||
// for handling OAuth results and errors.
|
||||
//
|
||||
// Parameters:
|
||||
// - port: The port number on which the server should listen
|
||||
//
|
||||
// Returns:
|
||||
// - *OAuthServer: A new OAuthServer instance
|
||||
func NewOAuthServer(port int) *OAuthServer {
|
||||
return &OAuthServer{
|
||||
port: port,
|
||||
@@ -39,8 +60,13 @@ func NewOAuthServer(port int) *OAuthServer {
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the OAuth callback server
|
||||
func (s *OAuthServer) Start(ctx context.Context) error {
|
||||
// Start starts the OAuth callback server.
|
||||
// It sets up the HTTP handlers for the callback and success endpoints,
|
||||
// and begins listening on the specified port.
|
||||
//
|
||||
// Returns:
|
||||
// - error: An error if the server fails to start
|
||||
func (s *OAuthServer) Start() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
@@ -79,7 +105,14 @@ func (s *OAuthServer) Start(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop gracefully stops the OAuth callback server
|
||||
// Stop gracefully stops the OAuth callback server.
|
||||
// It performs a graceful shutdown of the HTTP server with a timeout.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: The context for controlling the shutdown process
|
||||
//
|
||||
// Returns:
|
||||
// - error: An error if the server fails to stop gracefully
|
||||
func (s *OAuthServer) Stop(ctx context.Context) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -101,7 +134,16 @@ func (s *OAuthServer) Stop(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// WaitForCallback waits for the OAuth callback with a timeout
|
||||
// WaitForCallback waits for the OAuth callback with a timeout.
|
||||
// It blocks until either an OAuth result is received, an error occurs,
|
||||
// or the specified timeout is reached.
|
||||
//
|
||||
// Parameters:
|
||||
// - timeout: The maximum time to wait for the callback
|
||||
//
|
||||
// Returns:
|
||||
// - *OAuthResult: The OAuth result if successful
|
||||
// - error: An error if the callback times out or an error occurs
|
||||
func (s *OAuthServer) WaitForCallback(timeout time.Duration) (*OAuthResult, error) {
|
||||
select {
|
||||
case result := <-s.resultChan:
|
||||
@@ -113,7 +155,13 @@ func (s *OAuthServer) WaitForCallback(timeout time.Duration) (*OAuthResult, erro
|
||||
}
|
||||
}
|
||||
|
||||
// handleCallback handles the OAuth callback endpoint
|
||||
// handleCallback handles the OAuth callback endpoint.
|
||||
// It extracts the authorization code and state from the callback URL,
|
||||
// validates the parameters, and sends the result to the waiting channel.
|
||||
//
|
||||
// Parameters:
|
||||
// - w: The HTTP response writer
|
||||
// - r: The HTTP request
|
||||
func (s *OAuthServer) handleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
log.Debug("Received OAuth callback")
|
||||
|
||||
@@ -171,7 +219,12 @@ func (s *OAuthServer) handleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/success", http.StatusFound)
|
||||
}
|
||||
|
||||
// handleSuccess handles the success page endpoint
|
||||
// handleSuccess handles the success page endpoint.
|
||||
// It serves a user-friendly HTML page indicating that authentication was successful.
|
||||
//
|
||||
// Parameters:
|
||||
// - w: The HTTP response writer
|
||||
// - r: The HTTP request
|
||||
func (s *OAuthServer) handleSuccess(w http.ResponseWriter, r *http.Request) {
|
||||
log.Debug("Serving success page")
|
||||
|
||||
@@ -195,7 +248,16 @@ func (s *OAuthServer) handleSuccess(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// generateSuccessHTML creates the HTML content for the success page
|
||||
// generateSuccessHTML creates the HTML content for the success page.
|
||||
// It customizes the page based on whether additional setup is required
|
||||
// and includes a link to the platform.
|
||||
//
|
||||
// Parameters:
|
||||
// - setupRequired: Whether additional setup is required after authentication
|
||||
// - platformURL: The URL to the platform for additional setup
|
||||
//
|
||||
// Returns:
|
||||
// - string: The HTML content for the success page
|
||||
func (s *OAuthServer) generateSuccessHTML(setupRequired bool, platformURL string) string {
|
||||
html := LoginSuccessHtml
|
||||
|
||||
@@ -213,7 +275,11 @@ func (s *OAuthServer) generateSuccessHTML(setupRequired bool, platformURL string
|
||||
return html
|
||||
}
|
||||
|
||||
// sendResult sends the OAuth result to the waiting channel
|
||||
// sendResult sends the OAuth result to the waiting channel.
|
||||
// It ensures that the result is sent without blocking the handler.
|
||||
//
|
||||
// Parameters:
|
||||
// - result: The OAuth result to send
|
||||
func (s *OAuthServer) sendResult(result *OAuthResult) {
|
||||
select {
|
||||
case s.resultChan <- result:
|
||||
@@ -223,7 +289,11 @@ func (s *OAuthServer) sendResult(result *OAuthResult) {
|
||||
}
|
||||
}
|
||||
|
||||
// isPortAvailable checks if the specified port is available
|
||||
// isPortAvailable checks if the specified port is available.
|
||||
// It attempts to listen on the port to determine availability.
|
||||
//
|
||||
// Returns:
|
||||
// - bool: True if the port is available, false otherwise
|
||||
func (s *OAuthServer) isPortAvailable() bool {
|
||||
addr := fmt.Sprintf(":%d", s.port)
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
@@ -236,7 +306,10 @@ func (s *OAuthServer) isPortAvailable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsRunning returns whether the server is currently running
|
||||
// IsRunning returns whether the server is currently running.
|
||||
//
|
||||
// Returns:
|
||||
// - bool: True if the server is running, false otherwise
|
||||
func (s *OAuthServer) IsRunning() bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package codex
|
||||
|
||||
// PKCECodes holds PKCE verification codes for OAuth2 PKCE flow
|
||||
// PKCECodes holds the verification codes for the OAuth2 PKCE (Proof Key for Code Exchange) flow.
|
||||
// PKCE is an extension to the Authorization Code flow to prevent CSRF and authorization code injection attacks.
|
||||
type PKCECodes struct {
|
||||
// CodeVerifier is the cryptographically random string used to correlate
|
||||
// the authorization request to the token request
|
||||
@@ -9,7 +10,8 @@ type PKCECodes struct {
|
||||
CodeChallenge string `json:"code_challenge"`
|
||||
}
|
||||
|
||||
// CodexTokenData holds OAuth token information from OpenAI
|
||||
// CodexTokenData holds the OAuth token information obtained from OpenAI.
|
||||
// It includes the ID token, access token, refresh token, and associated user details.
|
||||
type CodexTokenData struct {
|
||||
// IDToken is the JWT ID token containing user claims
|
||||
IDToken string `json:"id_token"`
|
||||
@@ -25,7 +27,8 @@ type CodexTokenData struct {
|
||||
Expire string `json:"expired"`
|
||||
}
|
||||
|
||||
// CodexAuthBundle aggregates authentication data after OAuth flow completion
|
||||
// CodexAuthBundle aggregates all authentication-related data after the OAuth flow is complete.
|
||||
// This includes the API key, token data, and the timestamp of the last refresh.
|
||||
type CodexAuthBundle struct {
|
||||
// APIKey is the OpenAI API key obtained from token exchange
|
||||
APIKey string `json:"api_key"`
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Package codex provides authentication and token management for OpenAI's Codex API.
|
||||
// It handles the OAuth2 flow, including generating authorization URLs, exchanging
|
||||
// authorization codes for tokens, and refreshing expired tokens. The package also
|
||||
// defines data structures for storing and managing Codex authentication credentials.
|
||||
package codex
|
||||
|
||||
import (
|
||||
@@ -22,19 +26,24 @@ const (
|
||||
redirectURI = "http://localhost:1455/auth/callback"
|
||||
)
|
||||
|
||||
// CodexAuth handles OpenAI OAuth2 authentication flow
|
||||
// CodexAuth handles the OpenAI OAuth2 authentication flow.
|
||||
// It manages the HTTP client and provides methods for generating authorization URLs,
|
||||
// exchanging authorization codes for tokens, and refreshing access tokens.
|
||||
type CodexAuth struct {
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewCodexAuth creates a new OpenAI authentication service
|
||||
// NewCodexAuth creates a new CodexAuth service instance.
|
||||
// It initializes an HTTP client with proxy settings from the provided configuration.
|
||||
func NewCodexAuth(cfg *config.Config) *CodexAuth {
|
||||
return &CodexAuth{
|
||||
httpClient: util.SetProxy(cfg, &http.Client{}),
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateAuthURL creates the OAuth authorization URL with PKCE
|
||||
// GenerateAuthURL creates the OAuth authorization URL with PKCE (Proof Key for Code Exchange).
|
||||
// It constructs the URL with the necessary parameters, including the client ID,
|
||||
// response type, redirect URI, scopes, and PKCE challenge.
|
||||
func (o *CodexAuth) GenerateAuthURL(state string, pkceCodes *PKCECodes) (string, error) {
|
||||
if pkceCodes == nil {
|
||||
return "", fmt.Errorf("PKCE codes are required")
|
||||
@@ -57,7 +66,9 @@ func (o *CodexAuth) GenerateAuthURL(state string, pkceCodes *PKCECodes) (string,
|
||||
return authURL, nil
|
||||
}
|
||||
|
||||
// ExchangeCodeForTokens exchanges authorization code for access tokens
|
||||
// ExchangeCodeForTokens exchanges an authorization code for access and refresh tokens.
|
||||
// It performs an HTTP POST request to the OpenAI token endpoint with the provided
|
||||
// authorization code and PKCE verifier.
|
||||
func (o *CodexAuth) ExchangeCodeForTokens(ctx context.Context, code string, pkceCodes *PKCECodes) (*CodexAuthBundle, error) {
|
||||
if pkceCodes == nil {
|
||||
return nil, fmt.Errorf("PKCE codes are required for token exchange")
|
||||
@@ -143,7 +154,9 @@ func (o *CodexAuth) ExchangeCodeForTokens(ctx context.Context, code string, pkce
|
||||
return bundle, nil
|
||||
}
|
||||
|
||||
// RefreshTokens refreshes the access token using the refresh token
|
||||
// RefreshTokens refreshes an access token using a refresh token.
|
||||
// This method is called when an access token has expired. It makes a request to the
|
||||
// token endpoint to obtain a new set of tokens.
|
||||
func (o *CodexAuth) RefreshTokens(ctx context.Context, refreshToken string) (*CodexTokenData, error) {
|
||||
if refreshToken == "" {
|
||||
return nil, fmt.Errorf("refresh token is required")
|
||||
@@ -216,7 +229,8 @@ func (o *CodexAuth) RefreshTokens(ctx context.Context, refreshToken string) (*Co
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateTokenStorage creates a new CodexTokenStorage from auth bundle and user info
|
||||
// CreateTokenStorage creates a new CodexTokenStorage from a CodexAuthBundle.
|
||||
// It populates the storage struct with token data, user information, and timestamps.
|
||||
func (o *CodexAuth) CreateTokenStorage(bundle *CodexAuthBundle) *CodexTokenStorage {
|
||||
storage := &CodexTokenStorage{
|
||||
IDToken: bundle.TokenData.IDToken,
|
||||
@@ -231,7 +245,9 @@ func (o *CodexAuth) CreateTokenStorage(bundle *CodexAuthBundle) *CodexTokenStora
|
||||
return storage
|
||||
}
|
||||
|
||||
// RefreshTokensWithRetry refreshes tokens with automatic retry logic
|
||||
// RefreshTokensWithRetry refreshes tokens with a built-in retry mechanism.
|
||||
// It attempts to refresh the tokens up to a specified maximum number of retries,
|
||||
// with an exponential backoff strategy to handle transient network errors.
|
||||
func (o *CodexAuth) RefreshTokensWithRetry(ctx context.Context, refreshToken string, maxRetries int) (*CodexTokenData, error) {
|
||||
var lastErr error
|
||||
|
||||
@@ -257,7 +273,8 @@ func (o *CodexAuth) RefreshTokensWithRetry(ctx context.Context, refreshToken str
|
||||
return nil, fmt.Errorf("token refresh failed after %d attempts: %w", maxRetries, lastErr)
|
||||
}
|
||||
|
||||
// UpdateTokenStorage updates an existing token storage with new token data
|
||||
// UpdateTokenStorage updates an existing CodexTokenStorage with new token data.
|
||||
// This is typically called after a successful token refresh to persist the new credentials.
|
||||
func (o *CodexAuth) UpdateTokenStorage(storage *CodexTokenStorage, tokenData *CodexTokenData) {
|
||||
storage.IDToken = tokenData.IDToken
|
||||
storage.AccessToken = tokenData.AccessToken
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Package codex provides authentication and token management functionality
|
||||
// for OpenAI's Codex AI services. It handles OAuth2 PKCE (Proof Key for Code Exchange)
|
||||
// code generation for secure authentication flows.
|
||||
package codex
|
||||
|
||||
import (
|
||||
@@ -7,8 +10,10 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// GeneratePKCECodes generates a PKCE code verifier and challenge pair
|
||||
// following RFC 7636 specifications for OAuth 2.0 PKCE extension
|
||||
// GeneratePKCECodes generates a new pair of PKCE (Proof Key for Code Exchange) codes.
|
||||
// It creates a cryptographically random code verifier and its corresponding
|
||||
// SHA256 code challenge, as specified in RFC 7636. This is a critical security
|
||||
// feature for the OAuth 2.0 authorization code flow.
|
||||
func GeneratePKCECodes() (*PKCECodes, error) {
|
||||
// Generate code verifier: 43-128 characters, URL-safe
|
||||
codeVerifier, err := generateCodeVerifier()
|
||||
@@ -25,8 +30,10 @@ func GeneratePKCECodes() (*PKCECodes, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateCodeVerifier creates a cryptographically random string
|
||||
// of 128 characters using URL-safe base64 encoding
|
||||
// generateCodeVerifier creates a cryptographically secure random string to be used
|
||||
// as the code verifier in the PKCE flow. The verifier is a high-entropy string
|
||||
// that is later used to prove possession of the client that initiated the
|
||||
// authorization request.
|
||||
func generateCodeVerifier() (string, error) {
|
||||
// Generate 96 random bytes (will result in 128 base64 characters)
|
||||
bytes := make([]byte, 96)
|
||||
@@ -39,8 +46,10 @@ func generateCodeVerifier() (string, error) {
|
||||
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
// generateCodeChallenge creates a SHA256 hash of the code verifier
|
||||
// and encodes it using URL-safe base64 encoding without padding
|
||||
// generateCodeChallenge creates a code challenge from a given code verifier.
|
||||
// The challenge is derived by taking the SHA256 hash of the verifier and then
|
||||
// Base64 URL-encoding the result. This is sent in the initial authorization
|
||||
// request and later verified against the verifier.
|
||||
func generateCodeChallenge(codeVerifier string) string {
|
||||
hash := sha256.Sum256([]byte(codeVerifier))
|
||||
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(hash[:])
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Package codex provides authentication and token management functionality
|
||||
// for OpenAI's Codex AI services. It handles OAuth2 token storage, serialization,
|
||||
// and retrieval for maintaining authenticated sessions with the Codex API.
|
||||
package codex
|
||||
|
||||
import (
|
||||
@@ -7,28 +10,37 @@ import (
|
||||
"path"
|
||||
)
|
||||
|
||||
// CodexTokenStorage extends the existing GeminiTokenStorage for OpenAI-specific data
|
||||
// It maintains compatibility with the existing auth system while adding OpenAI-specific fields
|
||||
// CodexTokenStorage stores OAuth2 token information for OpenAI Codex API authentication.
|
||||
// It maintains compatibility with the existing auth system while adding Codex-specific fields
|
||||
// for managing access tokens, refresh tokens, and user account information.
|
||||
type CodexTokenStorage struct {
|
||||
// IDToken is the JWT ID token containing user claims
|
||||
// IDToken is the JWT ID token containing user claims and identity information.
|
||||
IDToken string `json:"id_token"`
|
||||
// AccessToken is the OAuth2 access token for API access
|
||||
// AccessToken is the OAuth2 access token used for authenticating API requests.
|
||||
AccessToken string `json:"access_token"`
|
||||
// RefreshToken is used to obtain new access tokens
|
||||
// RefreshToken is used to obtain new access tokens when the current one expires.
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
// AccountID is the OpenAI account identifier
|
||||
// AccountID is the OpenAI account identifier associated with this token.
|
||||
AccountID string `json:"account_id"`
|
||||
// LastRefresh is the timestamp of the last token refresh
|
||||
// LastRefresh is the timestamp of the last token refresh operation.
|
||||
LastRefresh string `json:"last_refresh"`
|
||||
// Email is the OpenAI account email
|
||||
// Email is the OpenAI account email address associated with this token.
|
||||
Email string `json:"email"`
|
||||
// Type indicates the type (gemini, chatgpt, claude) of token storage.
|
||||
// Type indicates the authentication provider type, always "codex" for this storage.
|
||||
Type string `json:"type"`
|
||||
// Expire is the timestamp of the token expire
|
||||
// Expire is the timestamp when the current access token expires.
|
||||
Expire string `json:"expired"`
|
||||
}
|
||||
|
||||
// SaveTokenToFile serializes the token storage to a JSON file.
|
||||
// SaveTokenToFile serializes the Codex token storage to a JSON file.
|
||||
// This method creates the necessary directory structure and writes the token
|
||||
// data in JSON format to the specified file path for persistent storage.
|
||||
//
|
||||
// Parameters:
|
||||
// - authFilePath: The full path where the token file should be saved
|
||||
//
|
||||
// Returns:
|
||||
// - error: An error if the operation fails, nil otherwise
|
||||
func (ts *CodexTokenStorage) SaveTokenToFile(authFilePath string) error {
|
||||
ts.Type = "codex"
|
||||
if err := os.MkdirAll(path.Dir(authFilePath), 0700); err != nil {
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
// Package empty provides a no-operation token storage implementation.
|
||||
// This package is used when authentication tokens are not required or when
|
||||
// using API key-based authentication instead of OAuth tokens for any provider.
|
||||
package empty
|
||||
|
||||
// EmptyStorage is a no-operation implementation of the TokenStorage interface.
|
||||
// It provides empty implementations for scenarios where token storage is not needed,
|
||||
// such as when using API keys instead of OAuth tokens for authentication.
|
||||
type EmptyStorage struct {
|
||||
// Type indicates the type (gemini, chatgpt, claude) of token storage.
|
||||
// Type indicates the authentication provider type, always "empty" for this implementation.
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// SaveTokenToFile serializes the token storage to a JSON file.
|
||||
func (ts *EmptyStorage) SaveTokenToFile(authFilePath string) error {
|
||||
// SaveTokenToFile is a no-operation implementation that always succeeds.
|
||||
// This method satisfies the TokenStorage interface but performs no actual file operations
|
||||
// since empty storage doesn't require persistent token data.
|
||||
//
|
||||
// Parameters:
|
||||
// - _: The file path parameter is ignored in this implementation
|
||||
//
|
||||
// Returns:
|
||||
// - error: Always returns nil (no error)
|
||||
func (ts *EmptyStorage) SaveTokenToFile(_ string) error {
|
||||
ts.Type = "empty"
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Package auth provides OAuth2 authentication functionality for Google Cloud APIs.
|
||||
// It handles the complete OAuth2 flow including token storage, web-based authentication,
|
||||
// proxy support, and automatic token refresh. The package supports both SOCKS5 and HTTP/HTTPS proxies.
|
||||
// Package gemini provides authentication and token management functionality
|
||||
// for Google's Gemini AI services. It handles OAuth2 authentication flows,
|
||||
// including obtaining tokens via web-based authorization, storing tokens,
|
||||
// and refreshing them when they expire.
|
||||
package gemini
|
||||
|
||||
import (
|
||||
@@ -38,9 +39,13 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
// GeminiAuth provides methods for handling the Gemini OAuth2 authentication flow.
|
||||
// It encapsulates the logic for obtaining, storing, and refreshing authentication tokens
|
||||
// for Google's Gemini AI services.
|
||||
type GeminiAuth struct {
|
||||
}
|
||||
|
||||
// NewGeminiAuth creates a new instance of GeminiAuth.
|
||||
func NewGeminiAuth() *GeminiAuth {
|
||||
return &GeminiAuth{}
|
||||
}
|
||||
@@ -48,6 +53,16 @@ func NewGeminiAuth() *GeminiAuth {
|
||||
// GetAuthenticatedClient configures and returns an HTTP client ready for making authenticated API calls.
|
||||
// It manages the entire OAuth2 flow, including handling proxies, loading existing tokens,
|
||||
// initiating a new web-based OAuth flow if necessary, and refreshing tokens.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: The context for the HTTP client
|
||||
// - ts: The Gemini token storage containing authentication tokens
|
||||
// - cfg: The configuration containing proxy settings
|
||||
// - noBrowser: Optional parameter to disable browser opening
|
||||
//
|
||||
// Returns:
|
||||
// - *http.Client: An HTTP client configured with authentication
|
||||
// - error: An error if the client configuration fails, nil otherwise
|
||||
func (g *GeminiAuth) GetAuthenticatedClient(ctx context.Context, ts *GeminiTokenStorage, cfg *config.Config, noBrowser ...bool) (*http.Client, error) {
|
||||
// Configure proxy settings for the HTTP client if a proxy URL is provided.
|
||||
proxyURL, err := url.Parse(cfg.ProxyURL)
|
||||
@@ -117,6 +132,16 @@ func (g *GeminiAuth) GetAuthenticatedClient(ctx context.Context, ts *GeminiToken
|
||||
|
||||
// createTokenStorage creates a new GeminiTokenStorage object. It fetches the user's email
|
||||
// using the provided token and populates the storage structure.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: The context for the HTTP request
|
||||
// - config: The OAuth2 configuration
|
||||
// - token: The OAuth2 token to use for authentication
|
||||
// - projectID: The Google Cloud Project ID to associate with this token
|
||||
//
|
||||
// Returns:
|
||||
// - *GeminiTokenStorage: A new token storage object with user information
|
||||
// - error: An error if the token storage creation fails, nil otherwise
|
||||
func (g *GeminiAuth) createTokenStorage(ctx context.Context, config *oauth2.Config, token *oauth2.Token, projectID string) (*GeminiTokenStorage, error) {
|
||||
httpClient := config.Client(ctx, token)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://www.googleapis.com/oauth2/v1/userinfo?alt=json", nil)
|
||||
@@ -174,6 +199,15 @@ func (g *GeminiAuth) createTokenStorage(ctx context.Context, config *oauth2.Conf
|
||||
// It starts a local HTTP server to listen for the callback from Google's auth server,
|
||||
// opens the user's browser to the authorization URL, and exchanges the received
|
||||
// authorization code for an access token.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: The context for the HTTP client
|
||||
// - config: The OAuth2 configuration
|
||||
// - noBrowser: Optional parameter to disable browser opening
|
||||
//
|
||||
// Returns:
|
||||
// - *oauth2.Token: The OAuth2 token obtained from the authorization flow
|
||||
// - error: An error if the token acquisition fails, nil otherwise
|
||||
func (g *GeminiAuth) getTokenFromWeb(ctx context.Context, config *oauth2.Config, noBrowser ...bool) (*oauth2.Token, error) {
|
||||
// Use a channel to pass the authorization code from the HTTP handler to the main function.
|
||||
codeChan := make(chan string)
|
||||
|
||||
@@ -8,11 +8,13 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// GeminiTokenStorage defines the structure for storing OAuth2 token information,
|
||||
// along with associated user and project details. This data is typically
|
||||
// serialized to a JSON file for persistence.
|
||||
// GeminiTokenStorage stores OAuth2 token information for Google Gemini API authentication.
|
||||
// It maintains compatibility with the existing auth system while adding Gemini-specific fields
|
||||
// for managing access tokens, refresh tokens, and user account information.
|
||||
type GeminiTokenStorage struct {
|
||||
// Token holds the raw OAuth2 token data, including access and refresh tokens.
|
||||
Token any `json:"token"`
|
||||
@@ -29,14 +31,13 @@ type GeminiTokenStorage struct {
|
||||
// Checked indicates if the associated Cloud AI API has been verified as enabled.
|
||||
Checked bool `json:"checked"`
|
||||
|
||||
// Type indicates the type (gemini, chatgpt, claude) of token storage.
|
||||
// Type indicates the authentication provider type, always "gemini" for this storage.
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// SaveTokenToFile serializes the token storage to a JSON file.
|
||||
// SaveTokenToFile serializes the Gemini token storage to a JSON file.
|
||||
// This method creates the necessary directory structure and writes the token
|
||||
// data in JSON format to the specified file path. It ensures the file is
|
||||
// properly closed after writing.
|
||||
// data in JSON format to the specified file path for persistent storage.
|
||||
//
|
||||
// Parameters:
|
||||
// - authFilePath: The full path where the token file should be saved
|
||||
@@ -54,7 +55,9 @@ func (ts *GeminiTokenStorage) SaveTokenToFile(authFilePath string) error {
|
||||
return fmt.Errorf("failed to create token file: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
if errClose := f.Close(); errClose != nil {
|
||||
log.Errorf("failed to close file: %v", errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
if err = json.NewEncoder(f).Encode(ts); err != nil {
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
// Package auth provides authentication functionality for various AI service providers.
|
||||
// It includes interfaces and implementations for token storage and authentication methods.
|
||||
package auth
|
||||
|
||||
// TokenStorage defines the interface for storing authentication tokens.
|
||||
// Implementations of this interface should provide methods to persist
|
||||
// authentication tokens to a file system location.
|
||||
type TokenStorage interface {
|
||||
// SaveTokenToFile persists authentication tokens to the specified file path.
|
||||
//
|
||||
// Parameters:
|
||||
// - authFilePath: The file path where the authentication tokens should be saved
|
||||
//
|
||||
// Returns:
|
||||
// - error: An error if the save operation fails, nil otherwise
|
||||
SaveTokenToFile(authFilePath string) error
|
||||
}
|
||||
|
||||
@@ -19,56 +19,77 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// OAuth Configuration
|
||||
// QwenOAuthDeviceCodeEndpoint is the URL for initiating the OAuth 2.0 device authorization flow.
|
||||
QwenOAuthDeviceCodeEndpoint = "https://chat.qwen.ai/api/v1/oauth2/device/code"
|
||||
QwenOAuthTokenEndpoint = "https://chat.qwen.ai/api/v1/oauth2/token"
|
||||
QwenOAuthClientID = "f0304373b74a44d2b584a3fb70ca9e56"
|
||||
QwenOAuthScope = "openid profile email model.completion"
|
||||
QwenOAuthGrantType = "urn:ietf:params:oauth:grant-type:device_code"
|
||||
// QwenOAuthTokenEndpoint is the URL for exchanging device codes or refresh tokens for access tokens.
|
||||
QwenOAuthTokenEndpoint = "https://chat.qwen.ai/api/v1/oauth2/token"
|
||||
// QwenOAuthClientID is the client identifier for the Qwen OAuth 2.0 application.
|
||||
QwenOAuthClientID = "f0304373b74a44d2b584a3fb70ca9e56"
|
||||
// QwenOAuthScope defines the permissions requested by the application.
|
||||
QwenOAuthScope = "openid profile email model.completion"
|
||||
// QwenOAuthGrantType specifies the grant type for the device code flow.
|
||||
QwenOAuthGrantType = "urn:ietf:params:oauth:grant-type:device_code"
|
||||
)
|
||||
|
||||
// QwenTokenData represents OAuth credentials
|
||||
// QwenTokenData represents the OAuth credentials, including access and refresh tokens.
|
||||
type QwenTokenData struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
AccessToken string `json:"access_token"`
|
||||
// RefreshToken is used to obtain a new access token when the current one expires.
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
TokenType string `json:"token_type"`
|
||||
ResourceURL string `json:"resource_url,omitempty"`
|
||||
Expire string `json:"expiry_date,omitempty"`
|
||||
// TokenType indicates the type of token, typically "Bearer".
|
||||
TokenType string `json:"token_type"`
|
||||
// ResourceURL specifies the base URL of the resource server.
|
||||
ResourceURL string `json:"resource_url,omitempty"`
|
||||
// Expire indicates the expiration date and time of the access token.
|
||||
Expire string `json:"expiry_date,omitempty"`
|
||||
}
|
||||
|
||||
// DeviceFlow represents device flow response
|
||||
// DeviceFlow represents the response from the device authorization endpoint.
|
||||
type DeviceFlow struct {
|
||||
DeviceCode string `json:"device_code"`
|
||||
UserCode string `json:"user_code"`
|
||||
VerificationURI string `json:"verification_uri"`
|
||||
// DeviceCode is the code that the client uses to poll for an access token.
|
||||
DeviceCode string `json:"device_code"`
|
||||
// UserCode is the code that the user enters at the verification URI.
|
||||
UserCode string `json:"user_code"`
|
||||
// VerificationURI is the URL where the user can enter the user code to authorize the device.
|
||||
VerificationURI string `json:"verification_uri"`
|
||||
// VerificationURIComplete is a URI that includes the user_code, which can be used to automatically
|
||||
// fill in the code on the verification page.
|
||||
VerificationURIComplete string `json:"verification_uri_complete"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
Interval int `json:"interval"`
|
||||
CodeVerifier string `json:"code_verifier"`
|
||||
// ExpiresIn is the time in seconds until the device_code and user_code expire.
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
// Interval is the minimum time in seconds that the client should wait between polling requests.
|
||||
Interval int `json:"interval"`
|
||||
// CodeVerifier is the cryptographically random string used in the PKCE flow.
|
||||
CodeVerifier string `json:"code_verifier"`
|
||||
}
|
||||
|
||||
// QwenTokenResponse represents token response
|
||||
// QwenTokenResponse represents the successful token response from the token endpoint.
|
||||
type QwenTokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
// AccessToken is the token used to access protected resources.
|
||||
AccessToken string `json:"access_token"`
|
||||
// RefreshToken is used to obtain a new access token.
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
TokenType string `json:"token_type"`
|
||||
ResourceURL string `json:"resource_url,omitempty"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
// TokenType indicates the type of token, typically "Bearer".
|
||||
TokenType string `json:"token_type"`
|
||||
// ResourceURL specifies the base URL of the resource server.
|
||||
ResourceURL string `json:"resource_url,omitempty"`
|
||||
// ExpiresIn is the time in seconds until the access token expires.
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
}
|
||||
|
||||
// QwenAuth manages authentication and credentials
|
||||
// QwenAuth manages authentication and token handling for the Qwen API.
|
||||
type QwenAuth struct {
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewQwenAuth creates a new QwenAuth
|
||||
// NewQwenAuth creates a new QwenAuth instance with a proxy-configured HTTP client.
|
||||
func NewQwenAuth(cfg *config.Config) *QwenAuth {
|
||||
return &QwenAuth{
|
||||
httpClient: util.SetProxy(cfg, &http.Client{}),
|
||||
}
|
||||
}
|
||||
|
||||
// generateCodeVerifier generates a random code verifier for PKCE
|
||||
// generateCodeVerifier generates a cryptographically random string for the PKCE code verifier.
|
||||
func (qa *QwenAuth) generateCodeVerifier() (string, error) {
|
||||
bytes := make([]byte, 32)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
@@ -77,13 +98,13 @@ func (qa *QwenAuth) generateCodeVerifier() (string, error) {
|
||||
return base64.RawURLEncoding.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
// generateCodeChallenge generates a code challenge from a code verifier using SHA-256
|
||||
// generateCodeChallenge creates a SHA-256 hash of the code verifier, used as the PKCE code challenge.
|
||||
func (qa *QwenAuth) generateCodeChallenge(codeVerifier string) string {
|
||||
hash := sha256.Sum256([]byte(codeVerifier))
|
||||
return base64.RawURLEncoding.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// generatePKCEPair generates PKCE code verifier and challenge pair
|
||||
// generatePKCEPair creates a new code verifier and its corresponding code challenge for PKCE.
|
||||
func (qa *QwenAuth) generatePKCEPair() (string, string, error) {
|
||||
codeVerifier, err := qa.generateCodeVerifier()
|
||||
if err != nil {
|
||||
@@ -93,7 +114,7 @@ func (qa *QwenAuth) generatePKCEPair() (string, string, error) {
|
||||
return codeVerifier, codeChallenge, nil
|
||||
}
|
||||
|
||||
// RefreshTokens refreshes the access token using refresh token
|
||||
// RefreshTokens exchanges a refresh token for a new access token.
|
||||
func (qa *QwenAuth) RefreshTokens(ctx context.Context, refreshToken string) (*QwenTokenData, error) {
|
||||
data := url.Values{}
|
||||
data.Set("grant_type", "refresh_token")
|
||||
@@ -145,7 +166,7 @@ func (qa *QwenAuth) RefreshTokens(ctx context.Context, refreshToken string) (*Qw
|
||||
}, nil
|
||||
}
|
||||
|
||||
// InitiateDeviceFlow initiates the OAuth device flow
|
||||
// InitiateDeviceFlow starts the OAuth 2.0 device authorization flow and returns the device flow details.
|
||||
func (qa *QwenAuth) InitiateDeviceFlow(ctx context.Context) (*DeviceFlow, error) {
|
||||
// Generate PKCE code verifier and challenge
|
||||
codeVerifier, codeChallenge, err := qa.generatePKCEPair()
|
||||
@@ -202,7 +223,7 @@ func (qa *QwenAuth) InitiateDeviceFlow(ctx context.Context) (*DeviceFlow, error)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// PollForToken polls for the access token using device code
|
||||
// PollForToken polls the token endpoint with the device code to obtain an access token.
|
||||
func (qa *QwenAuth) PollForToken(deviceCode, codeVerifier string) (*QwenTokenData, error) {
|
||||
pollInterval := 5 * time.Second
|
||||
maxAttempts := 60 // 5 minutes max
|
||||
@@ -267,7 +288,7 @@ func (qa *QwenAuth) PollForToken(deviceCode, codeVerifier string) (*QwenTokenDat
|
||||
// If JSON parsing fails, fall back to text response
|
||||
return nil, fmt.Errorf("device token poll failed: %d %s. Response: %s", resp.StatusCode, resp.Status, string(body))
|
||||
}
|
||||
log.Debugf(string(body))
|
||||
// log.Debugf("%s", string(body))
|
||||
// Success - parse token data
|
||||
var response QwenTokenResponse
|
||||
if err = json.Unmarshal(body, &response); err != nil {
|
||||
@@ -289,7 +310,7 @@ func (qa *QwenAuth) PollForToken(deviceCode, codeVerifier string) (*QwenTokenDat
|
||||
return nil, fmt.Errorf("authentication timeout. Please restart the authentication process")
|
||||
}
|
||||
|
||||
// RefreshTokensWithRetry refreshes tokens with automatic retry logic
|
||||
// RefreshTokensWithRetry attempts to refresh tokens with a specified number of retries upon failure.
|
||||
func (o *QwenAuth) RefreshTokensWithRetry(ctx context.Context, refreshToken string, maxRetries int) (*QwenTokenData, error) {
|
||||
var lastErr error
|
||||
|
||||
@@ -315,6 +336,7 @@ func (o *QwenAuth) RefreshTokensWithRetry(ctx context.Context, refreshToken stri
|
||||
return nil, fmt.Errorf("token refresh failed after %d attempts: %w", maxRetries, lastErr)
|
||||
}
|
||||
|
||||
// CreateTokenStorage creates a QwenTokenStorage object from a QwenTokenData object.
|
||||
func (o *QwenAuth) CreateTokenStorage(tokenData *QwenTokenData) *QwenTokenStorage {
|
||||
storage := &QwenTokenStorage{
|
||||
AccessToken: tokenData.AccessToken,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package gemini provides authentication and token management functionality
|
||||
// for Google's Gemini AI services. It handles OAuth2 token storage, serialization,
|
||||
// and retrieval for maintaining authenticated sessions with the Gemini API.
|
||||
// Package qwen provides authentication and token management functionality
|
||||
// for Alibaba's Qwen AI services. It handles OAuth2 token storage, serialization,
|
||||
// and retrieval for maintaining authenticated sessions with the Qwen API.
|
||||
package qwen
|
||||
|
||||
import (
|
||||
@@ -10,30 +10,29 @@ import (
|
||||
"path"
|
||||
)
|
||||
|
||||
// QwenTokenStorage defines the structure for storing OAuth2 token information,
|
||||
// along with associated user and project details. This data is typically
|
||||
// serialized to a JSON file for persistence.
|
||||
// QwenTokenStorage stores OAuth2 token information for Alibaba Qwen API authentication.
|
||||
// It maintains compatibility with the existing auth system while adding Qwen-specific fields
|
||||
// for managing access tokens, refresh tokens, and user account information.
|
||||
type QwenTokenStorage struct {
|
||||
// AccessToken is the OAuth2 access token for API access
|
||||
// AccessToken is the OAuth2 access token used for authenticating API requests.
|
||||
AccessToken string `json:"access_token"`
|
||||
// RefreshToken is used to obtain new access tokens
|
||||
// RefreshToken is used to obtain new access tokens when the current one expires.
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
// LastRefresh is the timestamp of the last token refresh
|
||||
// LastRefresh is the timestamp of the last token refresh operation.
|
||||
LastRefresh string `json:"last_refresh"`
|
||||
// ResourceURL is the request base url
|
||||
// ResourceURL is the base URL for API requests.
|
||||
ResourceURL string `json:"resource_url"`
|
||||
// Email is the OpenAI account email
|
||||
// Email is the Qwen account email address associated with this token.
|
||||
Email string `json:"email"`
|
||||
// Type indicates the type (gemini, chatgpt, claude) of token storage.
|
||||
// Type indicates the authentication provider type, always "qwen" for this storage.
|
||||
Type string `json:"type"`
|
||||
// Expire is the timestamp of the token expire
|
||||
// Expire is the timestamp when the current access token expires.
|
||||
Expire string `json:"expired"`
|
||||
}
|
||||
|
||||
// SaveTokenToFile serializes the token storage to a JSON file.
|
||||
// SaveTokenToFile serializes the Qwen token storage to a JSON file.
|
||||
// This method creates the necessary directory structure and writes the token
|
||||
// data in JSON format to the specified file path. It ensures the file is
|
||||
// properly closed after writing.
|
||||
// data in JSON format to the specified file path for persistent storage.
|
||||
//
|
||||
// Parameters:
|
||||
// - authFilePath: The full path where the token file should be saved
|
||||
|
||||
Reference in New Issue
Block a user