improvement(mcp): trace OAuth state writes to diagnose invalid_state clobber (#5772)

The MCP OAuth callback fails with invalid_state because the authorization
state is cleared/missing by the time the user authorizes — but clearState was
silent, so the clobber never surfaced in logs. Log every state save and clear
with a caller context so the exact source is visible on the next repro.
This commit is contained in:
Waleed
2026-07-20 11:51:09 -07:00
committed by GitHub
parent 58c87b2c43
commit 4d76385915
3 changed files with 8 additions and 6 deletions
+2 -2
View File
@@ -90,7 +90,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
if (errorParam) {
logger.warn(`MCP OAuth callback received error: ${errorParam}`)
if (initialRow) await clearState(initialRow.id).catch(() => {})
if (initialRow) await clearState(initialRow.id, 'callback:provider_error').catch(() => {})
return respond(`Authorization failed: ${errorParam}`, false, 'provider_error', stateRowServerId)
}
if (!state || !code) {
@@ -157,7 +157,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
}
// Burn state before token exchange so a replayed callback cannot reuse it.
await clearState(row.id)
await clearState(row.id, 'callback:burn-before-exchange')
const preregistered = await loadPreregisteredClient(server.id)
const provider = new SimMcpOauthProvider({ row, preregistered })
+2 -2
View File
@@ -77,7 +77,7 @@ export class SimMcpOauthProvider implements OAuthClientProvider {
async state(): Promise<string> {
const state = generateId()
await saveState(this.row.id, state)
await saveState(this.row.id, state, 'provider.state')
return state
}
@@ -140,7 +140,7 @@ export class SimMcpOauthProvider implements OAuthClientProvider {
}
if (scope === 'all' || scope === 'verifier') {
await clearVerifier(this.row.id)
await clearState(this.row.id)
await clearState(this.row.id, `invalidateCredentials:${scope}`)
this.row.codeVerifier = null
}
}
+4 -2
View File
@@ -192,12 +192,13 @@ export async function saveCodeVerifier(rowId: string, verifier: string): Promise
.where(eq(mcpServerOauth.id, rowId))
}
export async function saveState(rowId: string, state: string): Promise<void> {
export async function saveState(rowId: string, state: string, context = 'unknown'): Promise<void> {
const now = new Date()
await db
.update(mcpServerOauth)
.set({ state: hashState(state), stateCreatedAt: now, updatedAt: now })
.where(eq(mcpServerOauth.id, rowId))
logger.info('MCP OAuth authorization state saved', { rowId, context })
}
export async function clearTokens(rowId: string): Promise<void> {
@@ -221,11 +222,12 @@ export async function clearVerifier(rowId: string): Promise<void> {
.where(eq(mcpServerOauth.id, rowId))
}
export async function clearState(rowId: string): Promise<void> {
export async function clearState(rowId: string, context = 'unknown'): Promise<void> {
await db
.update(mcpServerOauth)
.set({ state: null, stateCreatedAt: null, updatedAt: new Date() })
.where(eq(mcpServerOauth.id, rowId))
logger.info('MCP OAuth authorization state cleared', { rowId, context })
}
/**