Issue refresh tokens that work
This check verifies that MCP Tower can exchange its refresh token for a new access token when the old one has expired, as an agent does every time a user comes back.
Check id token_refresh (token refresh). Results:
Why it matters
Section titled “Why it matters”Access tokens are short-lived. Without a working refresh, every user has to sign in again each time their token expires, often in the middle of a conversation.
- Claude refreshes tokens when your server answers
401, and also up to five minutes before the stored expiry. It expectsinvalid_grantwhen a refresh token is no longer valid, and for the public clients that CIMD and DCR create, it expects refresh tokens to be rotated, with the new refresh token in the same response that invalidates the old one (Claude: Token refresh). Claude asks foroffline_accesswhen your authorization server lists it inscopes_supported(Claude: PKCE and requested scopes). - The MCP authorization specification lets clients add
offline_accesswhen the authorization server lists it, tells them not to assume a refresh token will be issued, and tells MCP servers not to putoffline_accessin their own challenge or metadata, since refresh tokens are not a resource requirement (Refresh tokens).
What MCP Tower checks
Section titled “What MCP Tower checks”- Before a signed-in scan, MCP Tower looks at the stored access token. If there is none, or it
has not expired, no refresh is needed and the result is
skipped. - If it has expired, MCP Tower reads your authorization server metadata again and sends a
refresh_tokengrant to itstoken_endpoint, with the client it registered. - A new access token is
pass. The scan then continues with it. - An error response or an unreachable token endpoint is
fail. MCP Tower then clears the stored tokens for that user and keeps the client registration, so the next sign-in does not register again. The signed-in checks of that scan, such as token accepted, show “No usable token.”
Results
Section titled “Results”| Status | Message | Meaning | Fix |
|---|---|---|---|
| pass | The expired access token was refreshed. | Refresh works. | |
| fail | Refreshing the access token failed with HTTP <status>: <error> (<description>). | The token endpoint refused the refresh. <error> is usually invalid_grant. | Refresh token refused |
| fail | Refreshing the access token failed with HTTP <status>. | The token endpoint answered an error without an OAuth error field. | Refresh token refused |
| fail | Refreshing the access token failed: the token endpoint could not be reached (<error class>). | No answer from the token endpoint, or nothing to refresh with (see below). | Token endpoint unreachable |
| skipped | No refresh was needed. | The access token was still valid. |
Every failure has the same next step: “Sign in again. If this repeats, check that the provider
issues refresh tokens (offline_access).” <description> is the provider’s error_description,
cut at 200 characters. <error class> is dns_error, connect_error, tls_error, timeout or
address_blocked.
How to fix
Section titled “How to fix”The refresh token is refused
Section titled “The refresh token is refused”- Sign in again from the server’s page in MCP Tower. A refresh token that expired, was revoked, or was already used once is expected to fail, and a new sign-in replaces it.
- If the next refresh fails again, check that the authorization server issues refresh tokens to
this client at all. Many issue them only when the client asks for
offline_access, so listoffline_accessin the authorization server’sscopes_supportedand allow the client to request it. - Allow the
refresh_tokengrant for the client. MCP Tower registers with"grant_types": ["authorization_code", "refresh_token"]. - If you rotate refresh tokens, return the new refresh token in the same response that invalidates the old one.
- Answer an invalid refresh token with
400andinvalid_grant, notinvalid_requestor a custom code.
A token response that keeps the user signed in:
HTTP/1.1 200 OKContent-Type: application/jsonCache-Control: no-store
{"access_token":"<access-token>","token_type":"Bearer","expires_in":3600,"refresh_token":"<refresh-token>","scope":"mcp:tools"}And the error for a refresh token that can no longer be used:
HTTP/1.1 400 Bad RequestContent-Type: application/json
{"error":"invalid_grant","error_description":"refresh token is invalid"}The token endpoint cannot be reached
Section titled “The token endpoint cannot be reached”Check that token_endpoint in the authorization server metadata is an https URL that answers
from the public internet well within the time agents allow. Claude waits up to 30 seconds for a
refresh request
(Claude: Endpoint latency).
Check it yourself
Section titled “Check it yourself”Send the refresh grant an agent sends, with a refresh token from a recent sign-in of your own
client. For a public client, client_id goes in the body; a confidential client sends its
credentials with -u.
curl -si https://auth.example.com/token \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d grant_type=refresh_token \ -d refresh_token='<refresh-token>' \ -d client_id='<client-id>'The problem is fixed when the response is HTTP/1.1 200 OK with a new access_token and, if you
rotate, a new refresh_token. Send the same request again with the old refresh token: with rotation
it should now answer 400 with "error":"invalid_grant".
Then select Scan again on the server’s page.
References
Section titled “References”- MCP authorization
2026-07-28: Refresh tokens - RFC 6749 §6: refreshing an access token
- RFC 6749 §5.2: token error responses
and
invalid_grant - Claude: Token refresh and PKCE and requested scopes