Repository navigation
Ask for a sign-in's verification code only after the password - #780
Open
davidmckayv wants to merge 1 commit into
Open
davidmckayv wants to merge 1 commit into
davidmckayv wants to merge 1 commit into
Conversation
The private sign-in form asked for a username, a password and "a verification code, if the site asks for one" all at once, so a person had to guess whether a code was coming and type one before the site had sent it. The form now asks only for the username and password. The computer types them, submits, and if the site then shows a one-time-code field it reports codeRequested and stops. The request moves to a new code_required state, the form asks for the code the site just sent, and POST /api/sign-in-requests/:id/code has the computer type it into the page that is still waiting (new /sign-in/code on the computer, on the acting list like /sign-in/fill). A wrong code keeps the request in code_required with the site's refusal as its outcome; a site that falls back to its login page reopens the login form; an unanswered code expires with the request. Take over and cancel still work from the code step, and an abandoned code attempt reopens at the code step. The Bot is told only the final outcome, never the code. A typed login the site accepted is saved when the code is asked for, and a code is never saved. Audit: computer.sign_in_code_requested for the step, and computer.sign_in_failed carries step "login" or "code". Migration 0056 widens the status check to allow code_required.
davidmckayv
requested review from
MikeRyanDev,
guidovizoso,
mxmzb and
tylerslaton
as code owners
October 13, 2026 05:03
9 tasks done
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Problem. The private sign-in form asked for a username, a password and "Verification code, if the site asks for one" all at once. A person had to guess whether a code was coming, and type one before the site had sent it. If they left it blank and the site then asked, the Bot was told to have them take over the browser.
Rule this implements: every MFA code is asked for after the fact, never during or before.
New flow
/sign-in/fillreportscodeRequestedand stops. The request moves to a newcode_requiredstate instead of finishing or failing.POST /api/sign-in-requests/:id/codehas the computer type that code into the page that is still waiting for it (new/sign-in/codeon the computer, on the acting list next to/sign-in/fill).code_requiredand shows the site's refusal. A site that drops back to its login page reopens the login form. A code nobody enters expires with the request, and the person sees that. Take over and Cancel still work from the code step, and an abandoned code attempt (an expired filling lease) reopens at the code step.codeis no longer read onsubmitoruse-saved. An older form that sends one has it ignored, so it can never be typed early.Security notes
computer.sign_in_code_requestedevent for the step.computer.sign_in_failednow carriesstep: "login" | "code", andcomputer.sign_in_completedcarriesverificationCode: true. None of them include the value.Where it runs
code_requiredstatus on the existingsign_in_requestsrow in Postgres. The page waiting for the code lives in the Bot's own computer, as the login page always has. Nothing is held in server memory between the two requests.transition(code_required -> filling) used for the login, so two code submits racing end in one attempt.Boundary and audit
computer.sign_in_failed,step: "code").Changelog
CHANGELOG.mdunderUnreleased: the new flow, plus migration0056_sign_in_code_step(it widens thesign_in_requests.statuscheck). The server and the computer have to be upgraded together.Tests
Each fails before the change and passes after:
agent-computer/tests/sign-in.test.ts(real Chromium, runs in thecomputer-browserCI job): a fake site that shows a code field only after the password is accepted. The computer stops withcodeRequested: trueand leaves the code field empty, thenfillSignInCodefinishes on/home. Also covered: a refused code leaves the code field showing; a code field drawn astype=passwordis read as the code step, not a refused login; no code is typed on another origin or when no code field is showing. Before:SyntaxError: Export named 'parseSignInCode' not found.server/tests/passwords-sign-in.test.ts: the state machine. Submit login, getcode_required(Bot not told), submit code, getsigned_in(Bot told once). Also: wrong code; expiry while waiting for the code; code refused before the site asks; fallback to the login page; saved login; take over and cancel from the code step; lease reopen; the routes. Before: 9 failures.server/tests/passwords-store.integration.test.ts:code_requiredagainst real Postgres (the check constraint and the pending list).app/tests/sign-in-code-after-password.test.tsx: the realSignInFormagainst a fake server. Only username and password show first; the code field appears aftercode_required; a wrong code keeps asking; the code body is{code}alone. Before: fails atexpect(tree.queryByLabelText(/code/i)).toBeNull(), because the up-front code field is there.Live proof
I ran a full stack on this branch: the server, the app, and a computer image rebuilt from this branch. The site was a small local fake with login, then a code page, then an account page, using made-up test values. I drove it through the real app in Chrome:
computer_request_sign_in.POST /login credentials=acceptedthenGET /verify (code page shown). The dialog switched to "Enter the verification code".code=refusedand the dialog shows "The site did not accept that code…".code=acceptedthenGET /account (signed in). The Bot replied "You're signed in. The page says: Welcome back, Dana…".sign_in_requested,sign_in_code_requested,sign_in_failed {step: code},sign_in_completed {verificationCode: true}. The password and both codes have 0 hits in a fullpg_dumpof the database and 0 hits in the server log.