Skip to content

fix(login): resume the login in flight instead of restarting it (CL-8857) - #14

Merged
TheGreatAxios merged 4 commits into
mainfrom
cl-8857-resume-login-in-flight
Oct 20, 2026
Merged

TheGreatAxios merged 4 commits into
mainfrom
cl-8857-resume-login-in-flight

Conversation

@TheGreatAxios

@TheGreatAxios TheGreatAxios commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

The bug

Every attempt for a provider contends for the one registered redirect_uri, and the authorize page already open in the browser is bound to the state and PKCE verifier of the attempt that opened it. A second start therefore has two ways to fail, and both are reachable from an ordinary retry:

  • mountOAuthLogin starts a fresh login without ending the old one, so the callback server cannot bind the fixed port and the retry answers 409.
  • A caller that does end the old one first (as Solutions Builder's own hub mount did) mints a new state, and the page the operator is signing in on redirects into a listener that no longer recognises it — refused as state mismatch, with the sign-in lost. That is how this was found:
http://localhost:1455/auth/callback?code=…&state=B0IhJsI75VfLzlQyWFBiwCWkWZExfNUzfWwbk7qGa60

A well-formed state from generateState(), just not the one the listener then expected.

The change

createLoginRegistry (root export, no hono/@intx/*): one live login per key.

  • startOrResume(key, begin, { tag }) returns the handle already in flight, or starts one. resumed says which.
  • A login is resumed only for the tag that started it — one principal's authorization is not another's to continue. A different tag falls through to begin and contends for the port exactly as today.
  • An entry is released when its login settles, so the next attempt after a completed, failed or cancelled one starts clean. Observing completed there also stops a rejection landing unhandled when a caller only reads authorizeUrl.
  • cancel(key) ends the login in flight and frees the port.

mountOAuthLogin uses it: a start that collides with the same principal's pending login now answers 200 with that login's id and authorize URL instead of 409.

Also: a refused redirect is reported as a code — state_mismatch, provider_error (carrying the server's own error), no_code — instead of prose. What a person should be told differs by product and brand; what the redirect was does not. OAuthCallbackError carries the code too.

Breaking

CallbackServerConfig.failedHtml now takes CallbackFailure rather than a string. Both known consumers are ours; Solutions Builder's pin bump lands with the matching change.

Verification

bun run check — typecheck, lint, format, 58 tests pass (4 new for the registry: resume, tag isolation, release on settle, cancel). Not yet driven through a live ChatGPT sign-in; that happens on the Solutions Builder pin bump.

Hardening from security review

  • A mismatched redirect no longer ends the login. The listener is on a fixed port anything on the machine can reach; ending the wait on a bad state let any stray or crafted request (a stale tab, a page firing a link at localhost) cancel a legitimate sign-in. It is still refused with 400 and its code never trusted — the login just keeps waiting for its own.
  • mountOAuthLogin cleans up by login, not by provider. A settled login leaves the registry while its credential is still being stored; a newer login for the same provider (possibly another principal's) could then be cancelled by the TTL sweep reaching the older entry, or lose its id so a retry could not resume. Cancel now targets the login's own handle; the id is released only if it is still that login's.

59 pass. The mismatch change is covered by a test (a crafted redirect is refused, then the genuine one completes). The mount race has no test: mountOAuthLogin has no harness, since it needs a DB and tenant context. The fix comes from reading the code.

…857)

Every attempt for a provider contends for the one registered redirect_uri,
and the authorize page already open in the browser is bound to the state
and PKCE verifier of the attempt that opened it. Starting over therefore
either fails to bind the port or leaves that page redirecting into a
listener that no longer recognises its state, refused as a mismatch.

createLoginRegistry holds one live login per key and hands the same handle
back to a second start; an entry is released when the login settles. The
hub mount uses it, so a colliding start answers 200 with the pending
login's id rather than 409. A login is resumed only for the tag that
started it.

A refused redirect is also now reported as a code -- state_mismatch,
provider_error, no_code -- rather than prose, so each product maps it onto
its own copy.
close() stopped the listener but left waitForCode pending, so a caller
that gave up waited on a redirect that could no longer be received -- and
anything tracking the login, such as the new registry, never learned it
had ended and would resume a dead attempt.

CL-8857
The loopback listener is on a fixed port anything on the machine can
reach. Ending the wait on a mismatched state let any stray or crafted
request -- a stale tab, a page firing a link at localhost -- cancel a
legitimate sign-in. The redirect is still refused with 400 and its code
never trusted; the login keeps waiting for its own.

CL-8857
@TheGreatAxios
TheGreatAxios force-pushed the cl-8857-resume-login-in-flight branch from cd67cd5 to 3a720e6 Compare October 20, 2026 02:31
@linear-code

linear-code Bot commented Oct 20, 2026

Copy link
Copy Markdown

CL-8857

A login leaves the registry as soon as it settles, while its credential is
still being stored. A newer login for the same provider, possibly another
principal's, can be live by then: the TTL sweep reaching the older entry
cancelled the newer one, and its completion deleted the newer login's id,
so a retry could no longer be resumed. Cancel now targets the login's own
handle, and the id is released only if it is still that login's.

CL-8857
@TheGreatAxios
TheGreatAxios force-pushed the cl-8857-resume-login-in-flight branch from 3a720e6 to 16e1d79 Compare October 20, 2026 02:32
@TheGreatAxios
TheGreatAxios merged commit cd90c0b into main Oct 20, 2026
2 checks passed
@TheGreatAxios
TheGreatAxios deleted the cl-8857-resume-login-in-flight branch October 20, 2026 02:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant