From 7ea6fde1b6d7c606868fdc7d0b900d36a60dfe80 Mon Sep 17 00:00:00 2001 From: Matt <77928207+mattzcarey@users.noreply.github.com> Date: Thu, 25 Jun 2026 14:59:30 +0200 Subject: [PATCH] test(client): cover refresh retry after invalid token 401 (#2367) Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com> --- packages/client/test/client/auth.test.ts | 18 ++++ .../client/test/client/streamableHttp.test.ts | 101 +++++++++++++++++- 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/packages/client/test/client/auth.test.ts b/packages/client/test/client/auth.test.ts index 8239024c0c..35e86ffc35 100644 --- a/packages/client/test/client/auth.test.ts +++ b/packages/client/test/client/auth.test.ts @@ -134,6 +134,24 @@ describe('OAuth Authorization', () => { expect(extractWWWAuthenticateParams(mockResponse)).toEqual({ error: 'insufficient_scope', scope: 'admin' }); }); + + it('parses invalid_token challenges with protected resource metadata', async () => { + const resourceUrl = 'https://resource.example.com/.well-known/oauth-protected-resource/mcp'; + const mockResponse = { + headers: { + get: vi.fn(name => + name === 'WWW-Authenticate' + ? `Bearer resource_metadata="${resourceUrl}", error="invalid_token", error_description="The access token expired"` + : null + ) + } + } as unknown as Response; + + expect(extractWWWAuthenticateParams(mockResponse)).toEqual({ + resourceMetadataUrl: new URL(resourceUrl), + error: 'invalid_token' + }); + }); }); describe('discoverOAuthProtectedResourceMetadata', () => { diff --git a/packages/client/test/client/streamableHttp.test.ts b/packages/client/test/client/streamableHttp.test.ts index db1f80fe9e..ef152d5892 100644 --- a/packages/client/test/client/streamableHttp.test.ts +++ b/packages/client/test/client/streamableHttp.test.ts @@ -1,4 +1,4 @@ -import type { JSONRPCMessage, JSONRPCRequest } from '@modelcontextprotocol/core'; +import type { JSONRPCMessage, JSONRPCRequest, OAuthTokens } from '@modelcontextprotocol/core'; import { OAuthError, OAuthErrorCode, SdkErrorCode, SdkHttpError } from '@modelcontextprotocol/core'; import type { Mock, Mocked } from 'vitest'; @@ -784,6 +784,105 @@ describe('StreamableHTTPClientTransport', () => { expect(mockAuthProvider.redirectToAuthorization.mock.calls).toHaveLength(1); }); + it('silently refreshes and retries when a POST returns 401 invalid_token', async () => { + const message: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'tools/call', + params: { + name: 'power-bi-query', + arguments: {} + }, + id: 'tool-use-1' + }; + const resourceMetadataUrl = 'http://localhost:1234/.well-known/oauth-protected-resource/mcp'; + let currentTokens: OAuthTokens = { + access_token: 'expired-access-token', + token_type: 'Bearer', + refresh_token: 'refresh-token' + }; + + mockAuthProvider.tokens.mockImplementation(() => currentTokens); + mockAuthProvider.saveTokens.mockImplementation(tokens => { + currentTokens = tokens; + }); + + const fetchMock = globalThis.fetch as Mock; + fetchMock.mockImplementation(async (url, init) => { + const urlString = url.toString(); + + if (urlString === 'http://localhost:1234/mcp' && init?.method === 'POST') { + const headers = new Headers(init.headers); + const authorization = headers.get('authorization'); + + if (authorization === 'Bearer expired-access-token') { + return new Response('expired', { + status: 401, + statusText: 'Unauthorized', + headers: { + 'WWW-Authenticate': `Bearer resource_metadata="${resourceMetadataUrl}", error="invalid_token", error_description="The access token expired"` + } + }); + } + + if (authorization === 'Bearer new-access-token') { + return new Response(null, { status: 202 }); + } + + return new Response('unexpected bearer', { status: 401, statusText: 'Unauthorized' }); + } + + if (urlString === resourceMetadataUrl) { + return Response.json({ + resource: 'http://localhost:1234/mcp', + authorization_servers: ['http://localhost:1234'] + }); + } + + if (urlString === 'http://localhost:1234/.well-known/oauth-authorization-server') { + return Response.json({ + issuer: 'http://localhost:1234', + authorization_endpoint: 'http://localhost:1234/authorize', + token_endpoint: 'http://localhost:1234/token', + response_types_supported: ['code'], + code_challenge_methods_supported: ['S256'] + }); + } + + if (urlString === 'http://localhost:1234/token' && init?.method === 'POST') { + const params = new URLSearchParams(init.body as string); + expect(params.get('grant_type')).toBe('refresh_token'); + expect(params.get('refresh_token')).toBe('refresh-token'); + + return Response.json({ + access_token: 'new-access-token', + token_type: 'Bearer', + refresh_token: 'new-refresh-token' + }); + } + + return new Response('not found', { status: 404 }); + }); + + await transport.send(message); + + const mcpPostCalls = fetchMock.mock.calls.filter( + ([url, init]) => url.toString() === 'http://localhost:1234/mcp' && init?.method === 'POST' + ); + expect(mcpPostCalls).toHaveLength(2); + const firstPost = mcpPostCalls[0]!; + const secondPost = mcpPostCalls[1]!; + expect(new Headers(firstPost[1]?.headers).get('authorization')).toBe('Bearer expired-access-token'); + expect(new Headers(secondPost[1]?.headers).get('authorization')).toBe('Bearer new-access-token'); + expect(firstPost[1]?.body).toBe(JSON.stringify(message)); + expect(secondPost[1]?.body).toBe(JSON.stringify(message)); + expect(mockAuthProvider.saveTokens).toHaveBeenCalledWith({ + access_token: 'new-access-token', + token_type: 'Bearer', + refresh_token: 'new-refresh-token' + }); + expect(mockAuthProvider.redirectToAuthorization).not.toHaveBeenCalled(); + }); + it('attempts upscoping on 403 with WWW-Authenticate header', async () => { const message: JSONRPCMessage = { jsonrpc: '2.0',