Skip to content

bug: auth check + browserless cookie harvest keyed on labs.google cookie that flow.google.com never receives #644

Closed

Description

@ffroliva

Summary

On Flow's migrated flow.google.com origin, the Google SSO session is the Flow session — but gflow's auth check and browserless cookie harvest are both still keyed on the labs.google-scoped NextAuth cookie, which is never sent to the new host.

Measured 2026-09-03 on ffroliva and denon82, read-only, zero credits.

The measurement

__Secure-next-auth.session-token   domain: labs.google   -> NOT sent to flow.google.com

cookies actually SENT to flow.google.com:
  SAPISID, SID, HSID, SSID, APISID,
  __Secure-1PSID, __Secure-3PSID, __Secure-1PSIDTS, __Secure-3PSIDTS, ...

new jar domains after visiting: flow.google.com, .flow.google.com

Both profiles: signin_cta: 0, 52 and 47 button[aria-label] rendered — fully authenticated, on the .google.com SSO cookies alone.

Why it matters

1. The auth check inspects the wrong credential. api/client.py:209:

_FLOW_SESSION_COOKIE = "__Secure-next-auth.session-token"

used at client.py:589 (preread_session=...) and client.py:624. On a fully-migrated account this judges a cookie that is no longer in use, so gflow auth status and the preread can report a Flow session state that has nothing to do with whether the migrated host will authenticate.

2. The browserless cookie harvest collects the wrong set. auth/cookies.py:30:

httpx_cookies is intentionally limited to cookies scoped to labs.google.

with _name_value_cookies(cookies, flow_only=True) at cookies.py:132 and :181. Correct for labs.google; on flow.google.com it filters out precisely the cookies that do the authenticating.

This inverts the documented model — the existing note that "Flow session != Google SSO session" is true on labs.google and false on flow.google.com.

Suggested direction

Make the Flow-session predicate host-aware rather than cookie-name-fixed: on the migrated origin, a valid Google SSO session (SAPISID + __Secure-1PSID) is the credential. The existing google_session field on ChromeCookieSnapshot already carries that signal — it is computed from the full jar and then discarded for the migrated case.

Note this is not urgent for correctness of generation today (the browser sends whatever it holds), but it makes auth status and any headless path report the wrong thing on migrated accounts. It becomes load-bearing if #642's transport work proceeds.

Refs #639

