diff --git a/backend/app/services/auto_login_service.py b/backend/app/services/auto_login_service.py index 81f1382..9bfa014 100644 --- a/backend/app/services/auto_login_service.py +++ b/backend/app/services/auto_login_service.py @@ -176,22 +176,42 @@ def _perform_zerodha_login( ) -> dict: """Automates Zerodha login and returns session data with access_token.""" session = requests.Session() + # Use a real browser UA; do NOT set X-Kite-Version here — that header is + # for the Kite Connect REST API, not the web login endpoints, and confuses + # Zerodha's routing so it returns a plain profile response instead of an + # OAuth redirect_url. session.headers.update({ - "X-Kite-Version": "3", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/124.0.0.0 Safari/537.36" + ), }) - # Step 1: Initialize OAuth session with api_key so Zerodha knows - # which app is logging in and returns request_token after TOTP. - session.get( - f"https://kite.zerodha.com/connect/login?api_key={api_key}&v=3", + # Step 1: Initialize OAuth session. + # Use allow_redirects=False so we capture the Set-Cookie from the first + # response before any redirect overwrites/clears the OAuth context cookie. + connect_resp = session.get( + f"https://kite.zerodha.com/connect/login?v=3&api_key={api_key}", timeout=15, + allow_redirects=False, ) + print( + f"[AUTO-LOGIN-DEBUG] connect status={connect_resp.status_code} " + f"location={connect_resp.headers.get('Location', 'NONE')} " + f"cookies={list(session.cookies.keys())}", + flush=True, + ) + # Follow the redirect to the login page so the full OAuth init completes + redirect_to = connect_resp.headers.get("Location", "") + if connect_resp.status_code in (301, 302, 303, 307, 308) and redirect_to: + session.get(redirect_to, timeout=15, allow_redirects=True) # Step 2: Username + password login_resp = session.post( KITE_LOGIN_ENDPOINT, data={"user_id": zerodha_login_id, "password": password}, + headers={"Referer": "https://kite.zerodha.com/login"}, timeout=15, ) try: @@ -204,7 +224,7 @@ def _perform_zerodha_login( request_id = login_data["data"]["request_id"] - # Step 3: TOTP — don't follow redirect automatically + # Step 3: TOTP try: import pyotp except ImportError: @@ -218,30 +238,44 @@ def _perform_zerodha_login( "twofa_value": totp_value, "twofa_type": "totp", }, + headers={"Referer": "https://kite.zerodha.com/login"}, timeout=15, allow_redirects=False, ) print( f"[AUTO-LOGIN-DEBUG] twofa status={twofa_resp.status_code} " f"location={twofa_resp.headers.get('Location', 'NONE')} " - f"body={twofa_resp.text[:300]}", + f"body={twofa_resp.text[:400]}", flush=True, ) - # Step 4: Follow redirects manually to intercept request_token + # Step 4: Extract request_token. + # Modern Zerodha (SPA): returns 200 JSON with data.redirect_url containing request_token. + # Older behavior: 302 Location header redirect. request_token = None - location = twofa_resp.headers.get("Location", "") - for _ in range(10): - if "request_token" in location: - parsed = urlparse(location) + try: + twofa_json = twofa_resp.json() + redirect_url_body = twofa_json.get("data", {}).get("redirect_url", "") + if redirect_url_body and "request_token" in redirect_url_body: + parsed = urlparse(redirect_url_body) params = parse_qs(parsed.query) request_token = params.get("request_token", [None])[0] - break - if not location or twofa_resp.status_code not in (301, 302, 303, 307, 308): - break - twofa_resp = session.get(location, allow_redirects=False, timeout=15) + except Exception: + pass + + if not request_token: location = twofa_resp.headers.get("Location", "") + for _ in range(10): + if "request_token" in location: + parsed = urlparse(location) + params = parse_qs(parsed.query) + request_token = params.get("request_token", [None])[0] + break + if not location or twofa_resp.status_code not in (301, 302, 303, 307, 308): + break + twofa_resp = session.get(location, allow_redirects=False, timeout=15) + location = twofa_resp.headers.get("Location", "") if not request_token: raise AutoLoginError(