Skip to content

blue login --force leaves the old gateway session live, so pre-force inference JWTs keep working for up to 12h #33

Description

@cubeorgdev

What happened

blue login --force retires the old OAuth refresh grant but not the old gateway session, so an inference JWT minted before the forced re-login keeps working for up to 12 hours.

logout does both: revoke_gateway_session() then revoke() (crates/gh-cli/src/commands.rs:808-811), as does detach_tenant (:332-335). The --force path calls only session.revoke() (:726), and so does retire_superseded_session_with on the implicit-reauth path (:459).

That gap matters because revoking the gateway session is what kills already-issued JWTs. The resolver query requires ga.revoked_at is null (services/control-api/src/lib.rs:4237), and renew_gateway_auth_session records reactivated_at, which the proxy enforces as a not-before against the JWT's signed iat (services/inference-proxy/src/main.rs:1837). Its doc comment states the intent directly — "A binding revoked by blue logout is reactivated only by a token issued after the revocation, which keeps every pre-logout JWT dead" (services/control-api/src/gateway_auth.rs:31-33). Skipping the call leaves revoked_at NULL and reactivated_at unset, so neither check fires.

Both outcomes of a forced re-login leave the old JWT live:

  • New browser session — the old gateway_auth_sessions row is untouched and still satisfies revoked_at is null, so the old JWT resolves until the old browser session expires.
  • Existing browser session reused — same oauth_session_id, no new binding, nothing invalidates the old JWT at all.

Either way the ceiling is the 12h inference JWT TTL (gateway_inference_token_ttl_seconds: 43_200), clamped by the backing browser session.

This contradicts what we document --force for. apps/docs/next/concepts/authentication.mdx:41 — "replacing a session you no longer trust ... so no grant is left live server-side" — and apps/docs/next/cli/commands.mdx:15 — "revokes the stored session". The JWT is the credential that actually buys inference, and SECURITY.md notes it is written into agent config/env, which is exactly what leaks when a session stops being trustworthy.

Expected

--force revokes the gateway session as well, matching logout, so no inference JWT survives the re-login.

Implementation note: revoke_gateway_session authenticates with the bearer access token, so the fix has to mirror logout's ordering — adopt_refresh_contextrefresh_if_neededrevoke_gateway_sessionrevoke. The --force branch currently calls revoke() on stored with none of that setup. Best-effort/warn-only is fine, as elsewhere.

Secondary: retire_superseded_session_with skips revocation entirely when the previous session has no refresh token, so the gateway session is never revoked on that path either. Lower risk, since implicit reauth usually follows a browser session that already expired (the resolver's source_expires_at > now() and "expiresAt" > now() predicates already fail), but it can also fire on a 403.

Introduced by #28--force does not exist on main, so nothing released is affected. Mechanism came from #17.

Steps to reproduce

Gateway-mode deployment:

  1. blue login, then run blue once so a gateway inference JWT is minted; copy it out of the managed agent config.
  2. blue login --force and complete the new device authorization.
  3. Send a request to the inference proxy with the JWT captured in step 1.

Observed: the request is authorized — it succeeds until the JWT's exp (up to 12h).
Expected: 401, as it is after blue logout && blue login.

Check select oauth_session_id, revoked_at, reactivated_at from public.gateway_auth_sessions where user_id = ... — after --force the pre-existing row still has revoked_at NULL and reactivated_at NULL; after logout it does not.

Blue version

0.1.0 — branch fix/cli-login-cannot-recover-dead-session (#28), commit 41c595c. Not present in any release.

OS / environment

Any; the gap is in the CLI revocation sequence, not platform-specific.

Activity

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions