Skip to content

feat: add token refresh and rotation conformance scenarios#139

Open
jdmaturen wants to merge 2 commits intomodelcontextprotocol:mainfrom
jdmaturen:feat/token-refresh-scenarios
Open

feat: add token refresh and rotation conformance scenarios#139
jdmaturen wants to merge 2 commits intomodelcontextprotocol:mainfrom
jdmaturen:feat/token-refresh-scenarios

Conversation

@jdmaturen
Copy link

Summary

Adds 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 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

File Change
token-refresh.ts New file: TokenRefreshBasicScenario and TokenRefreshRotationScenario
createAuthServer.ts issueRefreshToken, rotateRefreshTokens, accessTokenExpiresIn, onRefreshTokenRequest options; full grant_type=refresh_token handler
createServer.ts perRequestServer option (fresh MCP Server per request, needed because server.close() on response end breaks subsequent requests across token boundaries)
mockTokenVerifier.ts Token expiration tracking (issuedAt, expiresIn per token); expired tokens throw InvalidTokenError with INFO conformance check
spec-references.ts OAUTH_2_1_REFRESH_TOKEN (§6) and OAUTH_2_1_TOKEN_ROTATION (§6.1)
index.ts Register both new scenarios
everything-client.ts runTokenRefreshClient exercising the full refresh flow

Test plan

  • All 79 tests pass (77 existing + 2 new token refresh scenarios)
  • auth/token-refresh-basic passes in ~3s (2s token TTL + 1s buffer)
  • auth/token-refresh-rotation passes in ~3s
  • tsc --noEmit clean

Dependencies

Depends on #138 (InvalidTokenError fix) — without it, expired tokens return 500 instead of 401 and the refresh flow never triggers.

Made with Cursor

jdmaturen and others added 2 commits February 8, 2026 22:06
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>
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.

1 participant