[BUG] MCP OAuth tokens without expires_in and refresh_token silently expires

Open 💬 17 comments Opened Feb 17, 2026 by DenisPalnitsky

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

When an MCP server's OAuth token exchange endpoint /oauth/token returns only an access_token without expires_in or refresh_token (e.g. GitHub OAuth tokens), Claude Code silently expires the credential after some time (more than 30 minutes but less than 24 hours) and requires re-authentication.

After the token expires, Claude Code attempts the MCP initialize request without the Authorization header, which fails. The user must then manually re-authenticate via /mcp.

The token response from the OAuth server looks like this:

{
  "access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "scope": "scopes",
  "token_type": "bearer"
}

This is a valid OAuth 2.0 response — expires_in and refresh_token are optional per the spec. The access token itself is long-lived (does not expire on the server side), yet Claude Code stops using it after a certain period.

Two distinct issues here:

  1. Claude Code appears to impose an internal TTL on tokens that have no expires_in, causing valid long-lived tokens to be discarded prematurely.
  2. After the token is discarded, the MCP init request is sent without the auth header instead of prompting the user to re-authenticate before retrying.

What Should Happen?

  • If the OAuth token response does not include expires_in, Claude Code should treat the token as non-expiring and continue using it until the server rejects it with a 401.
  • If the token is rejected with 401, Claude Code should prompt the user to re-authenticate before retrying the MCP init request, rather than sending an unauthenticated request.

Error Messages/Logs

Steps to Reproduce

  1. Configure an MCP server that uses OAuth and does not have refresh token or expiration date for token (e.g. a GitHub-based MCP server)
  2. Add MPC to claude code with claude mcp add --transport http local-mcp http://localhost:8081/mcp
  3. Open Claude Code and authenticate via the OAuth flow — the server returns a token without expires_in or refresh_token
  4. Use Claude Code normally with MCP tools working
  5. Close cCaude Code
  6. Reopen after some period (observed: between 30 min and 24 hours)
  7. Navigate to /mcp and check that mcp shows "error" in authentication
  8. Observe that the MCP init request is sent without the Authorization header

Claude Model

None

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.44

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

iTerm2

Additional Information

_No response_

View original on GitHub ↗

17 Comments

github-actions[bot] · 4 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/9017
  2. https://github.com/anthropics/claude-code/issues/21333
  3. https://github.com/anthropics/claude-code/issues/12447

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

DocX · 4 months ago

Related to https://github.com/anthropics/claude-code/issues/9017 - However we believe it is not just documentation issue, but actual usability bug

yp-talia · 4 months ago

Adding another reproduction case for this bug, specifically via Claude Cowork (not Claude Code CLI).

Setup: Todoist MCP connector authenticated via OAuth in Claude Cowork, used by a scheduled task that fires daily at 9am and 5pm.

What happens: The tool call (find-tasks-by-date) returns HTTP 401: Unauthorized. A second retry also fails. Manually re-authenticating via Settings > Connectors fixes it immediately.

Confirmed with Todoist: Todoist's OAuth tokens do not expire server-side, and their hosted MCP server works correctly with other clients (ChatGPT, etc.). The 401 is caused by Claude sending an invalid or missing token, not by server-side expiry.

Community reports: Multiple users hitting the same issue with Todoist + Claude, particularly with scheduled/automated tasks: https://www.reddit.com/r/todoist/comments/1rqdk6h/is_it_normal_that_the_todoist_mcp_connector_in/

This is consistent with the two issues described in this bug — Claude imposing an internal TTL on tokens without expires_in, and then retrying without the Authorization header rather than prompting re-auth.

matthew-williams · 4 months ago

This also occurs with Atlassian/Slack cloud MCP servers . Replicated using Claude Code CLI. Using the /mcp skill shows "error" in authentication. Restarting the claude session AFTER /mcp auths correctly.

  • I did not have to re-authenticate via Settings > Connectors.
  • Only /mcp and a session restart was required.
  • Session restart without /mcp still shows "error" in authentication on second /mcp usage.
dylancwood · 4 months ago

Unfortunately, this is a known limitation — Claude Code doesn't implement automatic OAuth token refresh properly for GitHub, Todoist (or any provider, really). Ironically, Todoist and Github issue immortal OAuth tokens, but Claude still messes them up somehow.

I built Bindify to solve exactly this. It's an MCP auth proxy that sits between Claude Code and GitHub, Todoist, etc. You authenticate once through Bindify, get a secret URL, and paste it into Claude Code (or Claude.ai) as a remote MCP server. Bindify handles the OAuth token refresh server-side. If you still run into issues, you can fall back to API token authentication, even with Claude.ai/mobile via Bindify's secure URLs.

Setup guide: https://bindify.dev/fix/claude-todoist-mcp-expired — use code CLAUDEAUTHFIX to try it free for 2 months (limited to 10 people).

