Skip to content

MCP OAuth: re-login silently reuses the remote grant, so logout cannot change the authorized workspace #6040

Description

@Hmbown

Symptom

Founder: "i logged out of my linear mcp thing multiple times and logged back in but it's still using shannonlabs instead of shannon-labs so the logout part doesn't work."

linear.app/shannonlabs is a different workspace from the intended linear.app/shannon-labs (AGENTS.md calls this out explicitly). Logging out and back in never changes which one the token is bound to.

Local logout is not the bug

crates/tui/src/mcp/oauth.rs derives the credential key identically on all three paths:

fn store_key(server_name: &str, url: &str) -> String {
    // sha256(server_name || NUL || url), base64url-nopad
    format!("mcp_oauth_{}", URL_SAFE_NO_PAD.encode(digest))
}

load_oauth_tokens, save_oauth_tokens, and delete_oauth_tokens all call it with the same arguments, so delete removes exactly what save wrote. Verified on disk: for linear @ https://mcp.linear.app/mcp the expected key mcp_oauth_TMpjAqZY78w3fuGMxVKdWr2zFq1W5uSOUteXS7nvrgQ is absent from both backends — keychain service deepseek and the file store ~/.codewhale/secrets/secrets.json (0 mcp_oauth_ entries). Local state really is clear.

The actual defect

The authorization request carries no re-consent or account-selection parameter:

AuthorizationRequest::new(redirect_uri)
    .with_scopes(scopes.iter().copied())
    .with_client_name("Codewhale")

grep -n "prompt" crates/tui/src/mcp/oauth.rs returns no query-parameter hit on either the dynamic-registration path or the configured-client_id path (OAuthClientConfig::new(client_id, redirect_uri).with_scopes(...)).

So the sequence is:

  1. Logout deletes the local token — correctly.
  2. Login opens the provider's authorize URL.
  3. The provider still holds a live session and a standing grant for this client, bound to shannonlabs.
  4. With no prompt=consent / prompt=select_account, the provider redirects straight back with a fresh code — no picker is ever shown.
  5. A new token is written for the same wrong workspace.

The user is logged out locally and unchanged remotely. Nothing in the product can currently move that binding, which is why repeating the cycle changes nothing.

Why this also explains the 401 loop

Codewhale's Linear server has no stored token at all, so every boot 401s:

'linear-connect': MCP server https://mcp.linear.app/mcp rejected the request with 401 Unauthorized
'linear-live':    … 401 Unauthorized
'linear-live2':   … 401 Unauthorized

The endpoint itself is healthy — an unauthenticated probe returns HTTP 401 in 0.047s. The failure is credential state, not reachability or a hung server.

Worth confirming as part of this fix whether a completed browser flow is actually persisting: the founder reports logging in multiple times, yet no mcp_oauth_* entry exists in either backend. If login reports success without a durable write, that is a second defect on the same path and the more serious of the two.

Suggested shape

  • Send prompt=consent on an explicit /mcp login after a logout, so the provider always re-shows the account/workspace picker instead of silently re-granting.
  • Have /mcp logout <server> say plainly that it cleared local credentials only, and surface the provider's revocation URL — local deletion cannot revoke a remote grant, and the UI currently implies it can.
  • Use the RFC 7009 revocation endpoint when the server's metadata advertises one, so logout actually severs the grant rather than dropping our copy of it.
  • Assert the token persisted after a successful flow and fail loudly if it did not.

Evidence

  • crates/tui/src/mcp/oauth.rs: store_key (~line 1458), delete_oauth_tokens_for_server (~line 1344), AuthorizationRequest construction (~line 1701).
  • Keychain service deepseek (codewhale_secrets::DEFAULT_SERVICE) — no Linear entry.
  • ~/.codewhale/secrets/secrets.json — 1 key total, 0 mcp_oauth_ entries.
  • ~/.codewhale/logs/*.log — three distinct 401s across linear-connect, linear-live, linear-live2.

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

    No labels
    No labels

    Projects

    • Status
      Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions