@@ -52,6 +52,7 @@ const SHARED_CLIENTS = [
5252 clientIdKey : 'HUBSPOT_MCP_CLIENT_ID' ,
5353 clientSecretKey : 'HUBSPOT_MCP_CLIENT_SECRET' ,
5454 url : 'https://mcp.hubspot.com' ,
55+ authorizationServer : 'https://mcp.hubspot.com' ,
5556 tokenAuthMethod : 'client_secret_post' ,
5657 scope : undefined ,
5758 } ,
@@ -61,6 +62,7 @@ const SHARED_CLIENTS = [
6162 clientIdKey : 'ZOOM_MCP_CLIENT_ID' ,
6263 clientSecretKey : 'ZOOM_MCP_CLIENT_SECRET' ,
6364 url : 'https://mcp.zoom.us/mcp/meeting/streamable' ,
65+ authorizationServer : 'https://zoom.us' ,
6466 tokenAuthMethod : 'client_secret_basic' ,
6567 scope : 'meeting:read:search meeting:read:assets' ,
6668 } ,
@@ -208,6 +210,7 @@ describe.each(SHARED_CLIENTS)('$name shared member connector', (connector) => {
208210 expect ( await loadPreregisteredClient ( mcpServer . id ) ) . toEqual ( {
209211 clientId : 'saved-client' ,
210212 clientSecret : 'saved-secret' ,
213+ issuer : connector . authorizationServer ,
211214 } )
212215 for ( const change of [
213216 { oauthClientId : null , oauthClientSecret : encrypted } ,
@@ -332,7 +335,7 @@ describe.each(SHARED_CLIENTS)('$name shared member connector', (connector) => {
332335 'restricts generic OAuth %s to registered read permissions' ,
333336 async ( phase ) => {
334337 const { mcpServer } = await create ( )
335- const issuer = 'https://oauth.fixture.test'
338+ const issuer = connector . authorizationServer
336339 const loadProvider = async ( ) =>
337340 new SimMcpOauthProvider ( {
338341 row : await getOrCreateOauthRow ( { mcpServerId : mcpServer . id , organizationId : org } ) ,
@@ -396,6 +399,72 @@ describe.each(SHARED_CLIENTS)('$name shared member connector', (connector) => {
396399 }
397400 )
398401 }
402+ it . each ( [ 'code exchange' , 'legacy refresh' ] as const ) (
403+ 'never presents shared credentials to a substituted authorization server on %s' ,
404+ async ( phase ) => {
405+ const { mcpServer } = await create ( )
406+ const substitute = 'https://oauth.substitute.test'
407+ const provider = new SimMcpOauthProvider ( {
408+ row : await getOrCreateOauthRow ( { mcpServerId : mcpServer . id , organizationId : org } ) ,
409+ preregistered : await loadPreregisteredClient ( mcpServer . id ) ,
410+ } )
411+ if ( phase === 'code exchange' ) await provider . saveCodeVerifier ( 'fixture-verifier' )
412+ else
413+ await provider . saveTokens ( {
414+ access_token : 'fixture-access' ,
415+ refresh_token : 'fixture-refresh' ,
416+ token_type : 'Bearer' ,
417+ } )
418+ const disclosed : string [ ] = [ ]
419+ const reached = new Set < string > ( )
420+ const fetchFn : typeof fetch = async ( request , init ) => {
421+ const url = new URL (
422+ typeof request === 'string' ? request : request instanceof URL ? request : request . url
423+ )
424+ if ( url . origin === substitute ) reached . add ( url . pathname )
425+ const sent = `${ new Headers ( init ?. headers ) . get ( 'authorization' ) ?? '' } ${ String ( init ?. body ?? '' ) } `
426+ if (
427+ url . origin === substitute &&
428+ ( sent . includes ( SECRET ) ||
429+ sent . includes ( Buffer . from ( `fixture-shared-client:${ SECRET } ` ) . toString ( 'base64' ) ) ||
430+ sent . includes ( 'fixture-refresh' ) )
431+ )
432+ disclosed . push ( `${ init ?. method ?? 'GET' } ${ url . pathname } ` )
433+ if ( url . pathname . includes ( 'oauth-protected-resource' ) )
434+ return Response . json ( { resource : connector . url , authorization_servers : [ substitute ] } )
435+ if (
436+ url . pathname . includes ( 'oauth-authorization-server' ) ||
437+ url . pathname . includes ( 'openid-configuration' )
438+ )
439+ return Response . json ( {
440+ issuer : substitute ,
441+ authorization_endpoint : `${ substitute } /authorize` ,
442+ token_endpoint : `${ substitute } /token` ,
443+ registration_endpoint : `${ substitute } /register` ,
444+ response_types_supported : [ 'code' ] ,
445+ code_challenge_methods_supported : [ 'S256' ] ,
446+ } )
447+ if ( url . pathname === '/register' )
448+ return Response . json ( {
449+ client_id : 'substitute-client' ,
450+ redirect_uris : [ provider . redirectUrl ] ,
451+ } )
452+ if ( url . pathname === '/token' )
453+ return Response . json ( { access_token : 'substitute-access' , token_type : 'Bearer' } )
454+ throw new Error ( `Unexpected OAuth fixture request: ${ url . origin } ${ url . pathname } ` )
455+ }
456+ const attempt = oauth . mcpAuthGuarded ( provider , {
457+ serverUrl : connector . url ,
458+ fetchFn,
459+ ...( phase === 'code exchange' ? { authorizationCode : 'fixture-code' } : { } ) ,
460+ } )
461+ if ( phase === 'code exchange' )
462+ await expect ( attempt ) . rejects . toThrow ( / E x i s t i n g O A u t h c l i e n t i n f o r m a t i o n i s r e q u i r e d / )
463+ else await expect ( attempt ) . rejects . toBeInstanceOf ( McpOauthRedirectRequired )
464+ expect ( reached . has ( '/.well-known/oauth-authorization-server' ) ) . toBe ( true )
465+ expect ( disclosed ) . toEqual ( [ ] )
466+ }
467+ )
399468 it ( 'binds the public OAuth round trip to the shared client and rejects rotation before exchange' , async ( ) => {
400469 const { mcpServer } = await create ( )
401470 const token = generateId ( )
@@ -427,7 +496,7 @@ describe.each(SHARED_CLIENTS)('$name shared member connector', (connector) => {
427496 } )
428497 let exchanges = 0
429498 let challenge : string | null = null
430- const issuer = 'https://oauth.fixture.test'
499+ const issuer = connector . authorizationServer
431500 /** Only provider HTTP is substituted; SDK, PKCE, Redis, use cases, encryption and database are real. */
432501 const fetchFn : typeof fetch = async ( request , init ) => {
433502 const url = new URL (
0 commit comments