155 lines
5.0 KiB
Go
155 lines
5.0 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"gpt-plus/pkg/httpclient"
|
|
)
|
|
|
|
// ObtainCodexTokens performs a lightweight OAuth re-authorization using existing
|
|
// session cookies to obtain Codex CLI tokens (with refresh_token) for a specific workspace.
|
|
//
|
|
// This should be called AFTER activation is complete, when the cookie jar already
|
|
// contains valid session cookies from the initial login.
|
|
//
|
|
// targetWorkspaceID: the workspace/account ID to authorize for. Pass "" to use the default (first) workspace.
|
|
func ObtainCodexTokens(
|
|
ctx context.Context,
|
|
client *httpclient.Client,
|
|
deviceID string,
|
|
targetWorkspaceID string,
|
|
) (*LoginResult, error) {
|
|
log.Printf("[codex] obtaining Codex CLI tokens for workspace=%s", targetWorkspaceID)
|
|
|
|
// Ensure oai-did cookie is set
|
|
cookieURL, _ := url.Parse(oauthIssuer)
|
|
client.GetCookieJar().SetCookies(cookieURL, []*http.Cookie{
|
|
{Name: "oai-did", Value: deviceID},
|
|
})
|
|
|
|
// Generate fresh PKCE pair and state for this authorization
|
|
codeVerifier, codeChallenge := generatePKCE()
|
|
state := generateState()
|
|
|
|
// Build authorize URL with Codex CLI params
|
|
authorizeParams := url.Values{
|
|
"response_type": {"code"},
|
|
"client_id": {oauthClientID},
|
|
"redirect_uri": {oauthRedirectURI},
|
|
"scope": {oauthScope},
|
|
"code_challenge": {codeChallenge},
|
|
"code_challenge_method": {"S256"},
|
|
"state": {state},
|
|
"id_token_add_organizations": {"true"},
|
|
"codex_cli_simplified_flow": {"true"},
|
|
}
|
|
authorizeURL := oauthIssuer + "/oauth/authorize?" + authorizeParams.Encode()
|
|
navH := navigateHeaders()
|
|
|
|
if targetWorkspaceID == "" {
|
|
// Default workspace: auto-follow redirects and use shortcut if code is returned directly.
|
|
resp, err := client.Get(authorizeURL, navH)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("codex authorize request: %w", err)
|
|
}
|
|
httpclient.ReadBody(resp)
|
|
|
|
if resp.Request != nil {
|
|
if code := extractCodeFromURL(resp.Request.URL.String()); code != "" {
|
|
log.Printf("[codex] got code directly from authorize redirect (default workspace)")
|
|
return exchangeCodeForTokens(ctx, client, code, codeVerifier)
|
|
}
|
|
}
|
|
|
|
log.Printf("[codex] authorize response: status=%d, url=%s", resp.StatusCode, resp.Request.URL.String())
|
|
|
|
// Fallback: go through consent flow
|
|
consentURL := oauthIssuer + "/sign-in-with-chatgpt/codex/consent"
|
|
authCode, err := followConsentRedirects(ctx, client, deviceID, consentURL, "")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("codex consent flow: %w", err)
|
|
}
|
|
return exchangeCodeForTokens(ctx, client, authCode, codeVerifier)
|
|
}
|
|
|
|
// ===== Specific workspace requested (e.g. Team) =====
|
|
log.Printf("[codex] specific workspace requested, using manual redirect flow")
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, authorizeURL, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("codex build authorize request: %w", err)
|
|
}
|
|
for k, v := range navH {
|
|
req.Header.Set(k, v)
|
|
}
|
|
|
|
// Follow redirects manually. Stop when we see the consent page or a code for the wrong workspace.
|
|
currentURL := authorizeURL
|
|
for i := 0; i < 15; i++ {
|
|
resp, err := client.DoNoRedirect(req)
|
|
if err != nil {
|
|
if code := extractCodeFromURL(currentURL); code != "" {
|
|
log.Printf("[codex] got code from failed redirect (default workspace), ignoring — need workspace %s", targetWorkspaceID)
|
|
break
|
|
}
|
|
return nil, fmt.Errorf("codex authorize redirect: %w", err)
|
|
}
|
|
httpclient.ReadBody(resp)
|
|
|
|
if resp.StatusCode >= 300 && resp.StatusCode < 400 {
|
|
loc := resp.Header.Get("Location")
|
|
if loc == "" {
|
|
break
|
|
}
|
|
if !strings.HasPrefix(loc, "http") {
|
|
loc = oauthIssuer + loc
|
|
}
|
|
|
|
if code := extractCodeFromURL(loc); code != "" {
|
|
log.Printf("[codex] auto-redirect has code (default workspace), ignoring — need workspace %s", targetWorkspaceID)
|
|
break
|
|
}
|
|
|
|
log.Printf("[codex] redirect %d: %d → %s", i+1, resp.StatusCode, truncURL(loc))
|
|
currentURL = loc
|
|
req, _ = http.NewRequestWithContext(ctx, http.MethodGet, loc, nil)
|
|
for k, v := range navH {
|
|
req.Header.Set(k, v)
|
|
}
|
|
continue
|
|
}
|
|
|
|
log.Printf("[codex] reached page: status=%d, url=%s", resp.StatusCode, currentURL)
|
|
break
|
|
}
|
|
|
|
// Now go through the consent flow with the target workspace.
|
|
consentURL := oauthIssuer + "/sign-in-with-chatgpt/codex/consent"
|
|
authCode, err := followConsentRedirects(ctx, client, deviceID, consentURL, targetWorkspaceID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("codex consent flow for workspace %s: %w", targetWorkspaceID, err)
|
|
}
|
|
|
|
tokens, err := exchangeCodeForTokens(ctx, client, authCode, codeVerifier)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("codex token exchange: %w", err)
|
|
}
|
|
|
|
log.Printf("[codex] tokens obtained for workspace %s: has_refresh=%v, account_id=%s",
|
|
targetWorkspaceID, tokens.RefreshToken != "", tokens.ChatGPTAccountID)
|
|
return tokens, nil
|
|
}
|
|
|
|
// truncURL shortens a URL for logging.
|
|
func truncURL(u string) string {
|
|
if len(u) > 100 {
|
|
return u[:100] + "..."
|
|
}
|
|
return u
|
|
}
|