Skip to content

blue login --force never opens a browser when the stored session cannot be refreshed #42

Description

@cubeorgdev

What happened

After #28, every path that starts a new device flow goes through reauthenticate_replacing (crates/gh-cli/src/commands.rs:463), which only opens a browser when the refresh grant either succeeds or is rejected with exactly {"error":"invalid_grant"}. Anything else comes back as RefreshFailure::Temporary and is propagated as a hard error.

The problem is that Temporary also covers sessions that can never refresh:

  • no refresh_token / token_endpoint / client_id on the session — Session::refreshed and refresh_form return Temporary(Unauthorized(...)) (crates/gh-service/src/identity.rs:107, :149). You get this from an OIDC deployment whose configured scopes omit offline_access; the list is operator-supplied and only validated for empty strings (discovery.rs:90).
  • a dead grant rejected with anything other than invalid_grantclassify_refresh_rejection (identity.rs:288) maps invalid_client, unauthorized_client, or an HTML 401 from a proxy in front of the token endpoint to Temporary(Service(...)). That happens when the OAuth client is rotated or deleted in the IdP.

In that state blue, blue login, and blue login --force all fail with "login expired — run blue login again" and never open a browser. blue logout && blue login is the only way out, and nothing tells the user that. It's the same dead end as #27, except the message now names the command that can't work.

Sign-in looks fine, because the access token is still valid — it only shows up at the first refresh, ~15 minutes later.

Expected: blue login --force should always be able to authorize a replacement. A session that can't be refreshed should be treated like invalid_grant (best-effort gateway revoke, then a device flow), not like an outage. Genuine transport failures and 5xx should keep today's behaviour of refusing to open a browser.

Suggested fix: split the classification — keep Temporary for transport errors and 5xx, and add an "unusable" variant for absent credentials and 4xx rejections, handled the same way InvalidGrant is today. Rule of thumb: a 4xx from a token endpoint means the grant is dead.

Not reachable on the reference deployment: deploy/blue.toml requests offline_access, and Better Auth returns invalid_grant for a revoked grant — which is why the e2e coverage passes.

Steps to reproduce

  1. blue login against an OIDC deployment, and let it succeed.
  2. Simulate a session that cannot refresh — either drop refresh_token from ~/.config/blue/session.json (keeping the file valid JSON), or point the deployment at an identity.scopes list without offline_access and log in again.
  3. Age the session past its access-token expiry: set expires_at to a past timestamp.
  4. Run blue login --force.

Expected: a device-authorization URL. Actual: exits non-zero with "login expired — run blue login again", no browser. Same for blue login and bare blue. Only blue logout && blue login recovers.

Blue version

0.1.0 (fix/cli-login-cannot-recover-dead-session, 315efd7)

OS / environment

Linux; any OIDC deployment. Found by code inspection while reviewing #28, not hit on a live tenant.

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