fix: Support WWW-Authenticate scope param for SEP-835 (#983)

Co-authored-by: OtotheA <oscar@otothea.com>
This commit is contained in:
Chip 2025-11-03 09:24:42 -06:00 • committed by GitHub
commit ee9fed28cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -7,14 +7,14 @@ jest.mock('../client/auth.js', () => {
return {
...actual,
auth: jest.fn(),
extractResourceMetadataUrl: jest.fn()
extractWWWAuthenticateParams: jest.fn()
};
});
import { auth, extractResourceMetadataUrl } from './auth.js';
import { auth, extractWWWAuthenticateParams } from './auth.js';
const mockAuth = auth as jest.MockedFunction<typeof auth>;
const mockExtractResourceMetadataUrl = extractResourceMetadataUrl as jest.MockedFunction<typeof extractResourceMetadataUrl>;
const mockExtractWWWAuthenticateParams = extractWWWAuthenticateParams as jest.MockedFunction<typeof extractWWWAuthenticateParams>;
describe('withOAuth', () => {
let mockProvider: jest.Mocked<OAuthClientProvider>;
@ -129,8 +129,11 @@ describe('withOAuth', () => {
mockFetch.mockResolvedValueOnce(unauthorizedResponse).mockResolvedValueOnce(successResponse);
const mockResourceUrl = new URL('https://oauth.example.com/.well-known/oauth-protected-resource');
mockExtractResourceMetadataUrl.mockReturnValue(mockResourceUrl);
const mockWWWAuthenticateParams = {
resourceMetadataUrl: new URL('https://oauth.example.com/.well-known/oauth-protected-resource'),
scope: 'read'
};
mockExtractWWWAuthenticateParams.mockReturnValue(mockWWWAuthenticateParams);
mockAuth.mockResolvedValue('AUTHORIZED');
const enhancedFetch = withOAuth(mockProvider, 'https://api.example.com')(mockFetch);
@ -141,7 +144,8 @@ describe('withOAuth', () => {
expect(mockFetch).toHaveBeenCalledTimes(2);
expect(mockAuth).toHaveBeenCalledWith(mockProvider, {
serverUrl: 'https://api.example.com',
resourceMetadataUrl: mockResourceUrl,
resourceMetadataUrl: mockWWWAuthenticateParams.resourceMetadataUrl,
scope: mockWWWAuthenticateParams.scope,
fetchFn: mockFetch
});
@ -172,8 +176,11 @@ describe('withOAuth', () => {
mockFetch.mockResolvedValueOnce(unauthorizedResponse).mockResolvedValueOnce(successResponse);
const mockResourceUrl = new URL('https://oauth.example.com/.well-known/oauth-protected-resource');
mockExtractResourceMetadataUrl.mockReturnValue(mockResourceUrl);
const mockWWWAuthenticateParams = {
resourceMetadataUrl: new URL('https://oauth.example.com/.well-known/oauth-protected-resource'),
scope: 'read'
};
mockExtractWWWAuthenticateParams.mockReturnValue(mockWWWAuthenticateParams);
mockAuth.mockResolvedValue('AUTHORIZED');
// Test without baseUrl - should extract from request URL
@ -185,7 +192,8 @@ describe('withOAuth', () => {
expect(mockFetch).toHaveBeenCalledTimes(2);
expect(mockAuth).toHaveBeenCalledWith(mockProvider, {
serverUrl: 'https://api.example.com', // Should be extracted from request URL
resourceMetadataUrl: mockResourceUrl,
resourceMetadataUrl: mockWWWAuthenticateParams.resourceMetadataUrl,
scope: mockWWWAuthenticateParams.scope,
fetchFn: mockFetch
});
@ -203,7 +211,7 @@ describe('withOAuth', () => {
});
mockFetch.mockResolvedValue(new Response('Unauthorized', { status: 401 }));
mockExtractResourceMetadataUrl.mockReturnValue(undefined);
mockExtractWWWAuthenticateParams.mockReturnValue({});
mockAuth.mockResolvedValue('REDIRECT');
// Test without baseUrl
@ -222,7 +230,7 @@ describe('withOAuth', () => {
});
mockFetch.mockResolvedValue(new Response('Unauthorized', { status: 401 }));
mockExtractResourceMetadataUrl.mockReturnValue(undefined);
mockExtractWWWAuthenticateParams.mockReturnValue({});
mockAuth.mockRejectedValue(new Error('Network error'));
const enhancedFetch = withOAuth(mockProvider, 'https://api.example.com')(mockFetch);
@ -239,7 +247,7 @@ describe('withOAuth', () => {
// Always return 401
mockFetch.mockResolvedValue(new Response('Unauthorized', { status: 401 }));
mockExtractResourceMetadataUrl.mockReturnValue(undefined);
mockExtractWWWAuthenticateParams.mockReturnValue({});
mockAuth.mockResolvedValue('AUTHORIZED');
const enhancedFetch = withOAuth(mockProvider, 'https://api.example.com')(mockFetch);
@ -345,7 +353,7 @@ describe('withOAuth', () => {
mockFetch.mockResolvedValueOnce(unauthorizedResponse).mockResolvedValueOnce(successResponse);
mockExtractResourceMetadataUrl.mockReturnValue(undefined);
mockExtractWWWAuthenticateParams.mockReturnValue({});
mockAuth.mockResolvedValue('AUTHORIZED');
const enhancedFetch = withOAuth(mockProvider)(mockFetch);
@ -876,7 +884,10 @@ describe('Integration Tests', () => {
mockFetch.mockResolvedValueOnce(unauthorizedResponse).mockResolvedValueOnce(successResponse);
mockExtractResourceMetadataUrl.mockReturnValue(new URL('https://auth.example.com/.well-known/oauth-protected-resource'));
mockExtractWWWAuthenticateParams.mockReturnValue({
resourceMetadataUrl: new URL('https://auth.example.com/.well-known/oauth-protected-resource'),
scope: 'read'
});
mockAuth.mockResolvedValue('AUTHORIZED');
// Use custom logger to avoid console output
@ -896,6 +907,7 @@ describe('Integration Tests', () => {
expect(mockAuth).toHaveBeenCalledWith(mockProvider, {
serverUrl: 'https://mcp-server.example.com',
resourceMetadataUrl: new URL('https://auth.example.com/.well-known/oauth-protected-resource'),
scope: 'read',
fetchFn: mockFetch
});
});