Skip to content

50% off your first year. Join before 2027. Claim →

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:

pass fail skipped

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 expects invalid_grant when 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 for offline_access when your authorization server lists it in scopes_supported (Claude: PKCE and requested scopes).
  • The MCP authorization specification lets clients add offline_access when the authorization server lists it, tells them not to assume a refresh token will be issued, and tells MCP servers not to put offline_access in their own challenge or metadata, since refresh tokens are not a resource requirement (Refresh tokens).
  1. 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.
  2. If it has expired, MCP Tower reads your authorization server metadata again and sends a refresh_token grant to its token_endpoint, with the client it registered.
  3. A new access token is pass. The scan then continues with it.
  4. 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.”
StatusMessageMeaningFix
passThe expired access token was refreshed.Refresh works.
failRefreshing the access token failed with HTTP <status>: <error> (<description>).The token endpoint refused the refresh. <error> is usually invalid_grant.Refresh token refused
failRefreshing the access token failed with HTTP <status>.The token endpoint answered an error without an OAuth error field.Refresh token refused
failRefreshing 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
skippedNo 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.

  1. 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.
  2. 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 list offline_access in the authorization server’s scopes_supported and allow the client to request it.
  3. Allow the refresh_token grant for the client. MCP Tower registers with "grant_types": ["authorization_code", "refresh_token"].
  4. If you rotate refresh tokens, return the new refresh token in the same response that invalidates the old one.
  5. Answer an invalid refresh token with 400 and invalid_grant, not invalid_request or a custom code.

A token response that keeps the user signed in:

Refresh response
HTTP/1.1 200 OK
Content-Type: application/json
Cache-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:

Refused refresh
HTTP/1.1 400 Bad Request
Content-Type: application/json
{"error":"invalid_grant","error_description":"refresh token is invalid"}

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).

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.

Terminal window
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.