Skip to content

auth: re-authorize when the access token expires without a refresh token - #1418

Merged
guglielmo-san merged 1 commit into
modelcontextprotocol:mainfrom
luiacuaniello:fix-reauthorize-without-refresh-token
Oct 20, 2026
Merged

guglielmo-san merged 1 commit into
modelcontextprotocol:mainfrom
luiacuaniello:fix-reauthorize-without-refresh-token

Conversation

@luiacuaniello

Copy link
Copy Markdown
Contributor

Fixes #1417.

When the token response carries no refresh_token, the token source that AuthorizationCodeHandler.Authorize built was oauth2.Config.TokenSource. Once the access token expired, its Token method failed with oauth2: token expired and refresh token is not set. setMCPHeaders in the streamable client transport only lets an invalid_grant RetrieveError through to the server, so every later request was rejected before it was sent, and Authorize was never called again. #917 handled the same hard-failed state for a rejected refresh token.

Issuing a refresh token is up to the authorization server, and the handler only asks for offline_access when RequestRefreshToken is set, so a token without one is a common case.

This uses oauth2.StaticTokenSource when there is no refresh token. The transport keeps sending the token after it expires, and the server's 401 goes through the existing re-authorization path. Token sources built by a custom NewTokenSource are unchanged.

Tests (each fails without the change):

  • TestAuthorize_ExpiredTokenWithoutRefreshToken: the token source keeps returning an expired access token that has no refresh token. Without the change, Authorize itself fails, because it reads the token back to record the granted scopes.
  • TestStreamableClientOAuth_ExpiredTokenWithoutRefreshToken: a session authorized by AuthorizationCodeHandler re-authorizes once the server rejects the expired token. Without the change, Connect fails.

go test ./..., go test -race on auth and mcp, go vet ./..., staticcheck v0.6.1 and scripts/client-conformance.sh (239 passed, same as main) are clean.

AI assistance: prepared with Claude Code; I reviewed and verified the change.

🤖 Generated with Claude Code

When the token response carries no refresh_token, the token source that
AuthorizationCodeHandler.Authorize built was oauth2.Config.TokenSource.
Once the access token expired, its Token method failed with "oauth2:
token expired and refresh token is not set". setMCPHeaders in the
streamable client transport only lets an invalid_grant RetrieveError
through to the server, so every later request was rejected before it
was sent, and Authorize was never called again. modelcontextprotocol#917 handled the same
hard-failed state for a rejected refresh token.

Issuing a refresh token is up to the authorization server, and the
handler only asks for offline_access when RequestRefreshToken is set,
so a token without one is a common case.

Use oauth2.StaticTokenSource when there is no refresh token. The
transport keeps sending the token after it expires, and the server's
401 goes through the existing re-authorization path. Token sources
built by a custom NewTokenSource are unchanged.

Tests (each fails without the change):
- TestAuthorize_ExpiredTokenWithoutRefreshToken: the token source keeps
  returning an expired access token that has no refresh token. Without
  the change, Authorize itself fails, because it reads the token back
  to record the granted scopes.
- TestStreamableClientOAuth_ExpiredTokenWithoutRefreshToken: a session
  authorized by AuthorizationCodeHandler re-authorizes once the server
  rejects the expired token. Without the change, Connect fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@guglielmo-san
guglielmo-san merged commit 7c501d7 into modelcontextprotocol:main Oct 20, 2026
9 checks passed
@guglielmo-san

Copy link
Copy Markdown
Contributor

@luiacuaniello thank you for the contribution!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mcp: a streamable client stops working once its access token expires, if no refresh token was issued

2 participants