Headless, unattended OAuth login for scheduled jobs — drives the Shoonya login page with Selenium, captures the redirect code automatically, and exchanges it for an access token.
Overview
This automates the same 3-step OAuth flow described in Manual Login (OAuth), but without a human clicking through it. A headless Chrome session fills in the login form (User ID, Password, TOTP), submits it, and sniffs the network log for the redirect code — then exchanges it for an access token via NorenRestApiPy.
When to use thisScheduled jobs, cron/systemd timers, or CI environments that need a fresh token before market open with nobody present to complete the OAuth redirect by hand. If a person is present, use Manual Login (OAuth) instead — it's simpler and doesn't depend on the login page's DOM.
Fragility warningThis drives the login page's UI, not a documented API — it depends on the current markup of Shoonya's hosted login screen and can break silently if that page changes. See the Notes section below.
Prerequisites
Requirement
Notes
Python 3.9+
Tested against selenium ≥ 4.x
Chrome + matching chromedriver
Must resolve on PATH, or pass webdriver.Chrome(service=...)
32-char base32 TOTP seed used to generate the 6-digit OTP
SHOONYA_API_SECRET
App secret used in the code → token exchange
Never hard-code theseLoad all five values from environment variables or a git-ignored secrets file — never commit them to source.
How it works
Step by step:
Launches headless Chrome with performance logging enabled.
Opens the OAuth login URL and waits for the password field to render.
Fills the first three visible, non-hidden inputs on the page — in order — with User ID, Password, then a freshly generated TOTP code.
Clicks LOGIN, then polls Chrome's performance log for an outgoing request containing a code= query param — the same redirect described in Manual Login (OAuth) Step 2.
If no code appears within 60 seconds, regenerates the TOTP (in case the 30-second window rolled over) and retries once.
Tears down the browser, then exchanges the captured code for an access token via NorenApiPy.getAccessToken(...) — equivalent to the GenAcsTok call in Step 3 of the manual flow.
Full example
python
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import InvalidSessionIdException, WebDriverException
from urllib.parse import urlparse, parse_qs
import pyotp
import time
import json
import os
# ─── CONFIG ───────────────────────────────────────────────────────────# Load from environment variables (or a git-ignored .env / cred.yml) —# never hard-code these directly in the script.
CLIENT_ID = os.environ["SHOONYA_CLIENT_ID"] # e.g. "AB1234_U"
USER_ID = os.environ["SHOONYA_USER_ID"] # e.g. "AB1234"
PASSWORD = os.environ["SHOONYA_PASSWORD"]
TOTP_SECRET = os.environ["SHOONYA_TOTP_SECRET"] # 32-char base32 string
SECRET_CODE = os.environ["SHOONYA_API_SECRET"]
LOGIN_URL = (
"https://api.shoonya.com/OAuthlogin/investor-entry-level/login"
f"?api_key={CLIENT_ID}&route_to={USER_ID}"
)
TOKEN_URL = "https://api.shoonya.com/NorenWClientAPI/GenAcsTok"defscan_network_for_code(driver):
try:
logs = driver.get_log("performance")
for entryin logs:
try:
message = json.loads(entry["message"])["message"]
if message.get("method") == "Network.requestWillBeSent":
url = message.get("params", {}).get("request", {}).get("url", "")
if"code="in url and"shoonya"in url.lower():
parsed = urlparse(url)
code = parse_qs(parsed.query).get("code", [None])[0]
if code:
return code
except Exception:
continueexcept Exception:
passreturnNonedeffast_fill(driver, element, value):
element.click()
time.sleep(0.1)
element.clear()
element.send_keys(value)
time.sleep(0.1)
# ── Chrome, headless ────────────────────────────────────────────────
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--window-size=1920,1080")
options.set_capability("goog:loggingPrefs", {"performance": "ALL"})
driver = webdriver.Chrome(options=options)
wait = WebDriverWait(driver, 30)
auth_code = Nonetry:
print("Logging in to Shoonya (background)...")
driver.get(LOGIN_URL)
wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "input[type='password']")))
time.sleep(1)
all_inputs = driver.find_elements(
By.CSS_SELECTOR,
"input:not([type='hidden']):not([type='checkbox']):not([type='radio'])"
)
visible_inputs = [inp for inp in all_inputs if inp.is_displayed()]
fast_fill(driver, visible_inputs[0], USER_ID)
fast_fill(driver, visible_inputs[1], PASSWORD)
otp_value = pyotp.TOTP(TOTP_SECRET).now()
fast_fill(driver, visible_inputs[2], otp_value)
wait.until(EC.element_to_be_clickable((By.XPATH, "//button[normalize-space()='LOGIN']"))).click()
print("Credentials submitted. Capturing auth code...")
start = time.time()
whileTrue:
auth_code = scan_network_for_code(driver)
if auth_code:
print("Auth code captured.")
breakif time.time() - start > 60:
new_otp = pyotp.TOTP(TOTP_SECRET).now()
if new_otp != otp_value:
fast_fill(driver, visible_inputs[2], new_otp)
wait.until(EC.element_to_be_clickable((By.XPATH, "//button[normalize-space()='LOGIN']"))).click()
start = time.time()
otp_value = new_otp
continueprint("[TIMEOUT] Could not capture auth code.")
break
time.sleep(0.5)
except (InvalidSessionIdException, WebDriverException) as e:
print(f"[ERROR] Browser issue: {e}")
except Exception as e:
print(f"[ERROR] {e}")
finally:
try:
driver.quit()
except Exception:
passifnot auth_code:
raiseSystemExit("No auth code captured — aborting before token exchange.")
# ── Exchange auth code for an access token ─────────────────────────from api_helper import NorenApiPy # noqa: E402
api = NorenApiPy()
result = api.getAccessToken(auth_code, SECRET_CODE, CLIENT_ID, USER_ID)
if result isnotNone:
acc_tok, usrid, ref_tok, actid = result
print(f"Access token retrieved for account: {actid}")
# Store acc_tok / ref_tok wherever your app reads them from# (e.g. write back to the same git-ignored cred file) — avoid# printing full tokens to logs in anything but local debugging.else:
print("Failed to retrieve access token.")
Best practices
Never hard-code CLIENT_ID, PASSWORD, TOTP_SECRET, or API_SECRET — load them from environment variables or a git-ignored secrets file.
Don't log or print acc_tok / ref_tok in production; the sample only prints the account ID.
Cache the resulting access token (e.g. in a git-ignored cred file) instead of re-running the full Selenium flow on every process start — tokens are typically valid for the trading day.
Pin your chromedriver version to your installed Chrome version to avoid silent breakage after browser auto-updates.
Wrap this in a retry/alerting wrapper if it runs unattended — headless UI automation is inherently more brittle than a documented API call and can break on any login-page redesign.
Notes
This script drives the login page, not a documented API endpoint — it depends on the current DOM structure of the OAuth login screen and will break if that markup changes.
Field targeting is positional: it assumes the first three visible, non-hidden <input> elements on the page are, in order, User ID → Password → OTP. If the page adds or reorders fields, this fills the wrong inputs silently rather than erroring — prefer targeting by name/id/placeholder if the login page exposes stable attributes.
NorenApiPy is imported after the try/finally block, deliberately after the browser has already quit — the Selenium portion only needs to produce auth_code; the token exchange itself is a plain HTTPS call, same as Step 3 in Manual Login (OAuth).
If the TOTP window rolls over mid-submit, the script regenerates the code once and resubmits automatically; it does not retry indefinitely.
Overview
This automates the same 3-step OAuth flow described in Manual Login (OAuth), but without a human clicking through it. A headless Chrome session fills in the login form (User ID, Password, TOTP), submits it, and sniffs the network log for the redirect
code— then exchanges it for an access token viaNorenRestApiPy.Prerequisites
selenium≥ 4.xPATH, or passwebdriver.Chrome(service=...)selenium,pyotppip install selenium pyotpNorenRestApiPygetAccessTokenexchangeSHOONYA_CLIENT_IDAB1234_U)SHOONYA_USER_IDAB1234)SHOONYA_PASSWORDSHOONYA_TOTP_SECRETSHOONYA_API_SECRETHow it works
Step by step:
code=query param — the same redirect described in Manual Login (OAuth) Step 2.NorenApiPy.getAccessToken(...)— equivalent to theGenAcsTokcall in Step 3 of the manual flow.Full example
Best practices
CLIENT_ID,PASSWORD,TOTP_SECRET, orAPI_SECRET— load them from environment variables or a git-ignored secrets file.acc_tok/ref_tokin production; the sample only prints the account ID.Notes
<input>elements on the page are, in order, User ID → Password → OTP. If the page adds or reorders fields, this fills the wrong inputs silently rather than erroring — prefer targeting byname/id/placeholderif the login page exposes stable attributes.NorenApiPyis imported after thetry/finallyblock, deliberately after the browser has already quit — the Selenium portion only needs to produceauth_code; the token exchange itself is a plain HTTPS call, same as Step 3 in Manual Login (OAuth).Related
See Manual Login (OAuth) for the interactive flow this automates, and Token Renewal for refreshing a token without a full re-login.