package auth import ( "context" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "log" "math/big" "net/http" "net/url" "strings" "time" "gpt-plus/pkg/httpclient" "gpt-plus/pkg/provider/email" ) // DefaultAcceptLanguage is the Accept-Language header value, set via SetLocale(). var DefaultAcceptLanguage = "en-US,en;q=0.9" // DefaultLanguage is the browser language code (e.g. "ko-KR"), set via SetLocale(). var DefaultLanguage = "en-US" // SetLocale updates the default language settings for the auth package. func SetLocale(language, acceptLanguage string) { if language != "" { DefaultLanguage = language } if acceptLanguage != "" { DefaultAcceptLanguage = acceptLanguage } } const ( oauthIssuer = "https://auth.openai.com" oauthClientID = "app_EMoamEEZ73f0CkXaXp7hrann" oauthRedirectURI = "http://localhost:1455/auth/callback" oauthScope = "openid profile email offline_access" ) // RegisterResult holds the output of a successful registration. type RegisterResult struct { Email string Password string DeviceID string MailboxID string CodeVerifier string State string FirstName string LastName string // Tokens are populated when registration completes the full OAuth flow. Tokens *LoginResult } // generatePKCE generates a PKCE code_verifier and code_challenge (S256). func generatePKCE() (verifier, challenge string) { b := make([]byte, 32) rand.Read(b) verifier = base64.RawURLEncoding.EncodeToString(b) h := sha256.Sum256([]byte(verifier)) challenge = base64.RawURLEncoding.EncodeToString(h[:]) return } // generateState returns a random URL-safe base64 state token. func generateState() string { b := make([]byte, 32) rand.Read(b) return base64.RawURLEncoding.EncodeToString(b) } // commonHeaders returns the base API headers used for auth.openai.com requests. func commonHeaders(deviceID string) map[string]string { h := map[string]string{ "accept": "application/json", "accept-language": DefaultAcceptLanguage, "content-type": "application/json", "origin": oauthIssuer, "sec-ch-ua": `"Google Chrome";v="145", "Not?A_Brand";v="8", "Chromium";v="145"`, "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": `"Windows"`, "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", "oai-device-id": deviceID, } for k, v := range GenerateDatadogHeaders() { h[k] = v } return h } // navigateHeaders returns headers for page-navigation GET requests. func navigateHeaders() map[string]string { h := map[string]string{ "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "accept-language": DefaultAcceptLanguage, "sec-ch-ua": `"Google Chrome";v="145", "Not?A_Brand";v="8", "Chromium";v="145"`, "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": `"Windows"`, "sec-fetch-dest": "document", "sec-fetch-mode": "navigate", "sec-fetch-site": "same-origin", "sec-fetch-user": "?1", "upgrade-insecure-requests": "1", } for k, v := range GenerateDatadogHeaders() { h[k] = v } return h } // Register performs the full 6-step registration flow. func Register(ctx context.Context, client *httpclient.Client, emailProvider email.EmailProvider, password string) (*RegisterResult, error) { // Create mailbox emailAddr, mailboxID, err := emailProvider.CreateMailbox(ctx) if err != nil { return nil, fmt.Errorf("create mailbox: %w", err) } deviceID := generateDeviceID() sentinel := &SentinelGenerator{DeviceID: deviceID, SID: generateUUID()} firstName, lastName := generateRandomName() birthdate := generateRandomBirthday() // Set oai-did cookie cookieURL, _ := url.Parse(oauthIssuer) client.GetCookieJar().SetCookies(cookieURL, []*http.Cookie{ {Name: "oai-did", Value: deviceID}, }) // PKCE codeVerifier, codeChallenge := generatePKCE() state := generateState() // ===== Step 0: OAuth session init ===== authorizeParams := url.Values{ "response_type": {"code"}, "client_id": {oauthClientID}, "redirect_uri": {oauthRedirectURI}, "scope": {oauthScope}, "code_challenge": {codeChallenge}, "code_challenge_method": {"S256"}, "state": {state}, "screen_hint": {"signup"}, "prompt": {"login"}, "id_token_add_organizations": {"true"}, "codex_cli_simplified_flow": {"true"}, } authorizeURL := oauthIssuer + "/oauth/authorize?" + authorizeParams.Encode() navH := navigateHeaders() resp, err := client.DoWithRetry(ctx, 5, func() (*http.Request, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, authorizeURL, nil) if err != nil { return nil, err } for k, v := range navH { req.Header.Set(k, v) } return req, nil }) if err != nil { return nil, fmt.Errorf("step0 authorize: %w", err) } httpclient.ReadBody(resp) log.Printf("[register] step0 authorize: status=%d, url=%s", resp.StatusCode, resp.Request.URL.String()) // Check for login_session cookie hasLoginSession := false for _, cookie := range client.GetCookieJar().Cookies(cookieURL) { log.Printf("[register] cookie: %s=%s (domain implicit)", cookie.Name, cookie.Value[:min(len(cookie.Value), 20)]+"...") if cookie.Name == "login_session" { hasLoginSession = true } } if !hasLoginSession { log.Printf("[register] WARNING: no login_session cookie found after step0") } // ===== Step 0b: POST authorize/continue with email ===== sentinelToken, err := sentinel.GenerateToken(ctx, client, "authorize_continue") if err != nil { return nil, fmt.Errorf("step0b sentinel: %w", err) } headers := commonHeaders(deviceID) headers["referer"] = oauthIssuer + "/create-account" headers["openai-sentinel-token"] = sentinelToken continueBody := map[string]interface{}{ "username": map[string]string{"kind": "email", "value": emailAddr}, "screen_hint": "signup", } continueJSON, _ := json.Marshal(continueBody) resp, err = client.DoWithRetry(ctx, 5, func() (*http.Request, error) { req, err := http.NewRequestWithContext(ctx, http.MethodPost, oauthIssuer+"/api/accounts/authorize/continue", strings.NewReader(string(continueJSON))) if err != nil { return nil, err } for k, v := range headers { req.Header.Set(k, v) } return req, nil }) if err != nil { return nil, fmt.Errorf("step0b authorize/continue: %w", err) } body, _ := httpclient.ReadBody(resp) if resp.StatusCode != 200 { return nil, fmt.Errorf("step0b failed (%d): %s", resp.StatusCode, string(body)) } log.Printf("[register] step0b authorize/continue: status=%d", resp.StatusCode) // ===== Step 2: POST register ===== sentinelToken, err = sentinel.GenerateToken(ctx, client, "register") if err != nil { return nil, fmt.Errorf("step2 sentinel: %w", err) } headers = commonHeaders(deviceID) headers["referer"] = oauthIssuer + "/create-account/password" headers["openai-sentinel-token"] = sentinelToken registerBody := map[string]string{ "username": emailAddr, "password": password, } resp, err = client.PostJSON(oauthIssuer+"/api/accounts/user/register", registerBody, headers) if err != nil { return nil, fmt.Errorf("step2 register: %w", err) } body, _ = httpclient.ReadBody(resp) if resp.StatusCode != 200 && resp.StatusCode != 302 { return nil, fmt.Errorf("step2 register failed (%d): %s", resp.StatusCode, string(body)) } log.Printf("[register] step2 register: status=%d, body=%s", resp.StatusCode, string(body)[:min(len(body), 200)]) // ===== Step 3: GET email-otp/send ===== navH["referer"] = oauthIssuer + "/create-account/password" resp, err = client.Get(oauthIssuer+"/api/accounts/email-otp/send", navH) if err != nil { return nil, fmt.Errorf("step3 send otp: %w", err) } otpBody, _ := httpclient.ReadBody(resp) log.Printf("[register] step3 send otp: status=%d, body=%s", resp.StatusCode, string(otpBody)[:min(len(otpBody), 200)]) // Also GET /email-verification page to accumulate cookies resp, err = client.Get(oauthIssuer+"/email-verification", navH) if err != nil { return nil, fmt.Errorf("step3 email-verification page: %w", err) } httpclient.ReadBody(resp) log.Printf("[register] step3 email-verification page: status=%d", resp.StatusCode) // ===== Step 4: Wait for OTP and validate ===== code, err := emailProvider.WaitForVerificationCode(ctx, mailboxID, 120*time.Second, time.Time{}) if err != nil { return nil, fmt.Errorf("step4 wait for otp: %w", err) } headers = commonHeaders(deviceID) headers["referer"] = oauthIssuer + "/email-verification" validateBody := map[string]string{"code": code} resp, err = client.PostJSON(oauthIssuer+"/api/accounts/email-otp/validate", validateBody, headers) if err != nil { return nil, fmt.Errorf("step4 validate otp: %w", err) } body, _ = httpclient.ReadBody(resp) if resp.StatusCode != 200 { return nil, fmt.Errorf("step4 validate failed (%d): %s", resp.StatusCode, string(body)) } // Parse continue_url from validate response var validateResp struct { ContinueURL string `json:"continue_url"` Page struct { Type string `json:"type"` } `json:"page"` } json.Unmarshal(body, &validateResp) // ===== Step 5: POST create_account ===== headers = commonHeaders(deviceID) headers["referer"] = oauthIssuer + "/about-you" createBody := map[string]string{ "name": firstName + " " + lastName, "birthdate": birthdate, } resp, err = client.PostJSON(oauthIssuer+"/api/accounts/create_account", createBody, headers) if err != nil { return nil, fmt.Errorf("step5 create account: %w", err) } body, _ = httpclient.ReadBody(resp) // 200 = success, 400 with "already_exists" is also acceptable if resp.StatusCode != 200 { if resp.StatusCode == 400 && strings.Contains(string(body), "already_exists") { // Account already created during registration, this is fine } else { return nil, fmt.Errorf("step5 create account failed (%d): %s", resp.StatusCode, string(body)) } } // ===== Step 6: Complete OAuth flow (consent → callback → tokens) ===== log.Printf("[register] step6 completing OAuth flow to get tokens") // Determine consent continue URL var createResp struct { ContinueURL string `json:"continue_url"` } json.Unmarshal(body, &createResp) consentURL := createResp.ContinueURL if consentURL == "" { consentURL = oauthIssuer + "/sign-in-with-chatgpt/codex/consent" } authCode, err := followConsentRedirects(ctx, client, deviceID, consentURL, "") if err != nil { log.Printf("[register] step6 consent flow failed (non-fatal): %v", err) // Return without tokens — caller will need to do a separate login return &RegisterResult{ Email: emailAddr, Password: password, DeviceID: deviceID, MailboxID: mailboxID, CodeVerifier: codeVerifier, State: state, FirstName: firstName, LastName: lastName, }, nil } tokens, err := exchangeCodeForTokens(ctx, client, authCode, codeVerifier) if err != nil { log.Printf("[register] step6 token exchange failed (non-fatal): %v", err) return &RegisterResult{ Email: emailAddr, Password: password, DeviceID: deviceID, MailboxID: mailboxID, CodeVerifier: codeVerifier, State: state, FirstName: firstName, LastName: lastName, }, nil } log.Printf("[register] step6 OAuth flow complete, got tokens") return &RegisterResult{ Email: emailAddr, Password: password, DeviceID: deviceID, MailboxID: mailboxID, CodeVerifier: codeVerifier, State: state, FirstName: firstName, LastName: lastName, Tokens: tokens, }, nil } // GenerateRandomPassword creates a random password with mixed character types. func GenerateRandomPassword(length int) string { if length < 8 { length = 16 } const ( upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" lower = "abcdefghijklmnopqrstuvwxyz" digits = "0123456789" special = "!@#$%^&*" ) all := upper + lower + digits + special // Ensure at least one of each type pw := make([]byte, length) pw[0] = upper[randInt(len(upper))] pw[1] = lower[randInt(len(lower))] pw[2] = digits[randInt(len(digits))] pw[3] = special[randInt(len(special))] for i := 4; i < length; i++ { pw[i] = all[randInt(len(all))] } // Shuffle for i := len(pw) - 1; i > 0; i-- { j := randInt(i + 1) pw[i], pw[j] = pw[j], pw[i] } return string(pw) } var firstNames = []string{ "James", "Mary", "Robert", "Patricia", "John", "Jennifer", "Michael", "Linda", "David", "Elizabeth", "William", "Barbara", "Richard", "Susan", "Joseph", "Jessica", "Thomas", "Sarah", "Charles", "Karen", "Christopher", "Lisa", "Daniel", "Nancy", } var lastNames = []string{ "Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin", "Lee", "Perez", "Thompson", "White", } func generateRandomName() (first, last string) { first = firstNames[randInt(len(firstNames))] last = lastNames[randInt(len(lastNames))] return } func generateRandomBirthday() string { year := 1985 + randInt(16) // 1985-2000 month := 1 + randInt(12) day := 1 + randInt(28) return fmt.Sprintf("%04d-%02d-%02d", year, month, day) } func generateDeviceID() string { return generateUUID() } func generateUUID() string { b := make([]byte, 16) rand.Read(b) b[6] = (b[6] & 0x0f) | 0x40 // version 4 b[8] = (b[8] & 0x3f) | 0x80 // variant 10 return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } func randInt(max int) int { n, _ := rand.Int(rand.Reader, big.NewInt(int64(max))) return int(n.Int64()) }