fix(login): stop reading a 401 from /login as "no such account" - #170
Merged
Conversation
POST /login no longer answers 401. An identifier with no usable account, which used to cover unknown, unverified, and no permitted method, now gets 200 and a decoy pre-auth token so the response cannot be used to test whether an account exists. Two branches in completeLogin read that 401 and are unreachable now: the "not verified yet" message and "No account was found for X". Both go, because there is no longer an answer for them to read. The messages around them were claiming something the CLI can no longer support. "A code was sent to X" becomes "If an account exists for X, a code is on its way", and "This account cannot use email otp login" is phrased as what the instance offered, since that list comes back for an unknown identifier too. An unknown identifier therefore runs the ordinary flow and fails at the code step. That is intended and not something the CLI can shortcut, so the final error names the identifier and points at registering rather than saying only that a code could not be verified. 423 is reported on its own terms with how long to wait, instead of falling through to a bare status. It is also the one answer left that does imply an account exists. Adds a conformance spec pinning the guarantee against a running instance. The loginMethods assertion is deliberately not equality between one account and one decoy: that list is filtered by what an account can do and a decoy's capabilities are derived per identifier, so any two can differ. What must hold is that a real account's list is one a decoy can also produce, and the first draft of the spec asserted equality and failed against a live instance for that reason.
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.
Follow-up to fells-code/seamless-auth-api#257, which made
POST /loginnon-enumerable.That change closed the oracle on the API side and broke the one branch in this repo that
depended on it.
What broke
POST /loginno longer answers401. An identifier with no usable account (unknown,unverified, or with no permitted continuation method) now gets
200and a decoy pre-authtoken, so the response cannot be used to test whether an account exists.
Two branches in
completeLoginread that401:loginFlow.ts:112mapped401+ a message matching/verify/to "The account for X isnot verified yet."
loginFlow.ts:122mapped401/403to "No account was found for X."Both are now unreachable for
401, and both are removed rather than rewired. There is nolonger an answer for them to read, which is the point of the upstream change.
The part that is not just deleting dead code
The surrounding messages were asserting something this CLI can no longer know:
A code was sent to dev@example.com.If an account exists for dev@example.com, a code is on its way.This account cannot use email otp login. Available methods: ...email otp login is not available for dev@example.com. Offered: ...Could not verify a code. Run seamless login to try again.Could not verify a code for X. If that address or number has no account yet, register it first. Otherwise run seamless login to try again.The middle one matters more than it looks:
loginMethodscomes back for an unknownidentifier too, so "this account cannot" was reporting an account that may not exist.
An unknown identifier now runs the ordinary flow and fails at the code step, after three
prompts. That is the intended behaviour and not something the CLI can shortcut, so the
final error says what the likely reason is instead of leaving the developer to guess.
403keeps a branch but no longer claims the account is missing, and423is now reportedon its own terms with how long to wait, rather than falling through to
Login request failed (423). It is also the one answer left that does imply an accountexists, which is a deliberate and documented tradeoff upstream.
Conformance spec
Adds
verify/harness/api/loginEnumeration.spec.tsto theapilayer. The API has unitcoverage for all of this; what only this harness can check is that the guarantee survives a
real instance with real signing keys and the real login policy.
Seven tests: same shape for a known and an unknown identifier, a stable subject and method
list across repeat probes, an OTP send that reports success and sends nothing, a verify that
fails the way a wrong code fails, a credential id that cannot exist refused identically for
both, and a malformed identifier still rejected with
400.The
loginMethodsassertion is worth reading before approving. My first draft assertedthat a decoy and a real account return the same list, and it failed against a live instance:
That is correct upstream behaviour, not a bug. The list is filtered by what an account can
actually do, and a decoy's capabilities are derived per identifier, so any one decoy and any
one account can legitimately differ. The guarantee is the weaker and more useful one: a real
account's list must be one a decoy can also produce, which is what stops a narrow list
being proof of existence. The spec asserts that over a sample of 40 identifiers (each combo
comes up about a quarter of the time, so missing it is roughly a 1-in-100,000 event, not a
flake).
I would not have found this without running the harness, which is the argument for the spec
existing.
Verification
npm run buildclean,npm test818 passed / 4 skipped,verify/harnesstsc --noEmitclean.
seamless verify --api-only --filter=loginEnumerationrun locally against the merged APIon
main: 7 passed, exit 0.Note for whoever runs this locally:
seamless verifybinds port 5312 and the compose filehardcodes it, so anything else on that port has to be stopped first. Unrelated to this
change, but it cost me a run to work out.