Disclaimer: I'm the developer behind Bindify. Feel free to reach out with questions.

OLH21 · 4 months ago

Considering that MCP is not encrypted, I would strongly discourage the use of such proxy. Not speaking of the authorization token handling. This is a use case for open source not for a paid service sorry.

Unfortunately, this is a known limitation — Claude Code doesn't implement automatic OAuth token refresh properly for GitHub, Todoist (or any provider, really). Ironically, Todoist and Github issue immortal OAuth tokens, but Claude still messes them up somehow. I built Bindify to solve exactly this. It's an MCP auth proxy that sits between Claude Code and GitHub, Todoist, etc. You authenticate once through Bindify, get a secret URL, and paste it into Claude Code (or Claude.ai) as a remote MCP server. Bindify handles the OAuth token refresh server-side. If you still run into issues, you can fall back to API token authentication, even with Claude.ai/mobile via Bindify's secure URLs. Setup guide: https://bindify.dev/fix/claude-todoist-mcp-expired — use code CLAUDEAUTHFIX to try it free for 2 months (limited to 10 people). Disclaimer: I'm the developer behind Bindify. Feel free to reach out with questions.
dylancwood · 4 months ago

Thanks @OLH21 — this is really valuable feedback. You’re right that trust is a prerequisite for a product like this, and you’re right that the proxy can see messages passing between the chat client and the MCP server. I take that seriously.
I’m planning to open source the core proxy code this week so people can verify what it does (and doesn’t do) with their data, and so anyone who prefers to self-host can do so. The hosted service will stick around for people who’d rather not manage their own infrastructure — not everyone using Claude with MCP connections is comfortable deploying their own services. The proxy doesn’t log request or response payloads — it only stores the metadata needed for token management.
I appreciate the pushback, genuinely. It’s making the product better.

Considering that MCP is not encrypted, I would strongly discourage the use of such proxy. Not speaking of the authorization token handling. This is a use case for open source not for a paid service sorry. > Unfortunately, this is a known limitation — Claude Code doesn't implement automatic OAuth token refresh properly for GitHub, Todoist (or any provider, really). Ironically, Todoist and Github issue immortal OAuth tokens, but Claude still messes them up somehow. > I built Bindify to solve exactly this. It's an MCP auth proxy that sits between Claude Code and GitHub, Todoist, etc. You authenticate once through Bindify, get a secret URL, and paste it into Claude Code (or Claude.ai) as a remote MCP server. Bindify handles the OAuth token refresh server-side. If you still run into issues, you can fall back to API token authentication, even with Claude.ai/mobile via Bindify's secure URLs. > Setup guide: https://bindify.dev/fix/claude-todoist-mcp-expired — use code CLAUDEAUTHFIX to try it free for 2 months (limited to 10 people). > Disclaimer: I'm the developer behind Bindify. Feel free to reach out with questions.
js-beaulieu · 3 months ago

Same thing happens on Claude.ai/Claude Desktop. Makes MCPs in this situation basically unusable through Claude. I can't realistically be expected to manually re-authentify every couple of hours for a bunch of services. No offense to your offering @dylancwood, but one would think we would not have to pay for a third-party proxy just to fix something that should work correctly by the product we pay for in the first place.

dylancwood · 3 months ago

@js-beaulieu : I agree for the most part. It should “just work”, and needing to reauthenticate all the time does make the MCP integrations almost useless. The open source claude code project just had some fixes land recently for auth token refresh, so that should help. My offering is just to help bridge the gap while Claude and other chat clients work through all the edge cases of managing OAuth on the client side. If you use the coupon code I provided for bindify.dev, you’ll get two months for free, and you don’t need to enter a credit card. A couple of people have signed up using that code already.

ibarrajo · 3 months ago

Confirmed a concrete real-world compatibility case for this.

I opened a corresponding Todoist MCP issue here:

A few details I was able to verify live:

  • https://ai.todoist.net/mcp is the hosted MCP endpoint and is POST-only.
  • https://ai.todoist.net/.well-known/oauth-protected-resource currently returns:
{
  "resource": "https://ai.todoist.net/mcp",
  "authorization_servers": ["https://todoist.com"],
  "bearer_methods_supported": ["header"],
  "scopes_supported": ["data:read_write"]
}
  • https://todoist.com/.well-known/oauth-authorization-server advertises:
  • authorization_endpoint: https://todoist.com/oauth/authorize
  • token_endpoint: https://todoist.com/oauth/access_token

I did not complete a successful OAuth exchange against the hosted service, so I did not directly inspect the returned token payload from the MCP flow itself.

However, Todoist's published OAuth docs still show a successful token exchange response containing only:

{
  "access_token": "...",
  "token_type": "Bearer"
}

Source:

So Todoist appears to be a plausible trigger for the behavior described in this issue: a valid OAuth token response without expires_in / refresh_token, followed by client-side local expiry and eventual unauthenticated MCP init.

