Initial sanitized code sync
This commit is contained in:
441
pkg/auth/register.go
Normal file
441
pkg/auth/register.go
Normal file
@@ -0,0 +1,441 @@
|
||||
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())
|
||||
}
|
||||
Reference in New Issue
Block a user