claude_oauth_service.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. package repository
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "net/http"
  7. "net/url"
  8. "strings"
  9. "time"
  10. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  11. "github.com/Wei-Shaw/sub2api/internal/pkg/oauth"
  12. "github.com/Wei-Shaw/sub2api/internal/pkg/proxyurl"
  13. "github.com/Wei-Shaw/sub2api/internal/service"
  14. "github.com/Wei-Shaw/sub2api/internal/util/logredact"
  15. "github.com/imroc/req/v3"
  16. )
  17. func NewClaudeOAuthClient() service.ClaudeOAuthClient {
  18. return &claudeOAuthService{
  19. baseURL: "https://claude.ai",
  20. tokenURL: oauth.TokenURL,
  21. clientFactory: createReqClient,
  22. }
  23. }
  24. type claudeOAuthService struct {
  25. baseURL string
  26. tokenURL string
  27. clientFactory func(proxyURL string) (*req.Client, error)
  28. }
  29. func (s *claudeOAuthService) GetOrganizationUUID(ctx context.Context, sessionKey, proxyURL string) (string, error) {
  30. client, err := s.clientFactory(proxyURL)
  31. if err != nil {
  32. return "", fmt.Errorf("create HTTP client: %w", err)
  33. }
  34. var orgs []struct {
  35. UUID string `json:"uuid"`
  36. Name string `json:"name"`
  37. RavenType *string `json:"raven_type"` // nil for personal, "team" for team organization
  38. }
  39. targetURL := s.baseURL + "/api/organizations"
  40. logger.LegacyPrintf("repository.claude_oauth", "[OAuth] Step 1: Getting organization UUID from %s", targetURL)
  41. resp, err := client.R().
  42. SetContext(ctx).
  43. SetCookies(&http.Cookie{
  44. Name: "sessionKey",
  45. Value: sessionKey,
  46. }).
  47. SetSuccessResult(&orgs).
  48. Get(targetURL)
  49. if err != nil {
  50. logger.LegacyPrintf("repository.claude_oauth", "[OAuth] Step 1 FAILED - Request error: %v", err)
  51. return "", fmt.Errorf("request failed: %w", err)
  52. }
  53. logger.LegacyPrintf("repository.claude_oauth", "[OAuth] Step 1 Response - Status: %d", resp.StatusCode)
  54. if !resp.IsSuccessState() {
  55. return "", fmt.Errorf("failed to get organizations: status %d, body: %s", resp.StatusCode, resp.String())
  56. }
  57. if len(orgs) == 0 {
  58. return "", fmt.Errorf("no organizations found")
  59. }
  60. // 如果只有一个组织,直接使用
  61. if len(orgs) == 1 {
  62. logger.LegacyPrintf("repository.claude_oauth", "[OAuth] Step 1 SUCCESS - Single org found, UUID: %s, Name: %s", orgs[0].UUID, orgs[0].Name)
  63. return orgs[0].UUID, nil
  64. }
  65. // 如果有多个组织,优先选择 raven_type 为 "team" 的组织
  66. for _, org := range orgs {
  67. if org.RavenType != nil && *org.RavenType == "team" {
  68. logger.LegacyPrintf("repository.claude_oauth", "[OAuth] Step 1 SUCCESS - Selected team org, UUID: %s, Name: %s, RavenType: %s",
  69. org.UUID, org.Name, *org.RavenType)
  70. return org.UUID, nil
  71. }
  72. }
  73. // 如果没有 team 类型的组织,使用第一个
  74. logger.LegacyPrintf("repository.claude_oauth", "[OAuth] Step 1 SUCCESS - No team org found, using first org, UUID: %s, Name: %s", orgs[0].UUID, orgs[0].Name)
  75. return orgs[0].UUID, nil
  76. }
  77. func (s *claudeOAuthService) GetAuthorizationCode(ctx context.Context, sessionKey, orgUUID, scope, codeChallenge, state, proxyURL string) (string, error) {
  78. client, err := s.clientFactory(proxyURL)
  79. if err != nil {
  80. return "", fmt.Errorf("create HTTP client: %w", err)
  81. }
  82. authURL := fmt.Sprintf("%s/v1/oauth/%s/authorize", s.baseURL, orgUUID)
  83. reqBody := map[string]any{
  84. "response_type": "code",
  85. "client_id": oauth.ClientID,
  86. "organization_uuid": orgUUID,
  87. "redirect_uri": oauth.RedirectURI,
  88. "scope": scope,
  89. "state": state,
  90. "code_challenge": codeChallenge,
  91. "code_challenge_method": "S256",
  92. }
  93. logger.LegacyPrintf("repository.claude_oauth", "[OAuth] Step 2: Getting authorization code from %s", authURL)
  94. reqBodyJSON, _ := json.Marshal(logredact.RedactMap(reqBody))
  95. logger.LegacyPrintf("repository.claude_oauth", "[OAuth] Step 2 Request Body: %s", string(reqBodyJSON))
  96. var result struct {
  97. RedirectURI string `json:"redirect_uri"`
  98. }
  99. resp, err := client.R().
  100. SetContext(ctx).
  101. SetCookies(&http.Cookie{
  102. Name: "sessionKey",
  103. Value: sessionKey,
  104. }).
  105. SetHeader("Accept", "application/json").
  106. SetHeader("Accept-Language", "en-US,en;q=0.9").
  107. SetHeader("Cache-Control", "no-cache").
  108. SetHeader("Origin", "https://claude.ai").
  109. SetHeader("Referer", "https://claude.ai/new").
  110. SetHeader("Content-Type", "application/json").
  111. SetBody(reqBody).
  112. SetSuccessResult(&result).
  113. Post(authURL)
  114. if err != nil {
  115. logger.LegacyPrintf("repository.claude_oauth", "[OAuth] Step 2 FAILED - Request error: %v", err)
  116. return "", fmt.Errorf("request failed: %w", err)
  117. }
  118. logger.LegacyPrintf("repository.claude_oauth", "[OAuth] Step 2 Response - Status: %d, Body: %s", resp.StatusCode, logredact.RedactJSON(resp.Bytes()))
  119. if !resp.IsSuccessState() {
  120. return "", fmt.Errorf("failed to get authorization code: status %d, body: %s", resp.StatusCode, resp.String())
  121. }
  122. if result.RedirectURI == "" {
  123. return "", fmt.Errorf("no redirect_uri in response")
  124. }
  125. parsedURL, err := url.Parse(result.RedirectURI)
  126. if err != nil {
  127. return "", fmt.Errorf("failed to parse redirect_uri: %w", err)
  128. }
  129. queryParams := parsedURL.Query()
  130. authCode := queryParams.Get("code")
  131. responseState := queryParams.Get("state")
  132. if authCode == "" {
  133. return "", fmt.Errorf("no authorization code in redirect_uri")
  134. }
  135. fullCode := authCode
  136. if responseState != "" {
  137. fullCode = authCode + "#" + responseState
  138. }
  139. logger.LegacyPrintf("repository.claude_oauth", "[OAuth] Step 2 SUCCESS - Got authorization code")
  140. return fullCode, nil
  141. }
  142. func (s *claudeOAuthService) ExchangeCodeForToken(ctx context.Context, code, codeVerifier, state, proxyURL string, isSetupToken bool) (*oauth.TokenResponse, error) {
  143. client, err := s.clientFactory(proxyURL)
  144. if err != nil {
  145. return nil, fmt.Errorf("create HTTP client: %w", err)
  146. }
  147. // Parse code which may contain state in format "authCode#state"
  148. authCode := code
  149. codeState := ""
  150. if idx := strings.Index(code, "#"); idx != -1 {
  151. authCode = code[:idx]
  152. codeState = code[idx+1:]
  153. }
  154. reqBody := map[string]any{
  155. "code": authCode,
  156. "grant_type": "authorization_code",
  157. "client_id": oauth.ClientID,
  158. "redirect_uri": oauth.RedirectURI,
  159. "code_verifier": codeVerifier,
  160. }
  161. if codeState != "" {
  162. reqBody["state"] = codeState
  163. }
  164. logger.LegacyPrintf("repository.claude_oauth", "[OAuth] Step 3: Exchanging code for token at %s", s.tokenURL)
  165. reqBodyJSON, _ := json.Marshal(logredact.RedactMap(reqBody))
  166. logger.LegacyPrintf("repository.claude_oauth", "[OAuth] Step 3 Request Body: %s", string(reqBodyJSON))
  167. var tokenResp oauth.TokenResponse
  168. resp, err := client.R().
  169. SetContext(ctx).
  170. SetHeader("Accept", "application/json, text/plain, */*").
  171. SetHeader("Content-Type", "application/json").
  172. SetHeader("User-Agent", "axios/1.13.6").
  173. SetBody(reqBody).
  174. SetSuccessResult(&tokenResp).
  175. Post(s.tokenURL)
  176. if err != nil {
  177. logger.LegacyPrintf("repository.claude_oauth", "[OAuth] Step 3 FAILED - Request error: %v", err)
  178. return nil, fmt.Errorf("request failed: %w", err)
  179. }
  180. logger.LegacyPrintf("repository.claude_oauth", "[OAuth] Step 3 Response - Status: %d, Body: %s", resp.StatusCode, logredact.RedactJSON(resp.Bytes()))
  181. if !resp.IsSuccessState() {
  182. return nil, fmt.Errorf("token exchange failed: status %d, body: %s", resp.StatusCode, resp.String())
  183. }
  184. logger.LegacyPrintf("repository.claude_oauth", "[OAuth] Step 3 SUCCESS - Got access token")
  185. return &tokenResp, nil
  186. }
  187. func (s *claudeOAuthService) RefreshToken(ctx context.Context, refreshToken, proxyURL string) (*oauth.TokenResponse, error) {
  188. client, err := s.clientFactory(proxyURL)
  189. if err != nil {
  190. return nil, fmt.Errorf("create HTTP client: %w", err)
  191. }
  192. reqBody := map[string]any{
  193. "grant_type": "refresh_token",
  194. "refresh_token": refreshToken,
  195. "client_id": oauth.ClientID,
  196. }
  197. var tokenResp oauth.TokenResponse
  198. resp, err := client.R().
  199. SetContext(ctx).
  200. SetHeader("Accept", "application/json, text/plain, */*").
  201. SetHeader("Content-Type", "application/json").
  202. SetHeader("User-Agent", "axios/1.13.6").
  203. SetBody(reqBody).
  204. SetSuccessResult(&tokenResp).
  205. Post(s.tokenURL)
  206. if err != nil {
  207. return nil, fmt.Errorf("request failed: %w", err)
  208. }
  209. if !resp.IsSuccessState() {
  210. return nil, fmt.Errorf("token refresh failed: status %d, body: %s", resp.StatusCode, resp.String())
  211. }
  212. return &tokenResp, nil
  213. }
  214. func createReqClient(proxyURL string) (*req.Client, error) {
  215. // 禁用 CookieJar,确保每次授权都是干净的会话
  216. client := req.C().
  217. SetTimeout(60 * time.Second).
  218. ImpersonateChrome().
  219. SetCookieJar(nil) // 禁用 CookieJar
  220. trimmed, _, err := proxyurl.Parse(proxyURL)
  221. if err != nil {
  222. return nil, err
  223. }
  224. if trimmed != "" {
  225. client.SetProxyURL(trimmed)
  226. }
  227. return instrumentReqClient(client), nil
  228. }