From 016e96849e7b13cd92cce3207c91056907b600b5 Mon Sep 17 00:00:00 2001 From: Denis Palnitsky Date: Wed, 25 Feb 2026 09:58:27 +0100 Subject: [PATCH] Handle re-registration of OAuth clients when redirect_uri changes --- .../api/src/mcp/__tests__/handler.test.ts | 59 +++++++++++++++++++ packages/api/src/mcp/oauth/handler.ts | 22 ++++--- 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/packages/api/src/mcp/__tests__/handler.test.ts b/packages/api/src/mcp/__tests__/handler.test.ts index 7225fc00cc..ae0412ab0c 100644 --- a/packages/api/src/mcp/__tests__/handler.test.ts +++ b/packages/api/src/mcp/__tests__/handler.test.ts @@ -1610,6 +1610,65 @@ describe('MCPOAuthHandler - Configurable OAuth Metadata', () => { // Should have fallen back to registerClient expect(mockRegisterClient).toHaveBeenCalled(); }); + + it('should re-register when stored redirect_uri differs from current configuration', async () => { + const existingClientInfo = { + client_id: 'existing-client-id', + client_secret: 'existing-client-secret', + redirect_uris: ['http://old-domain.com/api/mcp/test-server/oauth/callback'], + token_endpoint_auth_method: 'client_secret_basic', + }; + + mockGetClientInfoAndMetadata.mockResolvedValueOnce({ + clientInfo: existingClientInfo, + clientMetadata: {}, + }); + + mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValueOnce( + new Error('No resource metadata'), + ); + + mockDiscoverAuthorizationServerMetadata.mockResolvedValueOnce({ + issuer: 'https://example.com', + authorization_endpoint: 'https://example.com/authorize', + token_endpoint: 'https://example.com/token', + registration_endpoint: 'https://example.com/register', + response_types_supported: ['code'], + jwks_uri: 'https://example.com/.well-known/jwks.json', + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: ['RS256'], + } as AuthorizationServerMetadata); + + mockRegisterClient.mockResolvedValueOnce({ + client_id: 'new-client-id', + client_secret: 'new-client-secret', + redirect_uris: ['http://localhost:3080/api/mcp/test-server/oauth/callback'], + }); + + mockStartAuthorization.mockResolvedValueOnce({ + authorizationUrl: new URL('https://example.com/authorize?client_id=new-client-id'), + codeVerifier: 'test-code-verifier', + }); + + await MCPOAuthHandler.initiateOAuthFlow( + 'test-server', + 'https://example.com/mcp', + 'user-123', + {}, + undefined, + mockFindToken, + ); + + expect(mockRegisterClient).toHaveBeenCalled(); + expect(mockStartAuthorization).toHaveBeenCalledWith( + 'https://example.com/mcp', + expect.objectContaining({ + clientInformation: expect.objectContaining({ + client_id: 'new-client-id', + }), + }), + ); + }); }); describe('Fallback OAuth Metadata (Legacy Server Support)', () => { diff --git a/packages/api/src/mcp/oauth/handler.ts b/packages/api/src/mcp/oauth/handler.ts index 5e8d464125..3b3c15d2ff 100644 --- a/packages/api/src/mcp/oauth/handler.ts +++ b/packages/api/src/mcp/oauth/handler.ts @@ -496,10 +496,10 @@ export class MCPOAuthHandler { `[MCPOAuth] OAuth metadata discovered, auth server URL: ${sanitizeUrlForLogging(authServerUrl)}`, ); - const redirectUri = this.getDefaultRedirectUri(serverName); - logger.debug(`[MCPOAuth] Registering OAuth client with redirect URI: ${redirectUri}`); + /** Dynamic client registration based on the discovered metadata */ + const redirectUri = config?.redirect_uri || this.getDefaultRedirectUri(serverName); + logger.debug(`[MCPOAuth] Resolving OAuth client with redirect URI: ${redirectUri}`); - // Before registering, check if we already have a valid client registration let clientInfo: OAuthClientInformation | undefined; if (findToken) { @@ -510,10 +510,18 @@ export class MCPOAuthHandler { findToken, }); if (existing?.clientInfo?.client_id) { - logger.debug( - `[MCPOAuth] Reusing existing client registration: ${existing.clientInfo.client_id}`, - ); - clientInfo = existing.clientInfo; + const existingClient = existing.clientInfo as OAuthClientInformation; + const storedRedirectUri = existingClient.redirect_uris?.[0]; + if (storedRedirectUri && storedRedirectUri !== redirectUri) { + logger.debug( + `[MCPOAuth] Stored redirect_uri "${storedRedirectUri}" differs from current "${redirectUri}", will re-register`, + ); + } else { + logger.debug( + `[MCPOAuth] Reusing existing client registration: ${existingClient.client_id}`, + ); + clientInfo = existingClient; + } } } catch (error) { logger.debug(