Rewrite session scripts for new login flow

Fixes #1421
This commit is contained in:
Zed 2026-08-17 05:28:53 +07:00
commit 607765c463
4 changed files with 375 additions and 235 deletions

View file

@ -23,118 +23,353 @@ Output:
import asyncio import asyncio
import json import json
import os import os
import shutil
import sys import sys
import tempfile
import nodriver as uc import zendriver as zd
from zendriver import cdp
import pyotp import pyotp
async def login_and_get_cookies(username, password, totp_seed=None, headless=False): # Disable password manager to prevent the "Save password?" bubble from
"""Authenticate with X.com and extract session cookies""" # stealing focus during automated login.
# Note: headless mode may increase detection risk from bot-detection systems _SEED_PREFS = {
browser = await uc.start(headless=headless) "credentials_enable_service": False,
tab = await browser.get("https://x.com/i/flow/login") "profile": {"password_manager_enabled": False},
}
_BROWSER_ARGS = [
"--password-store=basic",
"--no-first-run",
"--no-default-browser-check",
"--disable-notifications",
]
def _log(*a):
print(*a, file=sys.stderr, flush=True)
def _make_profile():
"""Create a temp Chrome profile with password manager disabled."""
profile = tempfile.mkdtemp(prefix="xsess_")
default = os.path.join(profile, "Default")
os.makedirs(default)
with open(os.path.join(default, "Preferences"), "w") as f:
json.dump(_SEED_PREFS, f)
return profile
def _extract_user_id(cookies_dict):
"""Extract numeric user ID from the twid cookie."""
twid = cookies_dict.get("twid", "").strip('"')
for prefix in ("u%3D", "u="):
if prefix in twid:
return twid.split(prefix)[1].split("&")[0].strip('"')
return None
async def _check_login_error(tab):
"""Check if the login flow is showing an error (wrong password, etc.)."""
try: try:
# Enter username return await tab.evaluate('''(() => {
print(f"[*] Entering username {username}...", file=sys.stderr) // Check role="alert" elements (X's standard error display)
const alert = document.querySelector('[role="alert"]');
if (alert) {
const t = alert.textContent.trim();
if (t.length > 0 && t.length < 200) return t;
}
// Check for common error strings in visible text
for (const el of document.querySelectorAll('p, span, div')) {
const t = el.textContent.trim();
if (t.length > 5 && t.length < 150
&& (t.includes('Wrong password')
|| t.includes('incorrect')
|| t.includes('Could not log you in')
|| t.includes("can\\'t find")
|| t.includes('cannot find')
|| t.includes('suspended')
|| t.includes('locked')
|| t.includes('unusual login'))) {
return t;
}
}
return '';
})()''')
except Exception:
return ''
retry = 0
while retry < 5:
username_input = await tab.find(
'input[autocomplete="username"]', timeout=10
)
pos = await username_input.get_position() async def _click_continue(tab):
await tab.mouse_move(pos.x, pos.y, steps=50, flash=True) """Click the 'Continue' / 'Log in' button in the jf onboarding flow.
await asyncio.sleep(0.1)
await username_input.click() The button is a nested <div> containing <p>Continue</p> (or <p>Log in</p>),
not a standard <button type="submit">.
"""
try:
return await tab.evaluate('''(() => {
for (const p of document.querySelectorAll('p.jf-element')) {
const t = p.textContent.trim();
if (t === 'Continue' || t === 'Log in' || t === 'Next') {
p.parentElement.parentElement.parentElement.click();
return true;
}
}
return false;
})()''')
except Exception:
return False
async def _find_visible_input(tab, name, timeout=15):
"""Wait for a visible input[name=...] to appear and return it."""
for _ in range(timeout * 2):
try:
found = await tab.evaluate(f'''(() => {{
for (const inp of document.querySelectorAll('input[name="{name}"]')) {{
const r = inp.getBoundingClientRect();
if (r.width > 0 && r.height > 0) return true;
}}
return false;
}})()''')
if found:
return await tab.select(f'input[name="{name}"]')
except Exception:
pass
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
await username_input.send_keys(username) return None
await asyncio.sleep(0.2)
await username_input.send_keys("\n")
await asyncio.sleep(2)
page_content = await tab.get_content()
if "Could not log you in" in page_content:
retry += 1
wait = retry * 10
print(f"Retrying in {wait} seconds...")
await asyncio.sleep(wait)
else:
break
# Enter password async def _clear_otp(tab):
print("[*] Entering password...", file=sys.stderr) """Clear the 6-box OTP field so a fresh code can be entered.
pretry = 0
while pretry < 5: Selects all content in the focused input and deletes it, then re-focuses
password_input = await tab.find( the first OTP box.
'input[autocomplete="current-password"]', timeout=15 """
try:
await tab.evaluate('''(() => {
const inputs = document.querySelectorAll('input[autocomplete="one-time-code"]');
if (inputs.length) {
inputs.forEach(inp => { inp.value = ''; });
inputs[0].focus();
return true;
}
// Fallback: clear any focused input
const el = document.activeElement;
if (el && el.tagName === 'INPUT') {
el.value = '';
el.dispatchEvent(new Event('input', { bubbles: true }));
}
return false;
})()''')
except Exception:
pass
async def _type_otp(tab, code):
"""Type a 2FA code via CDP Input.insertText into the auto-focused OTP field.
The jf onboarding 2FA screen shows 6 individual boxes that auto-focus the
first one. insertText commits all digits at once; the field auto-submits
when all 6 are filled. This avoids DOM/Runtime methods that can hang on
this SPA screen.
"""
try:
await asyncio.wait_for(
tab.send(cdp.input_.insert_text(code)), timeout=8
) )
await password_input.click() return True
except Exception:
return False
async def _otp_error(tab):
"""Check if the 2FA screen shows an error message like 'Incorrect'."""
try:
return await tab.evaluate('''(() => {
const el = document.querySelector('[role="alert"]');
if (el) return el.textContent.trim().substring(0, 80);
for (const el of document.querySelectorAll('p, span')) {
const t = el.textContent.trim();
if (t.length < 100
&& (t.includes('Incorrect') || t.includes('try again')
|| t.includes('invalid') || t.includes('expired'))) {
return t;
}
}
return '';
})()''')
except Exception:
return ''
def _fresh_totp(totp_seed, min_remaining=5):
"""Generate a TOTP code with at least min_remaining seconds of validity.
If the current code is about to expire, waits for the next window.
"""
import time
totp = pyotp.TOTP(totp_seed)
code = totp.now()
# Check remaining validity: TOTP period is 30s
elapsed = time.time() % 30
remaining = 30 - elapsed
if remaining < min_remaining:
time.sleep(remaining + 1)
code = totp.now()
return code
async def _get_cookies(browser):
"""Read cookies from the browser, returning a name→value dict."""
cookies = await browser.cookies.get_all()
return {c.name: c.value for c in cookies}
async def _check_session(browser, username):
"""Check if auth cookies are present and build a session dict."""
cd = await _get_cookies(browser)
if "auth_token" in cd and "ct0" in cd:
return {
"kind": "cookie",
"username": username,
"id": _extract_user_id(cd),
"auth_token": cd["auth_token"],
"ct0": cd["ct0"],
}
return None
async def login_and_get_session(username, password, totp_seed=None, headless=False):
"""Authenticate with X.com and return a session dict, or None on failure.
Uses the new /i/jf/onboarding flow (as of mid-2026). A fresh Chrome
profile is created per login to avoid cookie bleed.
"""
profile = _make_profile()
browser = await zd.start(
headless=headless,
user_data_dir=profile,
browser_args=_BROWSER_ARGS,
)
try:
# --- Navigate to login ---
_log(f"[*] Logging in {username}...")
tab = await browser.get("https://x.com/i/flow/login")
await asyncio.sleep(4)
# --- Username ---
_log("[*] Entering username...")
uinput = await _find_visible_input(tab, "username_or_email")
if not uinput:
raise Exception("Username field not found")
await uinput.click()
await asyncio.sleep(0.3)
await uinput.send_keys(username)
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
await password_input.send_keys(password)
await asyncio.sleep(0.2)
await password_input.send_keys("\n")
await asyncio.sleep(2)
page_content = await tab.get_content() if not await _click_continue(tab):
if "Could not log you in" in page_content: await uinput.send_keys("\n")
pretry += 1 await asyncio.sleep(3)
wait = pretry * 10
print(f"Retrying in {wait} seconds...") err = await _check_login_error(tab)
await asyncio.sleep(wait) if err:
else: raise Exception(f"Username rejected: {err}")
# --- Password ---
_log("[*] Entering password...")
pw = await _find_visible_input(tab, "password")
if not pw:
raise Exception("Password field not found")
await pw.click()
await asyncio.sleep(0.3)
await pw.send_keys(password)
await asyncio.sleep(0.5)
if not await _click_continue(tab):
await pw.send_keys("\n")
await asyncio.sleep(3)
err = await _check_login_error(tab)
if err:
raise Exception(f"Login failed: {err}")
# --- Check for immediate auth (no 2FA) ---
session = await _check_session(browser, username)
if session:
_log("[*] Authenticated (no 2FA)")
return session
# --- 2FA ---
# Detect 2FA by URL fragment (reliable) or page content
for _ in range(10):
url = tab.url or ""
if "two_factor" in url:
break break
await asyncio.sleep(1)
await asyncio.sleep(1) # let the OTP field mount and auto-focus
# Handle 2FA if needed url = tab.url or ""
page_content = await tab.get_content() if "two_factor" in url:
if "verification code" in page_content or "Enter code" in page_content:
if not totp_seed: if not totp_seed:
raise Exception("2FA required but no TOTP seed provided") raise Exception("2FA required but no TOTP seed provided")
print("[*] 2FA detected, entering code...", file=sys.stderr) _log("[*] 2FA detected, entering code...")
totp_code = pyotp.TOTP(totp_seed).now() last_code = None
code_input = await tab.select('input[type="text"]') for attempt in range(2):
await code_input.send_keys(totp_code + "\n") code = _fresh_totp(totp_seed)
while code == last_code:
await asyncio.sleep(3) await asyncio.sleep(3)
code = _fresh_totp(totp_seed)
last_code = code
# Get cookies if attempt > 0:
print("[*] Retrieving cookies...", file=sys.stderr) await _clear_otp(tab)
for _ in range(20): # 20 second timeout await asyncio.sleep(0.5)
cookies = await browser.cookies.get_all()
cookies_dict = {cookie.name: cookie.value for cookie in cookies}
if "auth_token" in cookies_dict and "ct0" in cookies_dict: typed = await _type_otp(tab, code)
# Extract ID from twid cookie (may be URL-encoded) _log(f"[*] OTP attempt {attempt + 1}: typed={typed}")
user_id = None
if "twid" in cookies_dict:
twid = cookies_dict["twid"]
# Try to extract the ID from twid (format: u%3D<id> or u=<id>)
if "u%3D" in twid:
user_id = twid.split("u%3D")[1].split("&")[0].strip('"')
elif "u=" in twid:
user_id = twid.split("u=")[1].split("&")[0].strip('"')
cookies_dict["username"] = username # Check for success or error (fast loop)
if user_id: for _ in range(5):
cookies_dict["id"] = user_id await asyncio.sleep(2)
session = await _check_session(browser, username)
if session:
_log("[*] Authenticated (2FA)")
return session
err = await _otp_error(tab)
if err:
_log(f"[*] OTP rejected: {err}")
break
return cookies_dict raise Exception("2FA code rejected (account may be suspended or OTP reset)")
await asyncio.sleep(1) # --- Post-login interstitials (premium signup push, etc.) ---
_log("[*] Waiting for post-login redirect...")
for i in range(10):
session = await _check_session(browser, username)
if session:
_log("[*] Authenticated")
return session
await _click_continue(tab)
await asyncio.sleep(2)
raise Exception("Timeout waiting for cookies") raise Exception("Timeout waiting for authentication cookies")
finally: finally:
browser.stop() try:
await browser.stop()
except Exception:
pass
await asyncio.sleep(1)
shutil.rmtree(profile, ignore_errors=True)
async def main(): async def main():
if len(sys.argv) < 3: if len(sys.argv) < 3:
print( print(
"Usage: python3 create_session_browser.py username password [totp_seed] [--append file.jsonl] [--headless]" "Usage: python3 create_session_browser.py username password"
" [totp_seed] [--append file.jsonl] [--headless]"
) )
sys.exit(1) sys.exit(1)
@ -151,7 +386,7 @@ async def main():
if arg == "--append": if arg == "--append":
if i + 1 < len(sys.argv): if i + 1 < len(sys.argv):
append_file = sys.argv[i + 1] append_file = sys.argv[i + 1]
i += 2 # Skip '--append' and filename i += 2
else: else:
print("[!] Error: --append requires a filename", file=sys.stderr) print("[!] Error: --append requires a filename", file=sys.stderr)
sys.exit(1) sys.exit(1)
@ -163,19 +398,11 @@ async def main():
totp_seed = arg totp_seed = arg
i += 1 i += 1
else: else:
# Unkown args
print(f"[!] Warning: Unknown argument: {arg}", file=sys.stderr) print(f"[!] Warning: Unknown argument: {arg}", file=sys.stderr)
i += 1 i += 1
try: try:
cookies = await login_and_get_cookies(username, password, totp_seed, headless) session = await login_and_get_session(username, password, totp_seed, headless)
session = {
"kind": "cookie",
"username": cookies["username"],
"id": cookies.get("id"),
"auth_token": cookies["auth_token"],
"ct0": cookies["ct0"],
}
output = json.dumps(session) output = json.dumps(session)
if append_file: if append_file:

View file

@ -1,5 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
DEPRECATED: X now requires a castle token generated by client-side JavaScript
during the login flow, which blocks pure-API authentication. Use
create_session_browser.py instead.
This script is kept for reference but will fail with current X API defenses.
Requirements: Requirements:
pip install curl_cffi pyotp pip install curl_cffi pyotp

View file

@ -13,14 +13,14 @@ Examples:
# Append to sessions.jsonl # Append to sessions.jsonl
python3 tools/create_sessions_browser.py <accounts_file> --append sessions.jsonl python3 tools/create_sessions_browser.py <accounts_file> --append sessions.jsonl
# Add 5 second delay between sessions (default: 1) # Add 5 second delay between sessions (default: 3)
python3 tools/create_sessions_browser.py <accounts_file> --delay 5 python3 tools/create_sessions_browser.py <accounts_file> --delay 5
# Headless mode (may increase detection risk) # Headless mode (may increase detection risk)
python3 tools/create_sessions_browser.py <accounts_file> --headless python3 tools/create_sessions_browser.py <accounts_file> --headless
Input (accounts_file): Input (accounts_file):
[{"username": "user", "password": "pass", "totp": "totp_code"}, {...}, ...] [{"username": "user", "password": "pass", "totp": "totp_secret"}, {...}, ...]
Output: Output:
{"kind": "cookie", "username": "...", "id": "...", "auth_token": "...", "ct0": "..."} {"kind": "cookie", "username": "...", "id": "...", "auth_token": "...", "ct0": "..."}
@ -33,127 +33,21 @@ import json
import sys import sys
from time import sleep from time import sleep
import nodriver as uc from create_session_browser import login_and_get_session
import pyotp
async def login_and_get_cookies(account, headless=False):
"""Authenticate with X.com and extract session cookies"""
# Note: headless mode may increase detection risk from bot-detection systems
browser = await uc.start(headless=headless)
tab = await browser.get("https://x.com/i/flow/login")
username = account["username"]
password = account["password"]
totp_seed = account["totp"]
try:
# Enter username
print(f"[*] Entering username {username}...", file=sys.stderr)
retry = 0
while retry < 5:
username_input = await tab.find(
'input[autocomplete="username"]', timeout=10
)
pos = await username_input.get_position()
await tab.mouse_move(pos.x, pos.y, steps=50, flash=True)
await asyncio.sleep(0.1)
await username_input.click()
await asyncio.sleep(0.5)
await username_input.send_keys(username)
await asyncio.sleep(0.2)
await username_input.send_keys("\n")
await asyncio.sleep(2)
page_content = await tab.get_content()
if "Could not log you in" in page_content:
retry += 1
wait = retry * 10
print(f"Retrying in {wait} seconds...")
await asyncio.sleep(wait)
else:
break
# Enter password
print("[*] Entering password...", file=sys.stderr)
pretry = 0
while pretry < 5:
password_input = await tab.find(
'input[autocomplete="current-password"]', timeout=15
)
await password_input.click()
await asyncio.sleep(0.5)
await password_input.send_keys(password)
await asyncio.sleep(0.2)
await password_input.send_keys("\n")
await asyncio.sleep(2)
page_content = await tab.get_content()
if "Could not log you in" in page_content:
pretry += 1
wait = pretry * 10
print(f"Retrying in {wait} seconds...")
await asyncio.sleep(wait)
else:
break
# Handle 2FA if needed
page_content = await tab.get_content()
if "verification code" in page_content or "Enter code" in page_content:
if not totp_seed:
raise Exception("2FA required but no TOTP seed provided")
print("[*] 2FA detected, entering code...", file=sys.stderr)
totp_code = pyotp.TOTP(totp_seed).now()
code_input = await tab.select('input[type="text"]')
await code_input.send_keys(totp_code + "\n")
await asyncio.sleep(3)
# Get cookies
print("[*] Retrieving cookies...", file=sys.stderr)
for _ in range(20): # 20 second timeout
cookies = await browser.cookies.get_all()
cookies_dict = {cookie.name: cookie.value for cookie in cookies}
if "auth_token" in cookies_dict and "ct0" in cookies_dict:
# Extract ID from twid cookie (may be URL-encoded)
user_id = None
if "twid" in cookies_dict:
twid = cookies_dict["twid"]
# Try to extract the ID from twid (format: u%3D<id> or u=<id>)
if "u%3D" in twid:
user_id = twid.split("u%3D")[1].split("&")[0].strip('"')
elif "u=" in twid:
user_id = twid.split("u=")[1].split("&")[0].strip('"')
cookies_dict["username"] = username
if user_id:
cookies_dict["id"] = user_id
return cookies_dict
await asyncio.sleep(1)
raise Exception("Timeout waiting for cookies")
finally:
browser.stop()
async def main(): async def main():
if len(sys.argv) < 2: if len(sys.argv) < 2:
print( print(
"Usage: python3 create_sessions_browser.py <accounts_file> [--append sessions.jsonl] [--headless]" "Usage: python3 create_sessions_browser.py <accounts_file>"
" [--append sessions.jsonl] [--headless] [--delay N]"
) )
sys.exit(1) sys.exit(1)
input = sys.argv[1] input_file = sys.argv[1]
append_file = None append_file = None
headless = False headless = False
delay = 1 delay = 3
# Parse optional arguments # Parse optional arguments
i = 2 i = 2
@ -162,7 +56,7 @@ async def main():
if arg == "--append": if arg == "--append":
if i + 1 < len(sys.argv): if i + 1 < len(sys.argv):
append_file = sys.argv[i + 1] append_file = sys.argv[i + 1]
i += 2 # Skip '--append' and filename i += 2
else: else:
print("[!] Error: --append requires a filename", file=sys.stderr) print("[!] Error: --append requires a filename", file=sys.stderr)
sys.exit(1) sys.exit(1)
@ -173,30 +67,28 @@ async def main():
delay = int(sys.argv[i + 1]) delay = int(sys.argv[i + 1])
i += 2 i += 2
else: else:
# Unkown args
print(f"[!] Warning: Unknown argument: {arg}", file=sys.stderr) print(f"[!] Warning: Unknown argument: {arg}", file=sys.stderr)
i += 1 i += 1
accounts = [] with open(input_file) as f:
with open(input) as f:
accounts = json.load(f) accounts = json.load(f)
if len(accounts) == 0: if not accounts:
print("no accounts in file") print("No accounts in file")
sys.exit(0) sys.exit(0)
sessions = 0 ok, fail = [], []
for acc in accounts: for idx, acc in enumerate(accounts, 1):
sessions += 1 username = acc["username"]
print(
f"\n[{idx}/{len(accounts)}] {username}...",
file=sys.stderr,
flush=True,
)
try: try:
cookies = await login_and_get_cookies(acc, headless) session = await login_and_get_session(
session = { username, acc["password"], acc.get("totp"), headless
"kind": "cookie", )
"username": cookies["username"],
"id": cookies.get("id"),
"auth_token": cookies["auth_token"],
"ct0": cookies["ct0"],
}
if append_file: if append_file:
with open(append_file, "a") as f: with open(append_file, "a") as f:
@ -204,15 +96,30 @@ async def main():
else: else:
print(json.dumps(session)) print(json.dumps(session))
print(f"Progress: {sessions} / {len(accounts)}") ok.append(username)
if sessions < len(accounts):
print("Waiting", delay, "seconds")
sleep(delay)
except Exception as error:
print( print(
f"[!] Error getting session for {acc["username"]}, skipping: {error}", f" ✓ saved (id={session['id']})",
file=sys.stderr, file=sys.stderr,
flush=True,
) )
except Exception as error:
fail.append(username)
print(
f"{error}",
file=sys.stderr,
flush=True,
)
if idx < len(accounts):
sleep(delay)
print(
f"\nDone: {len(ok)} ok, {len(fail)} failed",
file=sys.stderr,
flush=True,
)
if fail:
print(f" failed: {fail}", file=sys.stderr, flush=True)
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -1,3 +1,3 @@
nodriver>=0.48.0 zendriver>=0.15.0
pyotp pyotp
curl_cffi curl_cffi