fix(auth): harden the desktop login-code flow (GTM-93)#1249
Open
deepme987 wants to merge 15 commits into
Open
Conversation
…in codes (GTM-93)
…t timings Review follow-ups on the desktop-login-code flow: - signInWithCustomToken/lookupAccount now go through the same timeout-guarded postJson as the login-code endpoints, so a hung identitytoolkit call fails the flow instead of stalling it forever with the banner up and no sign_in_failed. - createDesktopLoginCode rejects grants with expires_in or poll_interval <= 0 (tight-poll spin / instant expiry) while the legacy-bridge fallback is still available.
# Conflicts: # src/main/auth/firebaseBridge/index.ts
Review follow-ups on #1222. Each fix has a test that fails without it. - Re-check the view's origin before injecting the session. The inject script carries the Firebase refresh token into the page's main world, and minutes elapse between flow start and injection (browser sign-in + post-sign-in hold) — ample time for the view to have navigated away. Applied to the legacy bridge too, which has the same gap; there it only enforces when the start origin was determinable, so it cannot regress sign-in. - Bound the server-supplied grant timings. `> 0` alone admits Infinity, which makes the code deadline unreachable — the poll loop's only exit — so the loop never terminates and its banner/listener cleanup never runs. A seconds/milliseconds unit slip is the realistic source, so clamp rather than trust: poll_interval to [1,30]s, expires_in to <=900s. - Retry 408/429. Each sign-in issues tens of exchange polls, so a rate limiter on that endpoint is expected; treating it as terminal killed a sign-in whose code and browser tab were both still valid. - Send code_challenge_method=S256. RFC 7636 4.3 defaults an omitted method to `plain`. Ingest pins S256 server-side, so this is not exploitable today — it makes the contract explicit on the wire instead of an implicit cross-repo convention. - Emit error_status on sign_in_failed. The raw message stays out (it can carry response bodies), but without the HTTP status every failure collapses into one error_class/error_bucket pair — an old backend (404), a verifier mismatch (403), and a 5xx are indistinguishable exactly when the rollout needs them separated. - Fail safe if signInViaDesktopLoginCode throws. It was the only await outside a try, and the call site is fire-and-forget, so an unexpected throw meant no flow, no fallback, no sign_in_failed, and an unhandled rejection — a Sign in button that silently did nothing. Tests: assert the PKCE binding across create/exchange (the verifier presented at exchange is the preimage of the challenge presented at create). Mutation-checked: downgrading the challenge to plaintext, sending an unrelated verifier, and removing the origin guard each fail the suite. 123 tests green in src/main/auth; typecheck + lint clean.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Comment |
vi.fn(async () => ...) with no declared parameters types mock.calls as an empty tuple, so calls[0][1] fails typecheck. The file's other fetch mocks declare Parameters<typeof fetch>; match them.
deepme987
marked this pull request as ready for review
July 14, 2026 22:07
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.
Review follow-ups on #1222, raised as a separate PR against that branch rather than pushed onto it — @benceruleanlu owns the design, so these are proposals to accept/reject, not a fait accompli. Merge into #1222 if you agree.
Every fix has a test that fails without it. Nothing here changes the flow's shape.
What
> 0alone admitsInfinity, which makes the code deadline unreachable — and that deadline is the poll loop's only exit. The loop then never terminates and its banner/listener cleanup never runs. A seconds/ms unit slip is the realistic source, so clamp:poll_intervalto [1,30]s,expires_into ≤900s.code_challenge_method=S256plain. Ingest pins S256 server-side, so this is not exploitable today — it makes the contract explicit on the wire instead of an implicit cross-repo convention.error_statusonsign_in_failederror_class/error_bucketpair. An old backend (404), a verifier mismatch (403), and a 5xx are indistinguishable — exactly when the rollout needs them separated.signInViaDesktopLoginCodethrowsawaitoutside a try, and the call site is fire-and-forget. An unexpected throw meant no flow, no fallback, nosign_in_failed, and an unhandled rejection — a Sign in button that silently does nothing.On the legacy-bridge origin guard
The same injection gap exists on the loopback path, so the guard is applied there too — but it only enforces when the start origin was determinable, so it cannot regress a sign-in where
getURL()comes back empty. That path is shipped code; the new path's guard is strict.Testing
The PKCE binding had no coverage: the suite passed with the challenge downgraded to plaintext, with an unrelated verifier sent at exchange, and with
code_challengedropped entirely — becauseindex.test.tsmocks./clientand only ever assertedexchangeDesktopLoginCodecall counts, never its arguments. Added an assertion that the verifier presented at exchange is the preimage of the challenge presented at create.Mutation-checked — each of these now fails the suite:
code_challenge: codeChallengeS256(v)→code_challenge: v(plaintext downgrade)123 tests green in
src/main/auth(was 108). Typecheck + lint clean.Not in this PR (deliberately)
cloud.comfy.orgURL, and take over the account on approval. PKCE doesn't help — the attacker is the legitimate PKCE client. The standard mitigation is a shortuser_codeshown on the desktop that the approval page requires you to match (RFC 8628 §5.4; it's why Microsoft added number matching). That's a design change across three repos and needs an owner decision, not a patch.GTM-93 · follow-up to #1222