codex_oauth.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. package service
  2. import (
  3. "context"
  4. "crypto/rand"
  5. "crypto/sha256"
  6. "encoding/base64"
  7. "encoding/json"
  8. "errors"
  9. "fmt"
  10. "net/http"
  11. "net/url"
  12. "strings"
  13. "time"
  14. "github.com/QuantumNous/new-api/common"
  15. )
  16. const (
  17. codexOAuthClientID = "app_EMoamEEZ73f0CkXaXp7hrann"
  18. codexOAuthAuthorizeURL = "https://auth.openai.com/oauth/authorize"
  19. codexOAuthTokenURL = "https://auth.openai.com/oauth/token"
  20. codexOAuthRedirectURI = "http://localhost:1455/auth/callback"
  21. codexOAuthScope = "openid profile email offline_access"
  22. codexJWTClaimPath = "https://api.openai.com/auth"
  23. defaultHTTPTimeout = 20 * time.Second
  24. )
  25. type CodexOAuthTokenResult struct {
  26. AccessToken string
  27. RefreshToken string
  28. ExpiresAt time.Time
  29. }
  30. type CodexOAuthAuthorizationFlow struct {
  31. State string
  32. Verifier string
  33. Challenge string
  34. AuthorizeURL string
  35. }
  36. func RefreshCodexOAuthToken(ctx context.Context, refreshToken string) (*CodexOAuthTokenResult, error) {
  37. return RefreshCodexOAuthTokenWithProxy(ctx, refreshToken, "")
  38. }
  39. func RefreshCodexOAuthTokenWithProxy(ctx context.Context, refreshToken string, proxyURL string) (*CodexOAuthTokenResult, error) {
  40. client, err := getCodexOAuthHTTPClient(proxyURL)
  41. if err != nil {
  42. return nil, err
  43. }
  44. return refreshCodexOAuthToken(ctx, client, codexOAuthTokenURL, codexOAuthClientID, refreshToken)
  45. }
  46. func ExchangeCodexAuthorizationCode(ctx context.Context, code string, verifier string) (*CodexOAuthTokenResult, error) {
  47. return ExchangeCodexAuthorizationCodeWithProxy(ctx, code, verifier, "")
  48. }
  49. func ExchangeCodexAuthorizationCodeWithProxy(ctx context.Context, code string, verifier string, proxyURL string) (*CodexOAuthTokenResult, error) {
  50. client, err := getCodexOAuthHTTPClient(proxyURL)
  51. if err != nil {
  52. return nil, err
  53. }
  54. return exchangeCodexAuthorizationCode(ctx, client, codexOAuthTokenURL, codexOAuthClientID, code, verifier, codexOAuthRedirectURI)
  55. }
  56. func CreateCodexOAuthAuthorizationFlow() (*CodexOAuthAuthorizationFlow, error) {
  57. state, err := createStateHex(16)
  58. if err != nil {
  59. return nil, err
  60. }
  61. verifier, challenge, err := generatePKCEPair()
  62. if err != nil {
  63. return nil, err
  64. }
  65. u, err := buildCodexAuthorizeURL(state, challenge)
  66. if err != nil {
  67. return nil, err
  68. }
  69. return &CodexOAuthAuthorizationFlow{
  70. State: state,
  71. Verifier: verifier,
  72. Challenge: challenge,
  73. AuthorizeURL: u,
  74. }, nil
  75. }
  76. func refreshCodexOAuthToken(
  77. ctx context.Context,
  78. client *http.Client,
  79. tokenURL string,
  80. clientID string,
  81. refreshToken string,
  82. ) (*CodexOAuthTokenResult, error) {
  83. rt := strings.TrimSpace(refreshToken)
  84. if rt == "" {
  85. return nil, errors.New("empty refresh_token")
  86. }
  87. form := url.Values{}
  88. form.Set("grant_type", "refresh_token")
  89. form.Set("refresh_token", rt)
  90. form.Set("client_id", clientID)
  91. req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(form.Encode()))
  92. if err != nil {
  93. return nil, err
  94. }
  95. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  96. req.Header.Set("Accept", "application/json")
  97. resp, err := client.Do(req)
  98. if err != nil {
  99. return nil, err
  100. }
  101. defer resp.Body.Close()
  102. var payload struct {
  103. AccessToken string `json:"access_token"`
  104. RefreshToken string `json:"refresh_token"`
  105. ExpiresIn int `json:"expires_in"`
  106. }
  107. if err := common.DecodeJson(resp.Body, &payload); err != nil {
  108. return nil, err
  109. }
  110. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  111. return nil, fmt.Errorf("codex oauth refresh failed: status=%d", resp.StatusCode)
  112. }
  113. if strings.TrimSpace(payload.AccessToken) == "" || strings.TrimSpace(payload.RefreshToken) == "" || payload.ExpiresIn <= 0 {
  114. return nil, errors.New("codex oauth refresh response missing fields")
  115. }
  116. return &CodexOAuthTokenResult{
  117. AccessToken: strings.TrimSpace(payload.AccessToken),
  118. RefreshToken: strings.TrimSpace(payload.RefreshToken),
  119. ExpiresAt: time.Now().Add(time.Duration(payload.ExpiresIn) * time.Second),
  120. }, nil
  121. }
  122. func exchangeCodexAuthorizationCode(
  123. ctx context.Context,
  124. client *http.Client,
  125. tokenURL string,
  126. clientID string,
  127. code string,
  128. verifier string,
  129. redirectURI string,
  130. ) (*CodexOAuthTokenResult, error) {
  131. c := strings.TrimSpace(code)
  132. v := strings.TrimSpace(verifier)
  133. if c == "" {
  134. return nil, errors.New("empty authorization code")
  135. }
  136. if v == "" {
  137. return nil, errors.New("empty code_verifier")
  138. }
  139. form := url.Values{}
  140. form.Set("grant_type", "authorization_code")
  141. form.Set("client_id", clientID)
  142. form.Set("code", c)
  143. form.Set("code_verifier", v)
  144. form.Set("redirect_uri", redirectURI)
  145. req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(form.Encode()))
  146. if err != nil {
  147. return nil, err
  148. }
  149. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  150. req.Header.Set("Accept", "application/json")
  151. resp, err := client.Do(req)
  152. if err != nil {
  153. return nil, err
  154. }
  155. defer resp.Body.Close()
  156. var payload struct {
  157. AccessToken string `json:"access_token"`
  158. RefreshToken string `json:"refresh_token"`
  159. ExpiresIn int `json:"expires_in"`
  160. }
  161. if err := common.DecodeJson(resp.Body, &payload); err != nil {
  162. return nil, err
  163. }
  164. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  165. return nil, fmt.Errorf("codex oauth code exchange failed: status=%d", resp.StatusCode)
  166. }
  167. if strings.TrimSpace(payload.AccessToken) == "" || strings.TrimSpace(payload.RefreshToken) == "" || payload.ExpiresIn <= 0 {
  168. return nil, errors.New("codex oauth token response missing fields")
  169. }
  170. return &CodexOAuthTokenResult{
  171. AccessToken: strings.TrimSpace(payload.AccessToken),
  172. RefreshToken: strings.TrimSpace(payload.RefreshToken),
  173. ExpiresAt: time.Now().Add(time.Duration(payload.ExpiresIn) * time.Second),
  174. }, nil
  175. }
  176. func getCodexOAuthHTTPClient(proxyURL string) (*http.Client, error) {
  177. baseClient, err := GetHttpClientWithProxy(strings.TrimSpace(proxyURL))
  178. if err != nil {
  179. return nil, err
  180. }
  181. if baseClient == nil {
  182. return &http.Client{Timeout: defaultHTTPTimeout}, nil
  183. }
  184. clientCopy := *baseClient
  185. clientCopy.Timeout = defaultHTTPTimeout
  186. return &clientCopy, nil
  187. }
  188. func buildCodexAuthorizeURL(state string, challenge string) (string, error) {
  189. u, err := url.Parse(codexOAuthAuthorizeURL)
  190. if err != nil {
  191. return "", err
  192. }
  193. q := u.Query()
  194. q.Set("response_type", "code")
  195. q.Set("client_id", codexOAuthClientID)
  196. q.Set("redirect_uri", codexOAuthRedirectURI)
  197. q.Set("scope", codexOAuthScope)
  198. q.Set("code_challenge", challenge)
  199. q.Set("code_challenge_method", "S256")
  200. q.Set("state", state)
  201. q.Set("id_token_add_organizations", "true")
  202. q.Set("codex_cli_simplified_flow", "true")
  203. q.Set("originator", "codex_cli_rs")
  204. u.RawQuery = q.Encode()
  205. return u.String(), nil
  206. }
  207. func createStateHex(nBytes int) (string, error) {
  208. if nBytes <= 0 {
  209. return "", errors.New("invalid state bytes length")
  210. }
  211. b := make([]byte, nBytes)
  212. if _, err := rand.Read(b); err != nil {
  213. return "", err
  214. }
  215. return fmt.Sprintf("%x", b), nil
  216. }
  217. func generatePKCEPair() (verifier string, challenge string, err error) {
  218. b := make([]byte, 32)
  219. if _, err := rand.Read(b); err != nil {
  220. return "", "", err
  221. }
  222. verifier = base64.RawURLEncoding.EncodeToString(b)
  223. sum := sha256.Sum256([]byte(verifier))
  224. challenge = base64.RawURLEncoding.EncodeToString(sum[:])
  225. return verifier, challenge, nil
  226. }
  227. func ExtractCodexAccountIDFromJWT(token string) (string, bool) {
  228. claims, ok := decodeJWTClaims(token)
  229. if !ok {
  230. return "", false
  231. }
  232. raw, ok := claims[codexJWTClaimPath]
  233. if !ok {
  234. return "", false
  235. }
  236. obj, ok := raw.(map[string]any)
  237. if !ok {
  238. return "", false
  239. }
  240. v, ok := obj["chatgpt_account_id"]
  241. if !ok {
  242. return "", false
  243. }
  244. s, ok := v.(string)
  245. if !ok {
  246. return "", false
  247. }
  248. s = strings.TrimSpace(s)
  249. if s == "" {
  250. return "", false
  251. }
  252. return s, true
  253. }
  254. func ExtractEmailFromJWT(token string) (string, bool) {
  255. claims, ok := decodeJWTClaims(token)
  256. if !ok {
  257. return "", false
  258. }
  259. v, ok := claims["email"]
  260. if !ok {
  261. return "", false
  262. }
  263. s, ok := v.(string)
  264. if !ok {
  265. return "", false
  266. }
  267. s = strings.TrimSpace(s)
  268. if s == "" {
  269. return "", false
  270. }
  271. return s, true
  272. }
  273. func decodeJWTClaims(token string) (map[string]any, bool) {
  274. parts := strings.Split(token, ".")
  275. if len(parts) != 3 {
  276. return nil, false
  277. }
  278. payloadRaw, err := base64.RawURLEncoding.DecodeString(parts[1])
  279. if err != nil {
  280. return nil, false
  281. }
  282. var claims map[string]any
  283. if err := json.Unmarshal(payloadRaw, &claims); err != nil {
  284. return nil, false
  285. }
  286. return claims, true
  287. }