oauth.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. // Package oauth provides helpers for OAuth flows used by this service.
  2. package oauth
  3. import (
  4. "crypto/rand"
  5. "crypto/sha256"
  6. "encoding/base64"
  7. "encoding/hex"
  8. "fmt"
  9. "net/url"
  10. "strings"
  11. "sync"
  12. "time"
  13. )
  14. // Claude OAuth Constants
  15. const (
  16. // OAuth Client ID for Claude
  17. ClientID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
  18. // OAuth endpoints
  19. AuthorizeURL = "https://claude.com/cai/oauth/authorize"
  20. TokenURL = "https://platform.claude.com/v1/oauth/token"
  21. RedirectURI = "https://platform.claude.com/oauth/code/callback"
  22. // Scopes - Browser URL (includes org:create_api_key for user authorization)
  23. ScopeOAuth = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload"
  24. // Scopes - Internal API call (org:create_api_key not supported in API)
  25. ScopeAPI = "user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload"
  26. // Scopes - Setup token (inference only)
  27. ScopeInference = "user:inference"
  28. // Session TTL
  29. SessionTTL = 30 * time.Minute
  30. )
  31. // OAuthSession stores OAuth flow state
  32. type OAuthSession struct {
  33. State string `json:"state"`
  34. CodeVerifier string `json:"code_verifier"`
  35. Scope string `json:"scope"`
  36. ProxyURL string `json:"proxy_url,omitempty"`
  37. CreatedAt time.Time `json:"created_at"`
  38. }
  39. // SessionStore manages OAuth sessions in memory
  40. type SessionStore struct {
  41. mu sync.RWMutex
  42. sessions map[string]*OAuthSession
  43. stopOnce sync.Once
  44. stopCh chan struct{}
  45. }
  46. // NewSessionStore creates a new session store
  47. func NewSessionStore() *SessionStore {
  48. store := &SessionStore{
  49. sessions: make(map[string]*OAuthSession),
  50. stopCh: make(chan struct{}),
  51. }
  52. go store.cleanup()
  53. return store
  54. }
  55. // Stop stops the cleanup goroutine
  56. func (s *SessionStore) Stop() {
  57. s.stopOnce.Do(func() {
  58. close(s.stopCh)
  59. })
  60. }
  61. // Set stores a session
  62. func (s *SessionStore) Set(sessionID string, session *OAuthSession) {
  63. s.mu.Lock()
  64. defer s.mu.Unlock()
  65. s.sessions[sessionID] = session
  66. }
  67. // Get retrieves a session
  68. func (s *SessionStore) Get(sessionID string) (*OAuthSession, bool) {
  69. s.mu.RLock()
  70. defer s.mu.RUnlock()
  71. session, ok := s.sessions[sessionID]
  72. if !ok {
  73. return nil, false
  74. }
  75. if time.Since(session.CreatedAt) > SessionTTL {
  76. return nil, false
  77. }
  78. return session, true
  79. }
  80. // Delete removes a session
  81. func (s *SessionStore) Delete(sessionID string) {
  82. s.mu.Lock()
  83. defer s.mu.Unlock()
  84. delete(s.sessions, sessionID)
  85. }
  86. // cleanup removes expired sessions periodically
  87. func (s *SessionStore) cleanup() {
  88. ticker := time.NewTicker(5 * time.Minute)
  89. defer ticker.Stop()
  90. for {
  91. select {
  92. case <-s.stopCh:
  93. return
  94. case <-ticker.C:
  95. s.mu.Lock()
  96. for id, session := range s.sessions {
  97. if time.Since(session.CreatedAt) > SessionTTL {
  98. delete(s.sessions, id)
  99. }
  100. }
  101. s.mu.Unlock()
  102. }
  103. }
  104. }
  105. // GenerateRandomBytes generates cryptographically secure random bytes
  106. func GenerateRandomBytes(n int) ([]byte, error) {
  107. b := make([]byte, n)
  108. _, err := rand.Read(b)
  109. if err != nil {
  110. return nil, err
  111. }
  112. return b, nil
  113. }
  114. // GenerateState generates a random state string for OAuth (base64url encoded)
  115. func GenerateState() (string, error) {
  116. bytes, err := GenerateRandomBytes(32)
  117. if err != nil {
  118. return "", err
  119. }
  120. return base64URLEncode(bytes), nil
  121. }
  122. // GenerateSessionID generates a unique session ID
  123. func GenerateSessionID() (string, error) {
  124. bytes, err := GenerateRandomBytes(16)
  125. if err != nil {
  126. return "", err
  127. }
  128. return hex.EncodeToString(bytes), nil
  129. }
  130. // GenerateCodeVerifier generates a PKCE code verifier (RFC 7636).
  131. // Uses 32 random bytes → base64url-no-pad, producing a 43-char verifier.
  132. func GenerateCodeVerifier() (string, error) {
  133. bytes, err := GenerateRandomBytes(32)
  134. if err != nil {
  135. return "", err
  136. }
  137. return base64URLEncode(bytes), nil
  138. }
  139. // GenerateCodeChallenge generates a PKCE code challenge using S256 method
  140. func GenerateCodeChallenge(verifier string) string {
  141. hash := sha256.Sum256([]byte(verifier))
  142. return base64URLEncode(hash[:])
  143. }
  144. // base64URLEncode encodes bytes to base64url without padding
  145. func base64URLEncode(data []byte) string {
  146. encoded := base64.URLEncoding.EncodeToString(data)
  147. return strings.TrimRight(encoded, "=")
  148. }
  149. // BuildAuthorizationURL builds the OAuth authorization URL with correct parameter order
  150. func BuildAuthorizationURL(state, codeChallenge, scope string) string {
  151. encodedRedirectURI := url.QueryEscape(RedirectURI)
  152. encodedScope := strings.ReplaceAll(url.QueryEscape(scope), "%20", "+")
  153. return fmt.Sprintf("%s?code=true&client_id=%s&response_type=code&redirect_uri=%s&scope=%s&code_challenge=%s&code_challenge_method=S256&state=%s",
  154. AuthorizeURL,
  155. ClientID,
  156. encodedRedirectURI,
  157. encodedScope,
  158. codeChallenge,
  159. state,
  160. )
  161. }
  162. // TokenResponse represents the token response from OAuth provider
  163. type TokenResponse struct {
  164. AccessToken string `json:"access_token"`
  165. TokenType string `json:"token_type"`
  166. ExpiresIn int64 `json:"expires_in"`
  167. RefreshToken string `json:"refresh_token,omitempty"`
  168. Scope string `json:"scope,omitempty"`
  169. Organization *OrgInfo `json:"organization,omitempty"`
  170. Account *AccountInfo `json:"account,omitempty"`
  171. }
  172. // OrgInfo represents organization info from OAuth response
  173. type OrgInfo struct {
  174. UUID string `json:"uuid"`
  175. }
  176. // AccountInfo represents account info from OAuth response
  177. type AccountInfo struct {
  178. UUID string `json:"uuid"`
  179. EmailAddress string `json:"email_address"`
  180. }