fix: gate failFlow behind callback validation, propagate reusedStoredClient on join

- OAuth callback: move failFlow call to after CSRF/session/active-flow
  validation so an attacker with only a leaked state parameter cannot
  force-fail a flow without passing the same integrity checks required
  for legitimate callbacks
- PENDING join path: propagate reusedStoredClient from flow metadata
  into the return object so joiners can trigger stale-client cleanup
  if the joined flow later fails with a client rejection
This commit is contained in:
Danny Avila 2026-04-03 19:26:51 -04:00
parent 68ea22813c
commit fdfcf26d8c
2 changed files with 28 additions and 28 deletions

View file

@ -147,35 +147,13 @@ router.get('/:serverName/oauth/callback', async (req, res) => {
error: oauthError,
});
if (oauthError) {
logger.error('[MCP OAuth] OAuth error received', { error: oauthError });
if (state && typeof state === 'string') {
try {
const flowsCache = getLogStores(CacheKeys.FLOWS);
const flowManager = getFlowStateManager(flowsCache);
const flowId = await MCPOAuthHandler.resolveStateToFlowId(state, flowManager);
if (flowId) {
await flowManager.failFlow(flowId, 'mcp_oauth', String(oauthError));
logger.debug('[MCP OAuth] Marked flow as FAILED with OAuth error', {
flowId,
error: oauthError,
});
}
} catch (err) {
logger.debug('[MCP OAuth] Could not mark flow as failed', err);
}
}
return res.redirect(
`${basePath}/oauth/error?error=${encodeURIComponent(String(oauthError))}`,
);
}
if (!code || typeof code !== 'string') {
logger.error('[MCP OAuth] Missing or invalid code');
return res.redirect(`${basePath}/oauth/error?error=missing_code`);
}
if (!state || typeof state !== 'string') {
if (oauthError) {
logger.error('[MCP OAuth] OAuth error received without state', { error: oauthError });
return res.redirect(
`${basePath}/oauth/error?error=${encodeURIComponent(String(oauthError))}`,
);
}
logger.error('[MCP OAuth] Missing or invalid state');
return res.redirect(`${basePath}/oauth/error?error=missing_state`);
}
@ -227,6 +205,27 @@ router.get('/:serverName/oauth/callback', async (req, res) => {
return res.redirect(`${basePath}/oauth/error?error=csrf_validation_failed`);
}
if (oauthError) {
logger.error('[MCP OAuth] OAuth error received', { error: oauthError, flowId });
try {
await flowManager.failFlow(flowId, 'mcp_oauth', String(oauthError));
logger.debug('[MCP OAuth] Marked flow as FAILED with OAuth error', {
flowId,
error: oauthError,
});
} catch (err) {
logger.debug('[MCP OAuth] Could not mark flow as failed', err);
}
return res.redirect(
`${basePath}/oauth/error?error=${encodeURIComponent(String(oauthError))}`,
);
}
if (!code || typeof code !== 'string') {
logger.error('[MCP OAuth] Missing or invalid code');
return res.redirect(`${basePath}/oauth/error?error=missing_code`);
}
logger.debug('[MCP OAuth] Getting flow state for flowId: ' + flowId);
const flowState = await MCPOAuthHandler.getFlowState(flowId, flowManager);