package oauth import ( "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "fmt" "net/url" "strings" "sync" "time" ) // Claude OAuth Constants (from CRS project) const ( // OAuth Client ID for Claude ClientID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e" // OAuth endpoints AuthorizeURL = "https://claude.ai/oauth/authorize" TokenURL = "https://console.anthropic.com/v1/oauth/token" RedirectURI = "https://console.anthropic.com/oauth/code/callback" // Scopes ScopeProfile = "user:profile" ScopeInference = "user:inference" // Session TTL SessionTTL = 30 * time.Minute ) // OAuthSession stores OAuth flow state type OAuthSession struct { State string `json:"state"` CodeVerifier string `json:"code_verifier"` Scope string `json:"scope"` ProxyURL string `json:"proxy_url,omitempty"` CreatedAt time.Time `json:"created_at"` } // SessionStore manages OAuth sessions in memory type SessionStore struct { mu sync.RWMutex sessions map[string]*OAuthSession } // NewSessionStore creates a new session store func NewSessionStore() *SessionStore { store := &SessionStore{ sessions: make(map[string]*OAuthSession), } // Start cleanup goroutine go store.cleanup() return store } // Set stores a session func (s *SessionStore) Set(sessionID string, session *OAuthSession) { s.mu.Lock() defer s.mu.Unlock() s.sessions[sessionID] = session } // Get retrieves a session func (s *SessionStore) Get(sessionID string) (*OAuthSession, bool) { s.mu.RLock() defer s.mu.RUnlock() session, ok := s.sessions[sessionID] if !ok { return nil, false } // Check if expired if time.Since(session.CreatedAt) > SessionTTL { return nil, false } return session, true } // Delete removes a session func (s *SessionStore) Delete(sessionID string) { s.mu.Lock() defer s.mu.Unlock() delete(s.sessions, sessionID) } // cleanup removes expired sessions periodically func (s *SessionStore) cleanup() { ticker := time.NewTicker(5 * time.Minute) for range ticker.C { s.mu.Lock() for id, session := range s.sessions { if time.Since(session.CreatedAt) > SessionTTL { delete(s.sessions, id) } } s.mu.Unlock() } } // GenerateRandomBytes generates cryptographically secure random bytes func GenerateRandomBytes(n int) ([]byte, error) { b := make([]byte, n) _, err := rand.Read(b) if err != nil { return nil, err } return b, nil } // GenerateState generates a random state string for OAuth func GenerateState() (string, error) { bytes, err := GenerateRandomBytes(32) if err != nil { return "", err } return hex.EncodeToString(bytes), nil } // GenerateSessionID generates a unique session ID func GenerateSessionID() (string, error) { bytes, err := GenerateRandomBytes(16) if err != nil { return "", err } return hex.EncodeToString(bytes), nil } // GenerateCodeVerifier generates a PKCE code verifier (32 bytes -> base64url) func GenerateCodeVerifier() (string, error) { bytes, err := GenerateRandomBytes(32) if err != nil { return "", err } return base64URLEncode(bytes), nil } // GenerateCodeChallenge generates a PKCE code challenge using S256 method func GenerateCodeChallenge(verifier string) string { hash := sha256.Sum256([]byte(verifier)) return base64URLEncode(hash[:]) } // base64URLEncode encodes bytes to base64url without padding func base64URLEncode(data []byte) string { encoded := base64.URLEncoding.EncodeToString(data) // Remove padding return strings.TrimRight(encoded, "=") } // BuildAuthorizationURL builds the OAuth authorization URL func BuildAuthorizationURL(state, codeChallenge, scope string) string { params := url.Values{} params.Set("response_type", "code") params.Set("client_id", ClientID) params.Set("redirect_uri", RedirectURI) params.Set("scope", scope) params.Set("state", state) params.Set("code_challenge", codeChallenge) params.Set("code_challenge_method", "S256") return fmt.Sprintf("%s?%s", AuthorizeURL, params.Encode()) } // TokenRequest represents the token exchange request body type TokenRequest struct { GrantType string `json:"grant_type"` ClientID string `json:"client_id"` Code string `json:"code"` RedirectURI string `json:"redirect_uri"` CodeVerifier string `json:"code_verifier"` State string `json:"state"` } // TokenResponse represents the token response from OAuth provider type TokenResponse struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` ExpiresIn int64 `json:"expires_in"` RefreshToken string `json:"refresh_token,omitempty"` Scope string `json:"scope,omitempty"` // Organization and Account info from OAuth response Organization *OrgInfo `json:"organization,omitempty"` Account *AccountInfo `json:"account,omitempty"` } // OrgInfo represents organization info from OAuth response type OrgInfo struct { UUID string `json:"uuid"` } // AccountInfo represents account info from OAuth response type AccountInfo struct { UUID string `json:"uuid"` } // RefreshTokenRequest represents the refresh token request type RefreshTokenRequest struct { GrantType string `json:"grant_type"` RefreshToken string `json:"refresh_token"` ClientID string `json:"client_id"` } // BuildTokenRequest creates a token exchange request func BuildTokenRequest(code, codeVerifier, state string) *TokenRequest { return &TokenRequest{ GrantType: "authorization_code", ClientID: ClientID, Code: code, RedirectURI: RedirectURI, CodeVerifier: codeVerifier, State: state, } } // BuildRefreshTokenRequest creates a refresh token request func BuildRefreshTokenRequest(refreshToken string) *RefreshTokenRequest { return &RefreshTokenRequest{ GrantType: "refresh_token", RefreshToken: refreshToken, ClientID: ClientID, } }