claude_oauth.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. package service
  2. import (
  3. "context"
  4. "fmt"
  5. "net/http"
  6. "os"
  7. "strings"
  8. "time"
  9. "golang.org/x/oauth2"
  10. )
  11. const (
  12. // Default OAuth configuration values
  13. DefaultAuthorizeURL = "https://claude.ai/oauth/authorize"
  14. DefaultTokenURL = "https://console.anthropic.com/v1/oauth/token"
  15. DefaultClientID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
  16. DefaultRedirectURI = "https://console.anthropic.com/oauth/code/callback"
  17. DefaultScopes = "user:inference"
  18. )
  19. // getOAuthValues returns OAuth configuration values from environment variables or defaults
  20. func getOAuthValues() (authorizeURL, tokenURL, clientID, redirectURI, scopes string) {
  21. authorizeURL = os.Getenv("CLAUDE_AUTHORIZE_URL")
  22. if authorizeURL == "" {
  23. authorizeURL = DefaultAuthorizeURL
  24. }
  25. tokenURL = os.Getenv("CLAUDE_TOKEN_URL")
  26. if tokenURL == "" {
  27. tokenURL = DefaultTokenURL
  28. }
  29. clientID = os.Getenv("CLAUDE_CLIENT_ID")
  30. if clientID == "" {
  31. clientID = DefaultClientID
  32. }
  33. redirectURI = os.Getenv("CLAUDE_REDIRECT_URI")
  34. if redirectURI == "" {
  35. redirectURI = DefaultRedirectURI
  36. }
  37. scopes = os.Getenv("CLAUDE_SCOPES")
  38. if scopes == "" {
  39. scopes = DefaultScopes
  40. }
  41. return
  42. }
  43. type OAuth2Credentials struct {
  44. AuthURL string `json:"auth_url"`
  45. CodeVerifier string `json:"code_verifier"`
  46. State string `json:"state"`
  47. CodeChallenge string `json:"code_challenge"`
  48. }
  49. // GetClaudeOAuthConfig returns the Claude OAuth2 configuration
  50. func GetClaudeOAuthConfig() *oauth2.Config {
  51. authorizeURL, tokenURL, clientID, redirectURI, scopes := getOAuthValues()
  52. return &oauth2.Config{
  53. ClientID: clientID,
  54. RedirectURL: redirectURI,
  55. Scopes: strings.Split(scopes, " "),
  56. Endpoint: oauth2.Endpoint{
  57. AuthURL: authorizeURL,
  58. TokenURL: tokenURL,
  59. },
  60. }
  61. }
  62. // getOAuthConfig is kept for backward compatibility
  63. func getOAuthConfig() *oauth2.Config {
  64. return GetClaudeOAuthConfig()
  65. }
  66. // GenerateOAuthParams generates OAuth authorization URL and related parameters
  67. func GenerateOAuthParams() (*OAuth2Credentials, error) {
  68. config := getOAuthConfig()
  69. // Generate PKCE parameters
  70. codeVerifier := oauth2.GenerateVerifier()
  71. state := oauth2.GenerateVerifier() // Reuse generator as state
  72. // Generate authorization URL
  73. authURL := config.AuthCodeURL(state,
  74. oauth2.S256ChallengeOption(codeVerifier),
  75. oauth2.SetAuthURLParam("code", "true"), // Claude-specific parameter
  76. )
  77. return &OAuth2Credentials{
  78. AuthURL: authURL,
  79. CodeVerifier: codeVerifier,
  80. State: state,
  81. CodeChallenge: oauth2.S256ChallengeFromVerifier(codeVerifier),
  82. }, nil
  83. }
  84. // ExchangeCode
  85. func ExchangeCode(authorizationCode, codeVerifier, state string, client *http.Client) (*oauth2.Token, error) {
  86. config := getOAuthConfig()
  87. ctx := context.Background()
  88. if client != nil {
  89. ctx = context.WithValue(ctx, oauth2.HTTPClient, client)
  90. }
  91. token, err := config.Exchange(ctx, authorizationCode,
  92. oauth2.VerifierOption(codeVerifier),
  93. oauth2.SetAuthURLParam("state", state),
  94. )
  95. if err != nil {
  96. return nil, fmt.Errorf("token exchange failed: %w", err)
  97. }
  98. return token, nil
  99. }
  100. func ParseAuthorizationCode(input string) (string, error) {
  101. if input == "" {
  102. return "", fmt.Errorf("please provide a valid authorization code")
  103. }
  104. // URLs are not allowed
  105. if strings.Contains(input, "http") || strings.Contains(input, "https") {
  106. return "", fmt.Errorf("authorization code cannot contain URLs")
  107. }
  108. return input, nil
  109. }
  110. // GetClaudeHTTPClient returns a configured HTTP client for Claude OAuth operations
  111. func GetClaudeHTTPClient() *http.Client {
  112. return &http.Client{
  113. Timeout: 30 * time.Second,
  114. }
  115. }
  116. // RefreshClaudeToken refreshes a Claude OAuth token using the refresh token
  117. func RefreshClaudeToken(accessToken, refreshToken string) (*oauth2.Token, error) {
  118. config := GetClaudeOAuthConfig()
  119. // Create token from current values
  120. currentToken := &oauth2.Token{
  121. AccessToken: accessToken,
  122. RefreshToken: refreshToken,
  123. TokenType: "Bearer",
  124. }
  125. ctx := context.Background()
  126. if client := GetClaudeHTTPClient(); client != nil {
  127. ctx = context.WithValue(ctx, oauth2.HTTPClient, client)
  128. }
  129. // Refresh the token
  130. newToken, err := config.TokenSource(ctx, currentToken).Token()
  131. if err != nil {
  132. return nil, fmt.Errorf("failed to refresh Claude token: %w", err)
  133. }
  134. return newToken, nil
  135. }