Skip to content

Commit 4d76385

Browse files
authored
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.
1 parent 58c87b2 commit 4d76385

3 files changed

Lines changed: 8 additions & 6 deletions

File tree

apps/sim/app/api/mcp/oauth/callback/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
9090

9191
if (errorParam) {
9292
logger.warn(`MCP OAuth callback received error: ${errorParam}`)
93-
if (initialRow) await clearState(initialRow.id).catch(() => {})
93+
if (initialRow) await clearState(initialRow.id, 'callback:provider_error').catch(() => {})
9494
return respond(`Authorization failed: ${errorParam}`, false, 'provider_error', stateRowServerId)
9595
}
9696
if (!state || !code) {
@@ -157,7 +157,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
157157
}
158158

159159
// Burn state before token exchange so a replayed callback cannot reuse it.
160-
await clearState(row.id)
160+
await clearState(row.id, 'callback:burn-before-exchange')
161161

162162
const preregistered = await loadPreregisteredClient(server.id)
163163
const provider = new SimMcpOauthProvider({ row, preregistered })

apps/sim/lib/mcp/oauth/provider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export class SimMcpOauthProvider implements OAuthClientProvider {
7777

7878
async state(): Promise<string> {
7979
const state = generateId()
80-
await saveState(this.row.id, state)
80+
await saveState(this.row.id, state, 'provider.state')
8181
return state
8282
}
8383

@@ -140,7 +140,7 @@ export class SimMcpOauthProvider implements OAuthClientProvider {
140140
}
141141
if (scope === 'all' || scope === 'verifier') {
142142
await clearVerifier(this.row.id)
143-
await clearState(this.row.id)
143+
await clearState(this.row.id, `invalidateCredentials:${scope}`)
144144
this.row.codeVerifier = null
145145
}
146146
}

apps/sim/lib/mcp/oauth/storage.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,13 @@ export async function saveCodeVerifier(rowId: string, verifier: string): Promise
192192
.where(eq(mcpServerOauth.id, rowId))
193193
}
194194

195-
export async function saveState(rowId: string, state: string): Promise<void> {
195+
export async function saveState(rowId: string, state: string, context = 'unknown'): Promise<void> {
196196
const now = new Date()
197197
await db
198198
.update(mcpServerOauth)
199199
.set({ state: hashState(state), stateCreatedAt: now, updatedAt: now })
200200
.where(eq(mcpServerOauth.id, rowId))
201+
logger.info('MCP OAuth authorization state saved', { rowId, context })
201202
}
202203

203204
export async function clearTokens(rowId: string): Promise<void> {
@@ -221,11 +222,12 @@ export async function clearVerifier(rowId: string): Promise<void> {
221222
.where(eq(mcpServerOauth.id, rowId))
222223
}
223224

224-
export async function clearState(rowId: string): Promise<void> {
225+
export async function clearState(rowId: string, context = 'unknown'): Promise<void> {
225226
await db
226227
.update(mcpServerOauth)
227228
.set({ state: null, stateCreatedAt: null, updatedAt: new Date() })
228229
.where(eq(mcpServerOauth.id, rowId))
230+
logger.info('MCP OAuth authorization state cleared', { rowId, context })
229231
}
230232

231233
/**

0 commit comments

Comments
 (0)