feat: add token refresh and rotation conformance scenarios#139
Open
jdmaturen wants to merge 2 commits intomodelcontextprotocol:mainfrom
Open
feat: add token refresh and rotation conformance scenarios#139jdmaturen wants to merge 2 commits intomodelcontextprotocol:mainfrom
jdmaturen wants to merge 2 commits intomodelcontextprotocol:mainfrom
Conversation
The SDK's `requireBearerAuth` middleware only converts `InvalidTokenError` instances to HTTP 401 responses. Generic `Error` instances fall through as HTTP 500, which prevents clients from detecting authentication failures and initiating the OAuth refresh/re-auth flow. This was discovered while building token refresh conformance scenarios — the mock server was returning 500 for expired/invalid tokens instead of the expected 401. Co-authored-by: Cursor <cursoragent@cursor.com>
Add two new client auth conformance scenarios that test OAuth 2.1 refresh token behavior: - `auth/token-refresh-basic`: Tests that clients use the refresh_token grant to obtain a new access token when the current one expires (OAuth 2.1 §6). Server issues 2-second TTL access tokens + refresh token; client must detect 401, send grant_type=refresh_token, and use the new access token. - `auth/token-refresh-rotation`: Same flow but the server rotates the refresh token on each use (OAuth 2.1 §6.1). Client must store the new refresh token and not reuse the old one. Supporting changes: - `createAuthServer`: Add `issueRefreshToken`, `rotateRefreshTokens`, `accessTokenExpiresIn`, and `onRefreshTokenRequest` options. Add full `grant_type=refresh_token` handler with token validation, rotation, and conformance checks. - `createServer`: Add `perRequestServer` option to create a fresh MCP Server per request. Required for token refresh tests where requests span token expiry boundaries (the default behavior calls server.close() on response end, breaking subsequent requests). - `mockTokenVerifier`: Add token expiration tracking (issuedAt, expiresIn per token). Expired tokens now throw InvalidTokenError with an INFO conformance check, enabling proper 401 responses. - `spec-references`: Add OAUTH_2_1_REFRESH_TOKEN (§6) and OAUTH_2_1_TOKEN_ROTATION (§6.1) references. - `everything-client`: Add `runTokenRefreshClient` that exercises the full refresh flow (connect → request → wait for expiry → request). Depends on modelcontextprotocol#138 (InvalidTokenError fix). Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds two new client auth conformance scenarios that test OAuth 2.1 refresh token behavior:
auth/token-refresh-basic— Tests that clients use therefresh_tokengrant to obtain a new access token when the current one expires (OAuth 2.1 §6). Server issues 2-second TTL access tokens + refresh token; client must detect 401, sendgrant_type=refresh_token, and use the new access token successfully.auth/token-refresh-rotation— Same flow but the server rotates the refresh token on each use (OAuth 2.1 §6.1). Client must store the new refresh token and not reuse the old one.Motivation
Multiple MCP client implementations have been reported as not handling token refresh reliably — leading to auth loops, death spirals, and excessive retry RPM when access tokens expire. These scenarios provide a concrete conformance check against the OAuth 2.1 refresh token spec.
Changes
token-refresh.tsTokenRefreshBasicScenarioandTokenRefreshRotationScenariocreateAuthServer.tsissueRefreshToken,rotateRefreshTokens,accessTokenExpiresIn,onRefreshTokenRequestoptions; fullgrant_type=refresh_tokenhandlercreateServer.tsperRequestServeroption (fresh MCP Server per request, needed becauseserver.close()on response end breaks subsequent requests across token boundaries)mockTokenVerifier.tsissuedAt,expiresInper token); expired tokens throwInvalidTokenErrorwith INFO conformance checkspec-references.tsOAUTH_2_1_REFRESH_TOKEN(§6) andOAUTH_2_1_TOKEN_ROTATION(§6.1)index.tseverything-client.tsrunTokenRefreshClientexercising the full refresh flowTest plan
auth/token-refresh-basicpasses in ~3s (2s token TTL + 1s buffer)auth/token-refresh-rotationpasses in ~3stsc --noEmitcleanDependencies
Depends on #138 (
InvalidTokenErrorfix) — without it, expired tokens return 500 instead of 401 and the refresh flow never triggers.Made with Cursor