test(client): cover refresh retry after invalid token 401 (#2367)

Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
This commit is contained in:
Matt 2026-06-25 14:59:30 +02:00 • committed by GitHub
commit 7ea6fde1b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 118 additions and 1 deletions

View file

@ -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', () => {

View file

@ -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',