From my perspective:

  • client-side fix belongs in Claude Code
  • server-side compatibility workaround is for Todoist to include accurate expires_in

But this issue still looks like the correct root bug to fix on the Claude side.

scottlovegrove · 3 months ago
So Todoist appears to be a plausible trigger for the behavior described in this issue: a valid OAuth token response without expires_in / refresh_token, followed by client-side local expiry and eventual unauthenticated MCP init.

Unfortunately, in the case of Todoist, we already provide an expiry of 10 years, so I'm not sure if that is the cause.

dylancwood · 3 months ago

I agree with @scottlovegrove . I think it's much more likely that Claude (code) receives the access_token with no expires_in date, then applies it's own default expires_in value internally. When that time is up, Claude can't refresh, so it treats it as an auth failure. I think that this behavior is typical of OAuth clients, and I ran into it when I used Claude Code to build Bindify.

@scottlovegrove : could Todoist OAuth response include a 10-year expires_in value to ensure that clients do not misunderstand?

scottlovegrove · 3 months ago
could Todoist OAuth response include a 10-year expires_in value to ensure that clients do not misunderstand?

As I said, we already do this, have done since November as our first stab at trying to resolve this issue. I even verified it yesterday, just to make sure that nothing had regressed, but it's still there.

If you want to confirm this for yourself, you can prompt your agent to dynamically create a client with Todoist (details are in the https://todoist.com/.well-known/oauth-authorization-server file), and ask it to guide you through the OAuth process and it will then be able to confirm what we return with the token.

ibarrajo · 3 months ago

OAuth Token Exchange Verification (2026-03-20)

Completed a full OAuth flow against Todoist's authorization server to verify expires_in behavior. Here's what I found.

Setup

  • Used dynamic client registration via POST https://todoist.com/oauth/register
  • PKCE (S256) authorization code flow
  • Local callback server on 127.0.0.1 to capture the code
  • Token exchanged at POST https://todoist.com/oauth/access_token

Token Response (redacted)

{
    "access_token": "<redacted>",
    "expires_in": 315360000,
    "token_type": "Bearer"
}

expires_in is present: 315,360,000 seconds = ~10 years. @scottlovegrove's claim is confirmed.

What's missing from the response

| Field | Present | Notes |
|-------|---------|-------|
| access_token | ✅ | |
| token_type | ✅ | Bearer |
| expires_in | ✅ | 315,360,000s (~10 years) |
| refresh_token | ❌ | Not provided |
| scope | ❌ | Not echoed back (recommended by RFC 6749 §5.1) |

The authorization server metadata also does not advertise refresh_token as a supported grant type — only authorization_code.

Implications for this bug

Since expires_in is present and set to ~10 years, the root cause is not a missing expiry field. The bug is more likely:

  1. Claude's OAuth client ignores the server's expires_in and applies its own short internal TTL. When that TTL expires and no refresh_token is available, it discards the token instead of continuing to use it.
  2. Token storage doesn't persist across session restarts — each new CLI session may start with a blank auth state.
  3. On internal expiry, Claude retries without the Authorization header (per the original issue description) rather than prompting re-auth.

The absence of refresh_token is by design (tokens are ~immortal), but if Claude's OAuth client treats "no refresh_token + internal TTL expired" as "auth failed, discard token" rather than "token is still valid, keep using it" — that would explain the behavior.

Separate issue: Revocation endpoint returns 401

While cleaning up, I attempted to revoke the test token via the endpoint advertised in Todoist's authorization server metadata (revocation_endpoint: https://todoist.com/api/v1/revoke):

POST https://todoist.com/api/v1/revoke
  Content-Type: application/x-www-form-urlencoded
  token=<access_token>&client_id=<client_id>
→ 401 Unauthorized

Also tried with Authorization: Bearer <token> header — same 401. The legacy endpoint (/sync/v9/access_tokens/revoke) returns 410 Gone. This may be a separate Todoist-side issue — flagging for @scottlovegrove. The endpoint is referenced in their OAuth authorization server metadata.

dylancwood · 3 months ago

Thanks @ibarrajo. I confirmed as well. I think that my initial implementation was looking for a refresh_token first, and if it wasn't there, it didn't even check for expires_in. Possible that Claude is doing the same, but that's on them.

BTW: I've open-sourced the src for the Bindify MCP Proxy here: https://github.com/dylancwood/bindify-proxy. Maybe it'll help someone :-)

MarcelBigger · 2 months ago

Over 200 MCP servers are listed by Anthropic and all of them have this authentication issue?

Marsssssssssssdsss · 1 month ago

@DenisPalnitsky The gap is Claude Code expecting expires_in/refresh_token in the OAuth response, but GitHub/Todoist don't provide them. Workaround: wrap the access token in a middleware that adds explicit expiry metadata before passing it to the session. The token itself is valid; the metadata is what's missing.