fix: validate auth server identity and target cleanup to reused clients

- Gate client reuse on authorization server identity: compare stored
  issuer against freshly discovered metadata before reusing, preventing
  wrong-client reuse when the MCP server switches auth providers
- Add reusedStoredClient flag to MCPOAuthFlowMetadata so cleanup only
  runs when the failed flow actually reused a stored registration,
  not on unrelated failures (timeouts, user-denied consent, etc.)
- Add cleanup in returnOnOAuth path: when a prior flow that reused a
  stored client is detected as failed, clear the stale registration
  before re-initiating
- Add tests for issuer mismatch and reusedStoredClient flag assertions
This commit is contained in:
Danny Avila 2026-04-03 15:47:54 -04:00
parent d355be7dd0
commit 978ce2b4eb
4 changed files with 83 additions and 14 deletions

View file

@ -355,7 +355,17 @@ export class MCPConnectionFactory {
);
if (existingFlow) {
const oldState = (existingFlow.metadata as MCPOAuthFlowMetadata)?.state;
const oldMeta = existingFlow.metadata as MCPOAuthFlowMetadata | undefined;
if (oldMeta?.reusedStoredClient && this.tokenMethods?.deleteTokens) {
await MCPTokenStorage.deleteClientRegistration({
userId: this.userId!,
serverName: this.serverName,
deleteTokens: this.tokenMethods.deleteTokens,
}).catch((err) => {
logger.debug(`${this.logPrefix} Failed to clear stale client registration`, err);
});
}
const oldState = oldMeta?.state;
await this.flowManager!.deleteFlow(newFlowId, 'mcp_oauth');
if (oldState) {
await MCPOAuthHandler.deleteStateMapping(oldState, this.flowManager!);
@ -413,16 +423,21 @@ export class MCPConnectionFactory {
if (result?.tokens) {
connection.emit('oauthHandled');
} else {
// OAuth failed — clear stored client registration so the next attempt
// does a fresh DCR instead of reusing a potentially stale client_id
if (this.tokenMethods?.deleteTokens) {
await MCPTokenStorage.deleteClientRegistration({
userId: this.userId!,
serverName: this.serverName,
deleteTokens: this.tokenMethods.deleteTokens,
}).catch((err) => {
logger.debug(`${this.logPrefix} Failed to clear stale client registration`, err);
});
// OAuth failed — if we reused a stored client registration, clear it
// so the next attempt falls through to fresh DCR
if (result?.clientInfo && this.tokenMethods?.deleteTokens) {
const flowId = MCPOAuthHandler.generateFlowId(this.userId!, this.serverName);
const failedFlow = await this.flowManager?.getFlowState(flowId, 'mcp_oauth');
const failedMeta = failedFlow?.metadata as MCPOAuthFlowMetadata | undefined;
if (failedMeta?.reusedStoredClient) {
await MCPTokenStorage.deleteClientRegistration({
userId: this.userId!,
serverName: this.serverName,
deleteTokens: this.tokenMethods.deleteTokens,
}).catch((err) => {
logger.debug(`${this.logPrefix} Failed to clear stale client registration`, err);
});
}
}
logger.warn(`${this.logPrefix} OAuth failed, emitting oauthFailed event`);
connection.emit('oauthFailed', new Error('OAuth authentication failed'));