package service import ( "context" "fmt" "net/http" "os" "strings" "time" "golang.org/x/oauth2" ) const ( // Default OAuth configuration values DefaultAuthorizeURL = "https://claude.ai/oauth/authorize" DefaultTokenURL = "https://console.anthropic.com/v1/oauth/token" DefaultClientID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e" DefaultRedirectURI = "https://console.anthropic.com/oauth/code/callback" DefaultScopes = "user:inference" ) // getOAuthValues returns OAuth configuration values from environment variables or defaults func getOAuthValues() (authorizeURL, tokenURL, clientID, redirectURI, scopes string) { authorizeURL = os.Getenv("CLAUDE_AUTHORIZE_URL") if authorizeURL == "" { authorizeURL = DefaultAuthorizeURL } tokenURL = os.Getenv("CLAUDE_TOKEN_URL") if tokenURL == "" { tokenURL = DefaultTokenURL } clientID = os.Getenv("CLAUDE_CLIENT_ID") if clientID == "" { clientID = DefaultClientID } redirectURI = os.Getenv("CLAUDE_REDIRECT_URI") if redirectURI == "" { redirectURI = DefaultRedirectURI } scopes = os.Getenv("CLAUDE_SCOPES") if scopes == "" { scopes = DefaultScopes } return } type OAuth2Credentials struct { AuthURL string `json:"auth_url"` CodeVerifier string `json:"code_verifier"` State string `json:"state"` CodeChallenge string `json:"code_challenge"` } // GetClaudeOAuthConfig returns the Claude OAuth2 configuration func GetClaudeOAuthConfig() *oauth2.Config { authorizeURL, tokenURL, clientID, redirectURI, scopes := getOAuthValues() return &oauth2.Config{ ClientID: clientID, RedirectURL: redirectURI, Scopes: strings.Split(scopes, " "), Endpoint: oauth2.Endpoint{ AuthURL: authorizeURL, TokenURL: tokenURL, }, } } // getOAuthConfig is kept for backward compatibility func getOAuthConfig() *oauth2.Config { return GetClaudeOAuthConfig() } // GenerateOAuthParams generates OAuth authorization URL and related parameters func GenerateOAuthParams() (*OAuth2Credentials, error) { config := getOAuthConfig() // Generate PKCE parameters codeVerifier := oauth2.GenerateVerifier() state := oauth2.GenerateVerifier() // Reuse generator as state // Generate authorization URL authURL := config.AuthCodeURL(state, oauth2.S256ChallengeOption(codeVerifier), oauth2.SetAuthURLParam("code", "true"), // Claude-specific parameter ) return &OAuth2Credentials{ AuthURL: authURL, CodeVerifier: codeVerifier, State: state, CodeChallenge: oauth2.S256ChallengeFromVerifier(codeVerifier), }, nil } // ExchangeCode func ExchangeCode(authorizationCode, codeVerifier, state string, client *http.Client) (*oauth2.Token, error) { config := getOAuthConfig() if strings.Contains(authorizationCode, "#") { parts := strings.Split(authorizationCode, "#") if len(parts) > 0 { authorizationCode = parts[0] } } ctx := context.Background() if client != nil { ctx = context.WithValue(ctx, oauth2.HTTPClient, client) } token, err := config.Exchange(ctx, authorizationCode, oauth2.VerifierOption(codeVerifier), oauth2.SetAuthURLParam("state", state), ) if err != nil { return nil, fmt.Errorf("token exchange failed: %w", err) } return token, nil } func ParseAuthorizationCode(input string) (string, error) { if input == "" { return "", fmt.Errorf("please provide a valid authorization code") } // URLs are not allowed if strings.Contains(input, "http") || strings.Contains(input, "https") { return "", fmt.Errorf("authorization code cannot contain URLs") } return input, nil } // GetClaudeHTTPClient returns a configured HTTP client for Claude OAuth operations func GetClaudeHTTPClient() *http.Client { return &http.Client{ Timeout: 30 * time.Second, } } // RefreshClaudeToken refreshes a Claude OAuth token using the refresh token func RefreshClaudeToken(accessToken, refreshToken string) (*oauth2.Token, error) { config := GetClaudeOAuthConfig() // Create token from current values currentToken := &oauth2.Token{ AccessToken: accessToken, RefreshToken: refreshToken, TokenType: "Bearer", } ctx := context.Background() if client := GetClaudeHTTPClient(); client != nil { ctx = context.WithValue(ctx, oauth2.HTTPClient, client) } // Refresh the token newToken, err := config.TokenSource(ctx, currentToken).Token() if err != nil { return nil, fmt.Errorf("failed to refresh Claude token: %w", err) } return newToken, nil }