Skip to content

Commit aa8cfdc

Browse files
waleedlatif1claude
andcommitted
fix(mcp): anchor OAuth state TTL to dedicated stateCreatedAt column
hasActiveFlow and loadOauthRowByState both gated on updatedAt, but saveTokens bumps updatedAt on every refresh. An abandoned state column from one user combined with another user's tool calls (which trigger background token refreshes) could indefinitely extend the state TTL, producing permanent 409 "OAuth authorization already in progress" responses and a wider state-replay window. Add a stateCreatedAt column set only by saveState (and cleared by clearState). Both TTL checks now anchor to it, so token refreshes no longer interfere with state expiry. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent c292149 commit aa8cfdc

6 files changed

Lines changed: 16057 additions & 4 deletions

File tree

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,10 @@ export const GET = withRouteHandler(
7676
userId,
7777
workspaceId,
7878
})
79-
const hasActiveFlow = !!row.state && row.updatedAt.getTime() > Date.now() - OAUTH_START_TTL_MS
79+
const hasActiveFlow =
80+
!!row.state &&
81+
!!row.stateCreatedAt &&
82+
row.stateCreatedAt.getTime() > Date.now() - OAUTH_START_TTL_MS
8083
if (hasActiveFlow && row.userId && row.userId !== userId) {
8184
return createMcpErrorResponse(
8285
new Error('OAuth authorization already in progress'),

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface McpOauthRow {
2828
tokens: OAuthTokens | null
2929
codeVerifier: string | null
3030
state: string | null
31+
stateCreatedAt: Date | null
3132
updatedAt: Date
3233
}
3334

@@ -97,6 +98,7 @@ export async function getOrCreateOauthRow(params: {
9798
tokens: null,
9899
codeVerifier: null,
99100
state: null,
101+
stateCreatedAt: null,
100102
updatedAt: new Date(),
101103
}
102104
}
@@ -124,6 +126,7 @@ async function mapOauthRow(row: RawOauthRow): Promise<McpOauthRow> {
124126
? await safeDecrypt(row.id, 'codeVerifier', row.codeVerifier, (d) => d)
125127
: null,
126128
state: row.state,
129+
stateCreatedAt: row.stateCreatedAt,
127130
updatedAt: row.updatedAt,
128131
}
129132
}
@@ -152,7 +155,7 @@ export async function loadOauthRowByState(state: string): Promise<McpOauthRow |
152155
.where(
153156
and(
154157
eq(mcpServerOauth.state, hashState(state)),
155-
gt(mcpServerOauth.updatedAt, new Date(Date.now() - STATE_TTL_MS))
158+
gt(mcpServerOauth.stateCreatedAt, new Date(Date.now() - STATE_TTL_MS))
156159
)
157160
)
158161
.limit(1)
@@ -188,9 +191,10 @@ export async function saveCodeVerifier(rowId: string, verifier: string): Promise
188191
}
189192

190193
export async function saveState(rowId: string, state: string): Promise<void> {
194+
const now = new Date()
191195
await db
192196
.update(mcpServerOauth)
193-
.set({ state: hashState(state), updatedAt: new Date() })
197+
.set({ state: hashState(state), stateCreatedAt: now, updatedAt: now })
194198
.where(eq(mcpServerOauth.id, rowId))
195199
}
196200

@@ -218,7 +222,7 @@ export async function clearVerifier(rowId: string): Promise<void> {
218222
export async function clearState(rowId: string): Promise<void> {
219223
await db
220224
.update(mcpServerOauth)
221-
.set({ state: null, updatedAt: new Date() })
225+
.set({ state: null, stateCreatedAt: null, updatedAt: new Date() })
222226
.where(eq(mcpServerOauth.id, rowId))
223227
}
224228

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE "mcp_server_oauth" ADD COLUMN "state_created_at" timestamp;

0 commit comments

Comments
 (0)