Files
gpt-plus-gpt/pkg/auth/register.go
2026-03-15 20:48:19 +08:00

442 lines
14 KiB
Go

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())
}