mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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:
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user