Login is a hot path. A real user will hit it dozens of times a month, so your error budget for false positives is small. The right pattern is a narrow action-time check at password submit - not a page-load probe - paired with a step-up challenge on
inconclusive rather than a block.The threat
Credential-stuffing tools replay dumps of leakedemail:password pairs against login endpoints at high volume. The goal is account takeover: any hit becomes a compromised account. Unlike signup, the attacker isn’t trying to create new identity - they’re trying to prove that an existing identity is theirs. That changes the policy calculus.
The pattern to detect is automated typing and submission: a headless browser driving a form fill, or an HTTP-level script bypassing the UI entirely. Foil’s automation attribution (Playwright/Puppeteer/Selenium) and ai-agent attribution (LLM-driven login) both land here.
Two properties of the login surface shape the integration:
- Users arrive hot. They click a link in an email, they open a bookmark, they come back from a timeout, and they can submit the form within a second of the page loading.
getSession()waits for fingerprinting to finish, so the handoff for these users can take a moment longer, but you never need to disable the form. - False positives are expensive. Locking a human out of their own account is a worse user experience than letting a bot try three times against a non-matching password. The verdict is one input among several - Foil goes on one side of a scale that already has rate limiting and MFA on it.
The flow
1
Start Foil on page load
Collection begins. You don’t need to disable the form while Foil collects, because
getSession() waits for fingerprinting to finish. Users arriving cold can submit right away.2
Call getSession() at password submit
Not on page load. You want the freshest possible observation set, and you want collection to include the keystroke and mouse signals from the user typing their password.
3
Verify and fold into your auth decision
A
bot verdict returns the same generic error as a wrong password. An inconclusive verdict with an otherwise-valid password triggers a step-up challenge.4
Rate-limit by visitor fingerprint, not just IP
The verified token carries a
visitor_fingerprint.id. Use it to apply per-device limits that survive residential-proxy IP rotation.Client integration
CallgetSession() lazily at password submit. getSession() waits for fingerprinting to finish before it returns a handoff, so you don’t need to call waitForFingerprint() first.
getSession() fails, the page sends the login request without a handoff, so a blocked script doesn’t leave the form unresponsive. The server treats a request without a valid token like a bot verdict, as the next section shows.
Server verification
Decisioning policy
Four things to get right:
- Always check the password. Skipping the bcrypt compare on a
botverdict creates a timing oracle. Run the comparison, throw the result away if Foil blocks. - Return identical responses for bot-detected and password-mismatch. Status code, body shape, timing. Any difference is signal for an attacker automating against your endpoint.
- Prefer step-up over block on
inconclusive. If the password is correct and the verdict is ambiguous, a legitimate user can pass a TOTP prompt. A bot running on a dump of leaked credentials usually can’t. - Fail closed and reject stale tokens. A request with a missing, invalid, or expired token gets the same generic error as a
botverdict. Don’t accept the login without a token, because an attacker could skip Foil by omitting it. The server SDKs don’t check a token’s age, so reject a token whosedecision.evaluated_atis more than a few minutes old. See Verify the token on your server for the check in each language.
Rate-limiting by visitor fingerprint
The verified token exposesvisitor_fingerprint.id - a per-device identifier that survives cookie clears, incognito mode, and residential-proxy IP rotation. It’s the right key for login attempt counters.
Node.js
The server SDKs type
visitor_fingerprint as nullable, so the example checks for a missing ID. Foil resolves the visitor fingerprint before it issues a token, so a token from the browser SDK carries one. If your code does see a missing ID, skip the fingerprint counter and rely on the IP counter instead of blocking the request.What’s next
Signup protection
Block automated account creation.
Server verification
Reference for the underlying verification primitive.
Going to production
Rollout plan and monitoring.
Verdicts & scoring
Understand what
inconclusive actually means.