2025-12-20 11:56:11 +08:00
package repository
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
2025-12-23 17:14:39 +08:00
"strings"
2025-12-20 11:56:11 +08:00
"time"
2026-02-12 19:01:09 +08:00
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
2025-12-24 21:07:21 +08:00
"github.com/Wei-Shaw/sub2api/internal/pkg/oauth"
2026-03-02 15:53:26 +08:00
"github.com/Wei-Shaw/sub2api/internal/pkg/proxyurl"
2025-12-24 21:07:21 +08:00
"github.com/Wei-Shaw/sub2api/internal/service"
2026-01-02 17:40:57 +08:00
"github.com/Wei-Shaw/sub2api/internal/util/logredact"
2025-12-20 11:56:11 +08:00
"github.com/imroc/req/v3"
)
func NewClaudeOAuthClient ( ) service . ClaudeOAuthClient {
2025-12-25 10:52:56 +08:00
return & claudeOAuthService {
baseURL : "https://claude.ai" ,
tokenURL : oauth . TokenURL ,
clientFactory : createReqClient ,
}
}
type claudeOAuthService struct {
baseURL string
tokenURL string
2026-03-02 15:53:26 +08:00
clientFactory func ( proxyURL string ) ( * req . Client , error )
2025-12-20 11:56:11 +08:00
}
func ( s * claudeOAuthService ) GetOrganizationUUID ( ctx context . Context , sessionKey , proxyURL string ) ( string , error ) {
2026-03-02 15:53:26 +08:00
client , err := s . clientFactory ( proxyURL )
if err != nil {
return "" , fmt . Errorf ( "create HTTP client: %w" , err )
}
2025-12-20 11:56:11 +08:00
var orgs [ ] struct {
2026-01-23 16:30:12 +08:00
UUID string ` json:"uuid" `
Name string ` json:"name" `
RavenType * string ` json:"raven_type" ` // nil for personal, "team" for team organization
2025-12-20 11:56:11 +08:00
}
2025-12-25 10:52:56 +08:00
targetURL := s . baseURL + "/api/organizations"
2026-02-12 19:01:09 +08:00
logger . LegacyPrintf ( "repository.claude_oauth" , "[OAuth] Step 1: Getting organization UUID from %s" , targetURL )
2025-12-20 11:56:11 +08:00
resp , err := client . R ( ) .
SetContext ( ctx ) .
SetCookies ( & http . Cookie {
Name : "sessionKey" ,
Value : sessionKey ,
} ) .
SetSuccessResult ( & orgs ) .
Get ( targetURL )
if err != nil {
2026-02-12 19:01:09 +08:00
logger . LegacyPrintf ( "repository.claude_oauth" , "[OAuth] Step 1 FAILED - Request error: %v" , err )
2025-12-20 11:56:11 +08:00
return "" , fmt . Errorf ( "request failed: %w" , err )
}
2026-02-12 19:01:09 +08:00
logger . LegacyPrintf ( "repository.claude_oauth" , "[OAuth] Step 1 Response - Status: %d" , resp . StatusCode )
2025-12-20 11:56:11 +08:00
if ! resp . IsSuccessState ( ) {
return "" , fmt . Errorf ( "failed to get organizations: status %d, body: %s" , resp . StatusCode , resp . String ( ) )
}
if len ( orgs ) == 0 {
return "" , fmt . Errorf ( "no organizations found" )
}
2026-01-23 16:30:12 +08:00
// 如果只有一个组织,直接使用
if len ( orgs ) == 1 {
2026-02-12 19:01:09 +08:00
logger . LegacyPrintf ( "repository.claude_oauth" , "[OAuth] Step 1 SUCCESS - Single org found, UUID: %s, Name: %s" , orgs [ 0 ] . UUID , orgs [ 0 ] . Name )
2026-01-23 16:30:12 +08:00
return orgs [ 0 ] . UUID , nil
}
// 如果有多个组织,优先选择 raven_type 为 "team" 的组织
for _ , org := range orgs {
if org . RavenType != nil && * org . RavenType == "team" {
2026-02-12 19:01:09 +08:00
logger . LegacyPrintf ( "repository.claude_oauth" , "[OAuth] Step 1 SUCCESS - Selected team org, UUID: %s, Name: %s, RavenType: %s" ,
2026-01-23 16:30:12 +08:00
org . UUID , org . Name , * org . RavenType )
return org . UUID , nil
}
}
// 如果没有 team 类型的组织,使用第一个
2026-02-12 19:01:09 +08:00
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 )
2025-12-20 11:56:11 +08:00
return orgs [ 0 ] . UUID , nil
}
func ( s * claudeOAuthService ) GetAuthorizationCode ( ctx context . Context , sessionKey , orgUUID , scope , codeChallenge , state , proxyURL string ) ( string , error ) {
2026-03-02 15:53:26 +08:00
client , err := s . clientFactory ( proxyURL )
if err != nil {
return "" , fmt . Errorf ( "create HTTP client: %w" , err )
}
2025-12-20 11:56:11 +08:00
2025-12-25 10:52:56 +08:00
authURL := fmt . Sprintf ( "%s/v1/oauth/%s/authorize" , s . baseURL , orgUUID )
2025-12-20 11:56:11 +08:00
2025-12-20 16:19:40 +08:00
reqBody := map [ string ] any {
2025-12-20 11:56:11 +08:00
"response_type" : "code" ,
"client_id" : oauth . ClientID ,
"organization_uuid" : orgUUID ,
"redirect_uri" : oauth . RedirectURI ,
"scope" : scope ,
"state" : state ,
"code_challenge" : codeChallenge ,
"code_challenge_method" : "S256" ,
}
2026-02-12 19:01:09 +08:00
logger . LegacyPrintf ( "repository.claude_oauth" , "[OAuth] Step 2: Getting authorization code from %s" , authURL )
2026-01-02 17:40:57 +08:00
reqBodyJSON , _ := json . Marshal ( logredact . RedactMap ( reqBody ) )
2026-02-12 19:01:09 +08:00
logger . LegacyPrintf ( "repository.claude_oauth" , "[OAuth] Step 2 Request Body: %s" , string ( reqBodyJSON ) )
2025-12-20 11:56:11 +08:00
var result struct {
RedirectURI string ` json:"redirect_uri" `
}
resp , err := client . R ( ) .
SetContext ( ctx ) .
SetCookies ( & http . Cookie {
Name : "sessionKey" ,
Value : sessionKey ,
} ) .
SetHeader ( "Accept" , "application/json" ) .
SetHeader ( "Accept-Language" , "en-US,en;q=0.9" ) .
SetHeader ( "Cache-Control" , "no-cache" ) .
SetHeader ( "Origin" , "https://claude.ai" ) .
SetHeader ( "Referer" , "https://claude.ai/new" ) .
SetHeader ( "Content-Type" , "application/json" ) .
SetBody ( reqBody ) .
SetSuccessResult ( & result ) .
Post ( authURL )
if err != nil {
2026-02-12 19:01:09 +08:00
logger . LegacyPrintf ( "repository.claude_oauth" , "[OAuth] Step 2 FAILED - Request error: %v" , err )
2025-12-20 11:56:11 +08:00
return "" , fmt . Errorf ( "request failed: %w" , err )
}
2026-02-12 19:01:09 +08:00
logger . LegacyPrintf ( "repository.claude_oauth" , "[OAuth] Step 2 Response - Status: %d, Body: %s" , resp . StatusCode , logredact . RedactJSON ( resp . Bytes ( ) ) )
2025-12-20 11:56:11 +08:00
if ! resp . IsSuccessState ( ) {
return "" , fmt . Errorf ( "failed to get authorization code: status %d, body: %s" , resp . StatusCode , resp . String ( ) )
}
if result . RedirectURI == "" {
return "" , fmt . Errorf ( "no redirect_uri in response" )
}
parsedURL , err := url . Parse ( result . RedirectURI )
if err != nil {
return "" , fmt . Errorf ( "failed to parse redirect_uri: %w" , err )
}
queryParams := parsedURL . Query ( )
authCode := queryParams . Get ( "code" )
responseState := queryParams . Get ( "state" )
if authCode == "" {
return "" , fmt . Errorf ( "no authorization code in redirect_uri" )
}
fullCode := authCode
if responseState != "" {
fullCode = authCode + "#" + responseState
}
2026-02-12 19:01:09 +08:00
logger . LegacyPrintf ( "repository.claude_oauth" , "[OAuth] Step 2 SUCCESS - Got authorization code" )
2025-12-20 11:56:11 +08:00
return fullCode , nil
}
2025-12-27 20:42:00 +08:00
func ( s * claudeOAuthService ) ExchangeCodeForToken ( ctx context . Context , code , codeVerifier , state , proxyURL string , isSetupToken bool ) ( * oauth . TokenResponse , error ) {
2026-03-02 15:53:26 +08:00
client , err := s . clientFactory ( proxyURL )
if err != nil {
return nil , fmt . Errorf ( "create HTTP client: %w" , err )
}
2025-12-20 11:56:11 +08:00
2025-12-23 17:14:39 +08:00
// Parse code which may contain state in format "authCode#state"
2025-12-20 11:56:11 +08:00
authCode := code
codeState := ""
2025-12-23 17:14:39 +08:00
if idx := strings . Index ( code , "#" ) ; idx != - 1 {
authCode = code [ : idx ]
codeState = code [ idx + 1 : ]
2025-12-20 11:56:11 +08:00
}
2025-12-20 16:19:40 +08:00
reqBody := map [ string ] any {
2025-12-20 11:56:11 +08:00
"code" : authCode ,
"grant_type" : "authorization_code" ,
"client_id" : oauth . ClientID ,
"redirect_uri" : oauth . RedirectURI ,
"code_verifier" : codeVerifier ,
}
if codeState != "" {
reqBody [ "state" ] = codeState
}
2025-12-27 20:42:00 +08:00
// Setup token requires longer expiration (1 year)
if isSetupToken {
reqBody [ "expires_in" ] = 31536000 // 365 * 24 * 60 * 60 seconds
}
2026-02-12 19:01:09 +08:00
logger . LegacyPrintf ( "repository.claude_oauth" , "[OAuth] Step 3: Exchanging code for token at %s" , s . tokenURL )
2026-01-02 17:40:57 +08:00
reqBodyJSON , _ := json . Marshal ( logredact . RedactMap ( reqBody ) )
2026-02-12 19:01:09 +08:00
logger . LegacyPrintf ( "repository.claude_oauth" , "[OAuth] Step 3 Request Body: %s" , string ( reqBodyJSON ) )
2025-12-20 11:56:11 +08:00
var tokenResp oauth . TokenResponse
resp , err := client . R ( ) .
SetContext ( ctx ) .
2026-01-19 16:40:06 +08:00
SetHeader ( "Accept" , "application/json, text/plain, */*" ) .
2025-12-20 11:56:11 +08:00
SetHeader ( "Content-Type" , "application/json" ) .
2026-01-19 16:40:06 +08:00
SetHeader ( "User-Agent" , "axios/1.8.4" ) .
2025-12-20 11:56:11 +08:00
SetBody ( reqBody ) .
SetSuccessResult ( & tokenResp ) .
2025-12-25 10:52:56 +08:00
Post ( s . tokenURL )
2025-12-20 11:56:11 +08:00
if err != nil {
2026-02-12 19:01:09 +08:00
logger . LegacyPrintf ( "repository.claude_oauth" , "[OAuth] Step 3 FAILED - Request error: %v" , err )
2025-12-20 11:56:11 +08:00
return nil , fmt . Errorf ( "request failed: %w" , err )
}
2026-02-12 19:01:09 +08:00
logger . LegacyPrintf ( "repository.claude_oauth" , "[OAuth] Step 3 Response - Status: %d, Body: %s" , resp . StatusCode , logredact . RedactJSON ( resp . Bytes ( ) ) )
2025-12-20 11:56:11 +08:00
if ! resp . IsSuccessState ( ) {
return nil , fmt . Errorf ( "token exchange failed: status %d, body: %s" , resp . StatusCode , resp . String ( ) )
}
2026-02-12 19:01:09 +08:00
logger . LegacyPrintf ( "repository.claude_oauth" , "[OAuth] Step 3 SUCCESS - Got access token" )
2025-12-20 11:56:11 +08:00
return & tokenResp , nil
}
func ( s * claudeOAuthService ) RefreshToken ( ctx context . Context , refreshToken , proxyURL string ) ( * oauth . TokenResponse , error ) {
2026-03-02 15:53:26 +08:00
client , err := s . clientFactory ( proxyURL )
if err != nil {
return nil , fmt . Errorf ( "create HTTP client: %w" , err )
}
2025-12-20 11:56:11 +08:00
2025-12-27 13:50:35 +08:00
reqBody := map [ string ] any {
"grant_type" : "refresh_token" ,
"refresh_token" : refreshToken ,
"client_id" : oauth . ClientID ,
}
2025-12-20 11:56:11 +08:00
var tokenResp oauth . TokenResponse
resp , err := client . R ( ) .
SetContext ( ctx ) .
2026-01-19 16:40:06 +08:00
SetHeader ( "Accept" , "application/json, text/plain, */*" ) .
2025-12-27 13:50:35 +08:00
SetHeader ( "Content-Type" , "application/json" ) .
2026-01-19 16:40:06 +08:00
SetHeader ( "User-Agent" , "axios/1.8.4" ) .
2025-12-27 13:50:35 +08:00
SetBody ( reqBody ) .
2025-12-20 11:56:11 +08:00
SetSuccessResult ( & tokenResp ) .
2025-12-25 10:52:56 +08:00
Post ( s . tokenURL )
2025-12-20 11:56:11 +08:00
if err != nil {
return nil , fmt . Errorf ( "request failed: %w" , err )
}
if ! resp . IsSuccessState ( ) {
return nil , fmt . Errorf ( "token refresh failed: status %d, body: %s" , resp . StatusCode , resp . String ( ) )
}
return & tokenResp , nil
}
2026-03-02 15:53:26 +08:00
func createReqClient ( proxyURL string ) ( * req . Client , error ) {
2026-01-04 14:20:17 +08:00
// 禁用 CookieJar, 确保每次授权都是干净的会话
client := req . C ( ) .
SetTimeout ( 60 * time . Second ) .
ImpersonateChrome ( ) .
SetCookieJar ( nil ) // 禁用 CookieJar
2026-03-02 15:53:26 +08:00
trimmed , _ , err := proxyurl . Parse ( proxyURL )
if err != nil {
return nil , err
}
if trimmed != "" {
client . SetProxyURL ( trimmed )
2026-01-04 14:20:17 +08:00
}
2026-03-02 15:53:26 +08:00
return client , nil
2025-12-20 11:56:11 +08:00
}