Two-factor sign-in
Keep a site's sign-in details as a password credential once (user name, password and the 2FA setup key) and link it to a profile, then an agent run signs in with %NAME.username%, %NAME.password% and %NAME.otp%: the platform makes the current 6-digit code at the moment it is typed. The model never sees any of them, and they are typed only on that site.
When to use it: The AI should sign in to your site, which asks for an authenticator-app code, with nobody there.
| Uses | profiles.create, credentials.create (a password with a 2FA key), profiles.update (credential), agent.run with profile, agent.stream |
|---|---|
| Needs | a plan with password credentials (loginDetails), and a model on the API |
| Site | your sign-in page with an authenticator-app code (SIGNIN_URL). The runner uses its stand-in: a password page, then a real TOTP check |
| Output | output/result.json with the profile's id and what the credential shows of itself |
| Languages | Node and Python |
The code
The example as it is in its folder. Both versions do the same job and write the same output/, so one result check covers both.
/** * Two-factor sign-in: keep a site's sign-in details as a password credential once (user name, password and the 2FA * setup key) and link it to a profile, then an agent run signs in with %NAME.username%, %NAME.password% and %NAME.otp%: * the platform makes the current 6-digit code at the moment it is typed. The model never sees any of them, and they * are typed only on that site. * * SIGNIN_URL=https://… SITE_USERNAME=… SITE_PASSWORD=… SITE_TOTP_SECRET=… npx tsx node/index.ts (BOXLINE_API_KEY) * * SITE_TOTP_SECRET is the "setup key" the site shows when you turn on an authenticator app (or its otpauth:// link). * CREDENTIAL_NAME picks the credential's name (default TWO_FACTOR_EXAMPLE). With PROFILE_ID of a profile that already * links a password credential, the three are not needed. Needs a plan with password credentials (loginDetails). Writes * output/result.json. */import { mkdirSync, writeFileSync } from "node:fs";import { join } from "node:path";import { Boxline, CredentialExistsError } from "@boxline/sdk";const out = process.env.OUTPUT_DIR ?? "output";const signin = process.env.SIGNIN_URL;if (!signin) throw new Error("Set SIGNIN_URL to the site's sign-in page");const credentialName = process.env.CREDENTIAL_NAME ?? "TWO_FACTOR_EXAMPLE";const bx = new Boxline();// 1. The password credential, linked to a profile (sealed on the platform; only whether they are there is ever shown again).let profileId = process.env.PROFILE_ID;let made = false;let credentialMade = false;if (!profileId) { profileId = (await bx.profiles.create({ name: `${new URL(signin).host} (two-factor example)` })).id; made = true;}if (process.env.SITE_PASSWORD) { const { SITE_USERNAME: username, SITE_PASSWORD: password, SITE_TOTP_SECRET: totpSecret } = process.env; if (!username || !totpSecret) throw new Error("Set SITE_USERNAME and SITE_TOTP_SECRET with SITE_PASSWORD"); const origins = [new URL(signin).origin]; try { await bx.credentials.create({ name: credentialName, type: "password", origins, username, password, totpSecret }); credentialMade = true; } catch (err) { if (!(err instanceof CredentialExistsError)) throw err; await bx.credentials.update(credentialName, { origins, username, password, totpSecret }); } await bx.profiles.update(profileId, { credential: credentialName });}const saved = await bx.profiles.get(profileId);const credential = saved.credential ? await bx.credentials.get(saved.credential) : null;if (credential?.type !== "password") throw new Error(`profile ${profileId} links no password credential: set SITE_USERNAME, SITE_PASSWORD and SITE_TOTP_SECRET`);console.log(`Browser profile ${profileId} signs in with ${credential.name}: ${credential.username} on ${credential.origins.join(", ")}, 2FA ${credential.hasTotp ? "on" : "off"}`);// 2. An agent run in a session started from the profile: the profile's credential is added for you, and the AI types// its parts where the site asks for them.const run = await bx.agent.run({ task: `Open ${signin} and sign in: type %${credential.name}.username% as the email or user name and %${credential.name}.password% as the password. ` + `When the site asks for a verification code from an authenticator app, type %${credential.name}.otp%. Then tell me the name shown on the page.`, session: { profile: { id: profileId } }, maxSteps: 20,});console.log(`Agent run ${run.id} (${run.provider}/${run.model}) · Session: ${run.sessionId}`);for await (const e of bx.agent.stream(run.id)) { if (e.type === "tool") console.log(`→ ${e.name} ${JSON.stringify(e.input ?? {}).slice(0, 100)}`); // placeholders only else if (e.type === "done") console.log(`\n${e.status}: ${e.result ?? e.error}`);}const done = await bx.agent.get(run.id);mkdirSync(out, { recursive: true });writeFileSync( join(out, "result.json"), JSON.stringify( { signinUrl: signin, profileId, credential: { name: credential.name, username: credential.username, origins: credential.origins, hasTotp: credential.hasTotp }, agentRuns: [{ id: run.id }], status: done.status, answer: done.result, steps: done.steps.length, made: { profiles: made ? [profileId] : [], credentials: credentialMade ? [credentialName] : [] }, }, null, 2, ),);"""Two-factor sign-in: keep a site's sign-in details as a password credential once (user name, password and the 2FAsetup key) and link it to a profile, then an agent run signs in with %NAME.username%, %NAME.password% and %NAME.otp%:the platform makes the current 6-digit code at the moment it is typed. The model never sees any of them, and they aretyped only on that site. SIGNIN_URL=https://… SITE_USERNAME=… SITE_PASSWORD=… SITE_TOTP_SECRET=… python python/main.py (BOXLINE_API_KEY)SITE_TOTP_SECRET is the "setup key" the site shows when you turn on an authenticator app (or its otpauth:// link).CREDENTIAL_NAME picks the credential's name (default TWO_FACTOR_EXAMPLE). With PROFILE_ID of a profile that alreadylinks a password credential, the three are not needed. Needs a plan with password credentials (loginDetails). Writesoutput/result.json."""import jsonimport osfrom pathlib import Pathfrom urllib.parse import urlparsefrom boxline import Boxline, CredentialExistsErrorout = Path(os.environ.get("OUTPUT_DIR", "output"))signin = os.environ.get("SIGNIN_URL")if not signin: raise SystemExit("Set SIGNIN_URL to the site's sign-in page")u = urlparse(signin)credential_name = os.environ.get("CREDENTIAL_NAME", "TWO_FACTOR_EXAMPLE")bx = Boxline()# 1. The password credential, linked to a profile (sealed on the platform; only whether they are there is ever shown again).profile_id = os.environ.get("PROFILE_ID")made = Falsecredential_made = Falseif not profile_id: profile_id = bx.profiles.create(f"{u.netloc} (two-factor example)")["id"] made = Trueif os.environ.get("SITE_PASSWORD"): username, totp_secret = os.environ.get("SITE_USERNAME"), os.environ.get("SITE_TOTP_SECRET") if not username or not totp_secret: raise SystemExit("Set SITE_USERNAME and SITE_TOTP_SECRET with SITE_PASSWORD") origins = [f"{u.scheme}://{u.netloc}"] try: bx.credentials.create(credential_name, "password", origins=origins, username=username, password=os.environ["SITE_PASSWORD"], totp_secret=totp_secret) credential_made = True except CredentialExistsError: bx.credentials.update(credential_name, origins=origins, username=username, password=os.environ["SITE_PASSWORD"], totp_secret=totp_secret) bx.profiles.update(profile_id, credential=credential_name)saved = bx.profiles.get(profile_id)credential = bx.credentials.get(saved["credential"]) if saved.get("credential") else Noneif not credential or credential["type"] != "password": raise SystemExit(f"profile {profile_id} links no password credential: set SITE_USERNAME, SITE_PASSWORD and SITE_TOTP_SECRET")name = credential["name"]print(f"Browser profile {profile_id} signs in with {name}: {credential['username']} on {', '.join(credential['origins'])}, 2FA {'on' if credential['hasTotp'] else 'off'}")# 2. An agent run in a session started from the profile: the profile's credential is added for you, and the AI types# its parts where the site asks for them.run = bx.agent.run( f"Open {signin} and sign in: type %{name}.username% as the email or user name and %{name}.password% as the password. " f"When the site asks for a verification code from an authenticator app, type %{name}.otp%. Then tell me the name shown on the page.", session={"profile": {"id": profile_id}}, max_steps=20,)print(f"Agent run {run['id']} ({run['provider']}/{run['model']}) · Session: {run['sessionId']}", flush=True)for e in bx.agent.stream(run["id"]): if e["type"] == "tool": print(f"-> {e['name']} {json.dumps(e.get('input') or {})[:100]}") # placeholders only elif e["type"] == "done": print(f"\n{e['status']}: {e.get('result') or e.get('error')}")done = bx.agent.get(run["id"])out.mkdir(parents=True, exist_ok=True)result = {"signinUrl": signin, "profileId": profile_id, "credential": {"name": name, "username": credential["username"], "origins": credential["origins"], "hasTotp": credential["hasTotp"]}, "agentRuns": [{"id": run["id"]}], "status": done["status"], "answer": done["result"], "steps": len(done["steps"]), "made": {"profiles": [profile_id] if made else [], "credentials": [credential_name] if credential_made else []}}(out / "result.json").write_text(json.dumps(result, indent=2, ensure_ascii=False))Inputs
Environment variables the example reads:
SIGNIN_URL: your sign-in page (required).SITE_USERNAME: your user name or email.SITE_PASSWORD: your password (keep it in the environment, never in code).SITE_TOTP_SECRET: the site's 2FA setup key (base32) or otpauth:// link, as shown when you turn on an authenticator app.CREDENTIAL_NAME: the credential's name (defaultTWO_FACTOR_EXAMPLE).PROFILE_ID: a profile that already links a password credential (then the three above are not needed).
Run it
Get the examples with git clone https://github.com/Boxline-dev/examples.git and open this one: cd examples/two-factor-sign-in. Put your API key in BOXLINE_API_KEY (and BOXLINE_API_URL for another API), then run either version from the example’s folder. Both write to output/ and stop their sessions when they finish.
# Node 18 or newer, in the example's folderexport BOXLINE_API_KEY=… # your API keynpm install @boxline/sdk tsxnpx tsx node/index.ts# Python 3.9 or newer, in the example's folderexport BOXLINE_API_KEY=… # your API keypip install boxline-sdkpython python/main.pyThen check the result, from the same folder: npx tsx check.ts output. It exits with 0 only when the result is right.
What its check proves
check.ts reads what the example wrote and passes only when the result is right, not merely when the example ran.
On the stand-in, the site saw the password and then a valid 2FA code, the answer names the signed-in user, the run holds none of the user name, the password or the code, and the credential shows only its user name and that it has a 2FA key.
Source
github.com/Boxline-dev/examples/two-factor-sign-in on GitHub, with its README.md, check.ts and .env.example. The examples are open source. See all examples.
These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.