The form extraction endpoint returns every form on a rendered page as structured JSON — field names, types, action URLs, autocomplete attributes. To detect a login form, you filter for a form that contains a password field. One API call, one check, done.
But login form detection gets messy fast on the real web. Multi-step flows show the email field first and reveal the password field only after submission. SSO buttons redirect to Google or Microsoft without any password field at all. SPAs render their auth forms via JavaScript after page load, so static HTML parsing sees nothing. And shadow DOM components hide form fields from standard DOM queries entirely (SuperTokens documents that password managers can't even detect fields inside shadow roots).
This post walks through how to detect login forms programmatically using the Page Analysis API, including the edge cases that break simpler approaches.
curl -X POST https://api.screenshotrun.com/v1/analyze \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://app.example.com/login",
"include": ["forms", "interactive"]
}'
Why detect login forms programmatically?
There are more reasons than you'd expect. AI agents need to know when a page requires authentication before they waste tokens trying to extract content from a login wall. A GitHub issue on the agent-browser project specifically requests "automatic auth-wall detection" — without it, agents misinterpret login page DOM as actual content and produce garbage results.
Security teams map authentication endpoints during reconnaissance. Every login form is an entry point — credential stuffing, brute force, session hijacking, account takeover. Pentesting tools like LOGSENSOR and Acunetix have built-in login page identifiers for exactly this reason. And compliance audits (PCI DSS, SOC 2) require inventories of authentication surfaces across vendor portals.
Then there's the monitoring angle. Did a developer accidentally expose a staging login page? Did a third-party plugin inject an unexpected auth form? Did your SSO configuration change without anyone noticing? Scheduled form extraction catches these changes before they become incidents.
The basic detection pattern
At its core, login form detection is simple: find a form with a password field. A Python script that does it with the API:
import requests
API_KEY = "YOUR_API_KEY"
def has_login_form(url):
"""Check if a URL contains a login form."""
resp = requests.post(
"https://api.screenshotrun.com/v1/analyze",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"url": url,
"include": ["forms", "interactive"],
"stealth": True,
},
timeout=60,
)
result = resp.json()["result"]
for form in result.get("forms", []):
fields = form.get("fields", [])
has_password = any(f.get("type") == "password" for f in fields)
if has_password:
return {
"detected": True,
"action": form.get("action", ""),
"method": form.get("method", "POST"),
"fields": [
{"name": f["name"], "type": f["type"]}
for f in fields
],
}
return {"detected": False}
result = has_login_form("https://app.example.com")
if result["detected"]:
print(f"Login form found: {result['method']} → {result['action']}")
for f in result["fields"]:
print(f" {f['name']}: {f['type']}")
else:
print("No login form detected")
That covers the straightforward case — a page with a visible <form> containing input[type="password"]. Works for most traditional login pages. But the real web isn't that clean.
What makes login form detection hard?
Five things break the simple password-field check:
Multi-step login flows. Sites like Intuit, Microsoft, and many banks use "identifier-first" authentication. You see an email field on the first screen. The password field (or passkey prompt, or OTP input) appears only after you submit the email. A single page snapshot catches step one but misses the actual credential entry.
SSO buttons without forms. About 28% of websites support SSO login according to research from SSO-Monitor. A "Sign in with Google" button isn't inside a <form> tag and has no password field. It's a link or a button that redirects to an external identity provider. The clickable element extraction catches these — look for buttons or links containing "sign in", "log in", or OAuth provider names.
JavaScript-rendered forms are the third problem. React and Vue apps — really any SPA framework — often render login components after the initial page load. Static HTML analysis (curl, wget, regex) sees an empty shell. The API renders the page in Chromium and waits for JavaScript execution, so it returns the fully rendered form.
Shadow DOM. Web components can encapsulate form fields inside shadow roots. Standard document.querySelectorAll('input[type="password"]') won't find them. SuperTokens explicitly warns that password managers like Dashlane and LastPass fail to detect fields inside shadow DOMs. The Page Analysis API walks the rendered DOM including open shadow roots.
And then there are CAPTCHAs. CAPTCHA services like reCAPTCHA and hCaptcha trigger on the login page itself. They don't prevent form detection (the form fields still exist in the DOM), but they do signal that the page is an auth endpoint. The API's stealth mode reduces the chance of triggering a challenge during analysis.
Detecting SSO and OAuth login buttons
Not every login page has a traditional form. Some only offer "Sign in with Google" or "Continue with Microsoft." To catch these, combine form extraction with interactive element analysis:
SSO_KEYWORDS = [
"sign in with", "log in with", "continue with",
"google", "microsoft", "apple", "facebook", "github",
"sso", "saml", "okta", "auth0",
]
def detect_auth_page(url):
"""Detect both traditional login forms and SSO buttons."""
resp = requests.post(
"https://api.screenshotrun.com/v1/analyze",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"url": url,
"include": ["forms", "interactive"],
"stealth": True,
},
timeout=60,
)
result = resp.json()["result"]
signals = []
# Check for password fields in forms
for form in result.get("forms", []):
if any(f.get("type") == "password" for f in form.get("fields", [])):
signals.append("password_form")
# Check for SSO/OAuth buttons
for el in result.get("interactive", []):
text = el.get("text", "").lower()
href = el.get("href", "").lower()
combined = f"{text} {href}"
if any(kw in combined for kw in SSO_KEYWORDS):
signals.append(f"sso_button: {el.get('text', '')}")
# Check URL patterns
auth_paths = ["/login", "/signin", "/sign-in", "/auth", "/sso"]
if any(p in url.lower() for p in auth_paths):
signals.append("auth_url_pattern")
return {
"url": url,
"is_auth_page": len(signals) > 0,
"signals": signals,
}
page = detect_auth_page("https://app.example.com/login")
print(f"Auth page: {page['is_auth_page']}")
for s in page["signals"]:
print(f" → {s}")
This catches traditional password forms and SSO/OAuth buttons, plus URL-pattern hints as a secondary signal. The more signals fire, the higher your confidence.
Scanning multiple URLs for login pages
The real value shows up at scale. After subdomain enumeration or sitemap parsing, you've got dozens or hundreds of URLs. Which ones have login forms? Which expose admin panels?
urls = [
"https://app.example.com",
"https://admin.example.com",
"https://api.example.com",
"https://staging.example.com",
"https://portal.example.com",
"https://blog.example.com",
]
for url in urls:
try:
result = detect_auth_page(url)
if result["is_auth_page"]:
print(f"AUTH: {url}")
for s in result["signals"]:
print(f" {s}")
else:
print(f" ok: {url}")
except Exception as e:
print(f"FAIL: {url} ({e})")
Six URLs take about ten seconds. Fifty take under a minute. You get a structured inventory of every authentication endpoint across your attack surface (or your own infrastructure, if you're doing defensive monitoring).
Common login form field patterns
When the API returns form fields, certain patterns reliably indicate a login form versus a search box or newsletter signup:
autocomplete="current-password"— strongest signal. Browsers use this to offer saved credentials, and it's the Chromium-recommended attribute for login forms.input[type="password"]paired withinput[type="email"]orinput[type="text"]— classic username + password combo.- Field names containing "pass", "pwd", "password", "passwd" — varies by framework but consistently present.
- Form action pointing to
/auth,/login,/session,/api/authenticate— the action URL reveals the endpoint even when field names are obfuscated. autocomplete="username"on the text field — another Chromium hint that the form is for credentials.
A registration form looks similar but usually has extra fields: confirm password, name, phone, terms checkbox. If a form has exactly two visible inputs (one text, one password) and a submit button, it's almost certainly a login form.
How this compares to doing it yourself
You could build login form detection with Puppeteer or Playwright directly. Navigate to the page, run document.querySelectorAll('input[type="password"]'), check if any results came back. A few libraries try to automate this — html-login-field-detector on PyPI uses DistilBERT to classify form fields, and formasaurus uses logistic regression to categorize entire forms.
The problems start when you go to production. A browser instance eats a couple hundred megabytes per tab. Login pages often sit behind Cloudflare or Akamai, so you need bot evasion. SPAs won't show the form until JavaScript finishes. And every Chrome or Node update can break your setup. That's a lot of moving parts for what should be a simple check.
Our API handles all of that — browser, rendering, bot evasion, DOM parsing. Your code sends a URL and gets back JSON. Whether that tradeoff makes sense depends on scale — for checking five URLs once, Playwright is fine. But for scanning hundreds of URLs on a schedule, the infrastructure overhead adds up fast.
Putting it together
Login form detection is a filter: given a URL, is there an authentication wall? The answer feeds into whatever comes next — an AI agent deciding whether to request credentials, a security scanner logging the endpoint, a compliance tool checking MFA enforcement, or a monitoring script watching for unexpected auth pages on your own infrastructure.
The form extraction API gives you the raw data: every form on the page — fields, types, action URLs, attributes. The clickable element extraction adds SSO buttons and OAuth links. Combine them and filter for auth signals. The result is reliable login detection that handles JavaScript-heavy SPAs, shadow DOM components, bot-protected pages — no browser infrastructure on your side.