#!/usr/bin/env python3 """ investigate-2fa-login.py -- Detailed investigation of the TOTP login flow. Dumps the OIDC interaction page at multiple stages to find the TOTP prompt. Usage: docker compose run --rm --entrypoint python3 provision investigate-2fa-login.py """ import os, sys, time from pathlib import Path from playwright.sync_api import sync_playwright sys.path.insert(0, os.path.dirname(__file__)) from bw_helper import BitwardenHelper CLOUDRON_BASE = os.environ.get("CLOUDRON_BASE", "https://my.knownelement.com") STATE_DIR = Path("/app/state") BW_ITEM = "vp-techops Cloudron" EMAIL = "tsgstaff-coo-vptechops@turnsys.com" def dump_page_state(page, label): """Dump URL, all visible inputs/buttons, and page text snippet.""" ts = time.strftime("%H%M%S") page.screenshot(path=str(STATE_DIR / f"investigate-{label}-{ts}.png"), full_page=True) url = page.url inputs = page.evaluate("""() => { return Array.from(document.querySelectorAll( 'input, [role="button"], button, [id*="totp" i], [id*="Totp"]' )).filter(el => { return el.offsetParent !== null || el.style.display !== 'none'; }).map(el => ({ tag: el.tagName, type: el.type || '', id: el.id || '', name: el.getAttribute('name') || '', role: el.getAttribute('role') || '', placeholder: el.placeholder || '', text: (el.textContent || '').trim().substring(0, 40), visible: el.offsetParent !== null, })); }""") body_text = page.evaluate("() => document.body.innerText.substring(0, 500)") print(f"\n--- {label} ---") print(f"URL: {url}") print(f"Inputs/buttons ({len(inputs)}):") for inp in inputs: print(f" <{inp['tag']}> type={inp['type']} id={inp['id']} name={inp['name']} " f"role={inp['role']} placeholder={inp['placeholder']} text={inp['text']} " f"visible={inp['visible']}") print(f"Body text: {body_text[:200]}") (STATE_DIR / f"investigate-{label}-{ts}.txt").write_text( f"URL: {url}\n\nInputs: {inputs}\n\nBody: {body_text}") def main(): bw = BitwardenHelper( client_id=os.environ["BW_CLIENTID"], client_secret=os.environ["BW_CLIENTSECRET"], password=os.environ["BW_PASSWORD"], server_url=os.environ.get("BW_SERVER", ""), ) bw.login() password = bw.get_item_password(BW_ITEM) with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_context(viewport={"width": 1280, "height": 1024}).new_page() page.goto(f"{CLOUDRON_BASE}/login.html", wait_until="networkidle", timeout=15000) page.wait_for_timeout(2000) dump_page_state(page, "01-initial-load") # Fill password and submit page.wait_for_selector("#inputPassword", timeout=15000) page.click("#inputUsername") page.keyboard.type(EMAIL) page.click("#inputPassword") page.keyboard.type(password) dump_page_state(page, "02-form-filled") page.locator('[role="button"]:has-text("Log in")').first.click() # Wait and check multiple times for TOTP field for wait in [2, 3, 5, 5]: page.wait_for_timeout(wait * 1000) dump_page_state(page, f"03-after-login-{wait}s") totp = page.query_selector("#inputTotp") if totp and totp.is_visible(): print(f"\n=== TOTP FIELD FOUND after {wait}s! ===") code = bw.get_totp(BW_ITEM) print(f"Entering TOTP: {code}") totp.click() page.keyboard.type(code) page.locator('[role="button"]:has-text("Log in")').first.click() page.wait_for_timeout(5000) dump_page_state(page, "04-after-totp") print(f"Final URL: {page.url}") break else: print("\n=== No TOTP field found at any wait interval ===") # Check if we're already logged in if "login" not in page.url.lower() and "openid" not in page.url.lower(): print("Already past login (maybe session was active)") browser.close() if __name__ == "__main__": main()