Activity

  1. ffroliva commented on Sep 3, 2026

    @ffroliva
    OwnerAuthor

    Measurement update: this is LATENT, not currently reproducible — do not blind-fix

    Attempted to build the fix and measured first. The divergence this issue predicts cannot be produced today:

    labs_cookie_count: 5      labs_has_next_auth:   true
    google_cookie_count: 28   google_has_next_auth: false
    
    A) what verification actually probes (labs.google/fx/api/auth/session, labs cookies only):
       status 200, has_user: true, 651 bytes          <- works, on a migrated account
    
    B) does flow.google.com expose an equivalent?
       /api/auth/session      -> 200, 149,594 bytes, text/html
       /fx/api/auth/session   -> 200, 149,616 bytes, text/html
       /_/api/auth/session    -> 200, 149,609 bytes, text/html
       ...all three are the SPA catch-all, not an API.
    

    Two things follow:

    1. labs.google's auth BFF is still live and still issues __Secure-next-auth.session-token, even for an account whose frontend has fully migrated. So gflow auth status and the preread are still reading a real, valid credential. The false-negative scenario in the original report requires that cookie to die while the Google SSO session stays alive — which has not happened yet on either test account.

    2. There is no endpoint to switch the probe to. The migrated host has no session API; identity there lives in the page HTML and in the o30O0e batchexecute RPC (the stock Google people RPC). Re-pointing verification would mean adopting the batchexecute client from spike: flow.google.com is cookie-auth batchexecute — pure-Python, no-browser access PROVEN (3 RPCs, HTTP 200) #642, not a small edit.

    Per this project's rule — if a claim can't be verified in the current environment it is LIKELY, not CONFIRMED; keep the issue open and ship diagnostics rather than a blind fix — I am not implementing a fix. A fix built now would be guarding a failure mode nobody can demonstrate, on a code path that currently works.

    What would make this actionable:

    • An account where labs.google/fx/api/auth/session returns {}/no-user while flow.google.com/ still renders authenticated. That is the real divergence, and it is a single cheap probe on any account someone has.
    • Or labs.google beginning to 4xx/redirect the auth BFF, which would make it urgent rather than latent.

    Staying open, unlabelled as a bug for now — it is a correctly-identified latent risk with the mechanism documented, and the measurement above is what a future fix should start from rather than re-derive.

    Refs #642

  2. ffroliva commented on Sep 6, 2026

    @ffroliva
    OwnerAuthor

    Re-measured 2026-09-06 — still latent; and the token is not a migrated-host credential

    Ran the probe this issue's triage named as the thing that would make it actionable. Read-only, zero credits, profile denon82. Spike doc: docs/superpowers/spikes/2026-09-06-labs-vs-migrated-session-credential.md (PR #688).

    Q: is __Secure-next-auth.session-token valid for flow.google.com? No.

    Request host Cookies sent __Secure-next-auth.session-token SSO cookies
    labs.google/fx/tools/flow 3 no no
    flow.google.com/ 14 no yes (SAPISID, __Secure-1PSID)
    flow.google.com/project/x 14 no yes

    labs.google and google.com are different registrable domains, so it is never attached. Whether it would be honoured if forced into the header is reasoned from the issuer/validator split (Auth.js handle signed by the labs BFF vs Google SSO) — not measured, since that needs a live session and this profile's is dead.

    The divergence still has not appeared. The experiment needs Google SSO alive + labs Flow session dead. What I got is everything dead:

    labs.google/fx/api/auth/session -> 200, 2 bytes ({}), has_user=False
    flow.google.com/                -> redirects to /about (marketing), avatar=0
    myaccount.google.com/           -> redirects to the signed-out marketing page
    

    That is still worth something: it reaches the has_user=false arm that the 2026-09-03 measurement could not observe, and both hosts agree on signed-out. That is evidence against the two hosts having independent session lifetimes — the mechanism this issue's false-negative scenario depends on.

    Re-localizing the claimed defect sites, so a future fix does not start at the wrong line:

    • api/client.py:209 _FLOW_SESSION_COOKIE is a log field (preread_session=, client.py:624) plus the macOS: generation runs on bundled Playwright Chromium despite .gflow_browser_strategy=chrome → 401 AuthExpiredError on createProject #222 macOS seed — not the auth check.
    • The auth check is a live server probe (verify_flow_profile → fetch_flow_session_httpx → labs.google/fx/api/auth/session), not a cookie-name test. It asks the server, so it cannot "inspect the wrong credential"; on the measured state its GOOGLE_SESSION_ONLY verdict and re-login guidance were correct.
    • auth/cookies.py's flow_only=True is a deliberate origin boundary, correct for its only consumer (an httpx client that talks solely to labs.google). Widening it would send .google.com SSO cookies to labs.google — a regression, not a fix.

    So the harvest becomes wrong only once a browserless path to the migrated host exists, which is #642's scope and where that change belongs.

    Instrument trap now recorded: a raw httpx GET of flow.google.com/ returns ~146 KB of Angular SPA shell with WIZ_global_data for signed-in and signed-out alike. It is not an auth signal — the rendered DOM is.

    Still open, still unlabelled as a bug. The doc carries a re-runnable probe and the exact pass condition: divergence confirmed iff labs has_user=False and the migrated host renders an avatar. Anyone with a live migrated session can settle it in about a minute.

  3. ffroliva commented on Sep 6, 2026

    @ffroliva
    OwnerAuthor

    The sharper risk this exposes: a labs-deprecation re-login loop

    Follow-up to the measurement above. Tracing the auth path surfaced something more load-bearing than this issue as filed — recorded as Q4 in the spike doc (PR #688). Not implemented, for the same reason the original triage refused a fix: the trigger is unobserved.

    labs.google's session BFF is the sole oracle behind every login — including for profiles whose actual generation path never touches it (UI automation, the migrated composer). RealChromeStrategy.login (auth/real_chrome.py:255) ends with verify_flow_profile, and that asks labs.google/fx/api/auth/session and nothing else.

    If labs stops minting Flow sessions:

    1. User runs gflow auth login and signs in fully — SSO cookies land, flow.google.com renders authenticated.
    2. verify_flow_profile asks labs → no user → verified = False.
    3. real_chrome.py:262 unlinks .gflow_browser_strategy (rollback of the speculative write).
    4. User is told to re-run gflow auth login. → goto 1, forever.

    Step 3 is the damaging part: without that marker channel_for_profile stops returning "chrome", so FlowApiClient downgrades to bundled Chromium — which the surrounding comment identifies as the [[real-browser-auth-mandatory]] failure. A labs outage would not merely block login; it would degrade a profile that still works through the browser.

    The discriminator already exists. ChromeCookieSnapshot.google_session (SAPISID, computed from the full jar) already separates the states — only the remediation is unreachable in the bottom two rows:

    labs BFF SSO cookies outcome today remediation correct?
    user present — AUTHENTICATED ✅
    {} no SAPISID NO_SESSION → re-login ✅ re-login fixes it
    {} SAPISID GOOGLE_SESSION_ONLY → re-login ✅ today · ❌ under deprecation, never
    non-200 any VERIFICATION_ERROR → "network" ✅ today · ❌ under deprecation, permanent

    Why I did not build it. The trigger is unobserved, and the measurement above found the two hosts share a session lifetime — so "SSO alive + labs dead" may not be reachable while labs is healthy. A fix now would guard a failure nobody can demonstrate.

    Why it still deserves a decision. The check is nearly free, the failure is unrecoverable by the user, and it cannot be shipped after the deprecation — by then every affected user is already stuck on a version that loops.

    Suggested next step is /gflow:predict on "decouple profile validity from the labs.google BFF", weighing two slices, smallest first:

    1. Never let a probe failure delete a marker a successful browser login just earned — narrow, defensible today, removes the step-3 degradation on its own.
    2. Give GOOGLE_SESSION_ONLY / VERIFICATION_ERROR an escape hatch, so a user with a live SSO session is not told to repeat an action that cannot help.

    Happy to take either slice if you want it — flagging rather than building, since it is a design call.

  4. ffroliva commented on Sep 6, 2026

    @ffroliva
    OwnerAuthor

    Correction + a live-session measurement

    Two things: I have to retract part of my earlier comment, and the re-run with a live session settles the credential question and turns up something new.

    Retraction — I measured the wrong directory

    My first pass read %LOCALAPPDATA%\gflow-cli\profile_denon82. The real home is %LOCALAPPDATA%\ffroliva\gflow-cli\ — paths.default_home() is user_data_dir(APP_NAME, APP_AUTHOR) and APP_AUTHOR = "ffroliva" is part of the Windows path. The author-less directory also exists, holds one stale profile_denon82, and has none of the real home's scaffolding.

    Struck: "both hosts agree on signed-out, therefore evidence against independent session lifetimes." That was an abandoned profile being logged out. It proves nothing. The divergence remains unobserved with no evidence either way — unchanged from your 2026-09-03 triage.

    Worth flagging as a trap: a stale profile fails quietly. It decrypts, yields cookies, and answers every probe plausibly. Nothing in the output says "wrong account". The doc now resolves paths via profile_subdir(default_home(), name) and asserts existence.

    Q1b, now measured rather than reasoned

    Re-ran against the correct profile with a live verified session (denon82@gmail.com). One credential set per request to https://flow.google.com/:

    credential sent bytes account email in body GAIA id
    labs __Secure-next-auth.session-token only 146,488 ❌ ❌
    Google SSO cookies only 150,744 ✅ ✅
    no cookies at all (control) 146,497 ❌ ❌

    The labs token is indistinguishable from sending nothing — 9 bytes off the anonymous control, carrying no identity. It confers zero authentication on the migrated host.

    The probe instrument is now calibrated

    Previously the pass condition was an unvalidated guess. Both arms, same profile:

    profile state lands on avatar signin_cta
    logged out /about (marketing) 0 1
    live session / (app) 1 0

    So a future "no divergence" from this probe means no divergence, not broken instrument.

    New finding — the frontend has migrated, the auth BFF has not

    Navigating to labs.google/fx/tools/flow redirected to flow.google.com, while labs.google/fx/api/auth/session still answered 200 / 653 B / has_user=True for the same profile at the same moment.

    Google has moved the frontend off labs.google for this account and left the auth BFF behind. gflow's only auth oracle now sits on the host the user is actively being redirected away from.

    That is not the #644 divergence — but it means Q4's trigger is no longer hypothetical. The migration is visibly in progress, and the remaining step to the re-login loop is that one endpoint following the frontend it belongs to. I still would not blind-fix it, but slice 1 (never let a probe failure delete a marker a successful browser login just earned) now looks like cheap insurance rather than speculation.

    Updated spike doc + all of the above in PR #688.

  5. ffroliva commented on Sep 17, 2026

    @ffroliva
    OwnerAuthor

    Confirming this on 0.77.1 with a consequence I don't think is stated yet: it makes auth login
    a no-op, so the one command that could recover cannot.

    ffroliva, migrated host, session revoked server-side around 2026-09-02. Three surfaces reported
    green over it:

    command reported actual
    gflow auth status Flow session verified as ffroliva@gmail.com signed out
    gflow auth login --browser chrome authenticated, elapsed_s: 0.2, [OK] Flow session verified no sign-in window ever opened
    gflow auth list Session: present (every profile) signed out
    gflow image t2i FlowAppError exit 31, route: "/about" ← the truth
    {"strategy": "chrome", "source": "chrome", "user_email": "ffroliva@gmail.com",
     "probe": "in_context", "event": "auth_flow_session_verified", "cli_version": "0.77.1"}
    {"strategy": "chrome", "outcome": "authenticated", "elapsed_s": 0.2,
     "event": "auth_login_session_detected", "cli_version": "0.77.1"}

    0.2 s, no browser interaction. Run it again and you get the same thing — the stale cookie is
    exactly what convinces login there is nothing to do
    , so it is not a retry, it is a no-op. From a
    user's seat this is the worst shape the bug can take: every diagnostic says you are fine, and the
    documented remedy silently does nothing. It cost us about an hour, and the session before this one
    failed the same way without ever identifying the cause.

    scripts/dev/spike_host_lane.py was the only instrument that told the truth:

    [spike] labs      final=https://flow.google.com/about                         mat_icon=85
    [spike] migrated  final=https://accounts.google.com/v3/signin/challenge/pk
    [spike] VERDICT   SIGNED_OUT
    

    A cookie-jar check is also not a valid workaround — we tried and it misled us in the other
    direction. flow.google.com's OSID / __Secure-OSID were still dated 2026-09-02 after a
    successful interactive sign-in, i.e. the app session was live while the cookie looked 15 days stale.
    A free image t2i was the only cheap ground truth.

    Recovery that worked (no gflow command can currently do it): open the profile in real Chrome,
    sign in until the Flow project grid renders, close cleanly.

    "C:\Program Files\Google\Chrome\Application\chrome.exe" \
      --user-data-dir="<gflow profile dir>" --password-store=basic https://flow.google.com/
    

    Two traps in that recovery, both of which cost us an extra cycle: a Google sign-in is not enough
    (Flow mints its own app session — stopping at the account page looks identical to failure), and
    force-killing Chrome discards cookies that a clean exit would have flushed.

    Suggestions, in the order I'd value them:

    1. auth login should decide "already authenticated" from the server (verify_flow_profile),
      not the jar — and on a failed probe open the sign-in window instead of returning success.
    2. When the probe fails, flush the dead session automatically rather than requiring a user to guess
      at auth logout. Today recovery is undiscoverable from any gflow output.
    3. Fold the spike_host_lane.py discriminator into auth status so its answer matches reality.

    Also worth noting #471 is closed as completed, and its stated motivation — "the command users run
    to answer 'is my login working' can currently report OK when the session is dead"
    — is precisely
    what happened here. It does not hold on the migrated host, for the reason this issue identifies.

    Related: #480 (import the session from the user's real Chrome) would have removed the entire manual
    recovery above.

  6. ffroliva commented on Oct 24, 2026

    @ffroliva
    OwnerAuthor

    Superseded by #1025 (v0.87.0). labs.google's session URL now answers 410, so verification runs the probe tied to flow.google.com's own Google session, and the labs cookie is only logged. Reopen if auth status says verified while flow.google.com sends the account to /about.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions