Handle re-registration of OAuth clients when redirect_uri changes

This commit is contained in:
Denis Palnitsky 2026-02-25 09:58:27 +01:00 committed by Danny Avila
parent 2fcf8c5419
commit 016e96849e
2 changed files with 74 additions and 7 deletions

View file

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

View file

@ -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(