Skip to content

Conversation

@vibegui
Copy link
Contributor

@vibegui vibegui commented Jan 3, 2026

Summary by cubic

Implements automatic OAuth token refresh for downstream MCP connections to keep users signed in when access tokens expire or after hot reload. The proxy now uses cached tokens and refreshes them using the refresh_token grant.

  • New Features

    • Persist downstream OAuth tokens with refresh data (access, refresh, expiry, scope, clientId/secret, tokenEndpoint).
    • Token refresh utility using the OAuth2 refresh_token grant.
    • API endpoints to save/delete tokens and check token status.
    • Proxy reads cached tokens, auto-refreshes on expiry, and falls back to connection_token when needed.
    • Frontend posts full token info to the new API after OAuth.
    • Enforces organization access on token save.
  • Migration

    • Adds migration 016 to store clientId, clientSecret, and tokenEndpoint, plus an index for connectionId+userId.
    • Run DB migrations; no breaking changes.
    • To enable refresh, ensure the OAuth flow returns a refresh_token and the frontend posts token info to the new endpoint.

Written for commit a9a76f9. Summary will update on new commits.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 3, 2026

🧪 Benchmark

Should we run the MCP Gateway benchmark for this PR?

React with 👍 to run the benchmark.

Reaction Action
👍 Run quick benchmark (10 & 128 tools)

Benchmark will run on the next push after you react.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 3, 2026

Release Options

Should a new version be published when this PR is merged?

React with an emoji to vote on the release type:

Reaction Type Next Version
👍 Prerelease 1.0.18-alpha.1
🎉 Patch 1.0.18
❤️ Minor 1.1.0
🚀 Major 2.0.0

Current version: 1.0.17

Deployment

  • Deploy to production (triggers ArgoCD sync after Docker image is published)

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 11 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="apps/mesh/src/storage/downstream-token.ts">

<violation number="1" location="apps/mesh/src/storage/downstream-token.ts:91">
P2: Race condition in upsert: if two concurrent calls both find no existing token, both will attempt to insert, causing a unique constraint violation. Consider using Kysely&#39;s `onConflict` for atomic upsert, or wrap in a transaction with proper locking.</violation>

<violation number="2" location="apps/mesh/src/storage/downstream-token.ts:199">
P1: Invalid date strings in `expiresAt` cause `isExpired` to return `false`, treating corrupted tokens as never expired. When `new Date(invalidString).getTime()` returns `NaN`, the comparison `NaN &lt; Date.now()` is always `false`. Consider validating the date and defaulting to expired (fail-safe) for invalid values.</violation>
</file>

<file name="apps/mesh/src/api/routes/downstream-token.ts">

<violation number="1" location="apps/mesh/src/api/routes/downstream-token.ts:39">
P1: Authorization check missing: The comment states &quot;Verify connection exists and user has access&quot; but the code only checks existence. Consider passing `organizationId` to `findById()` to verify user has access to this connection, preventing cross-organization token association.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

}

async upsert(data: DownstreamTokenData): Promise<DownstreamToken> {
const existing = await this.get(data.connectionId, data.userId);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Race condition in upsert: if two concurrent calls both find no existing token, both will attempt to insert, causing a unique constraint violation. Consider using Kysely's onConflict for atomic upsert, or wrap in a transaction with proper locking.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/storage/downstream-token.ts, line 91:

<comment>Race condition in upsert: if two concurrent calls both find no existing token, both will attempt to insert, causing a unique constraint violation. Consider using Kysely&#39;s `onConflict` for atomic upsert, or wrap in a transaction with proper locking.</comment>

<file context>
@@ -0,0 +1,246 @@
+  }
+
+  async upsert(data: DownstreamTokenData): Promise&lt;DownstreamToken&gt; {
+    const existing = await this.get(data.connectionId, data.userId);
+    const now = new Date().toISOString();
+
</file context>

✅ Addressed in 47683a8

}

const expiresAt =
token.expiresAt instanceof Date
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Invalid date strings in expiresAt cause isExpired to return false, treating corrupted tokens as never expired. When new Date(invalidString).getTime() returns NaN, the comparison NaN < Date.now() is always false. Consider validating the date and defaulting to expired (fail-safe) for invalid values.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/storage/downstream-token.ts, line 199:

<comment>Invalid date strings in `expiresAt` cause `isExpired` to return `false`, treating corrupted tokens as never expired. When `new Date(invalidString).getTime()` returns `NaN`, the comparison `NaN &lt; Date.now()` is always `false`. Consider validating the date and defaulting to expired (fail-safe) for invalid values.</comment>

<file context>
@@ -0,0 +1,246 @@
+    }
+
+    const expiresAt =
+      token.expiresAt instanceof Date
+        ? token.expiresAt
+        : new Date(token.expiresAt);
</file context>

✅ Addressed in ea42fc8

}

// Verify connection exists and user has access
const connection = await ctx.storage.connections.findById(connectionId);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Authorization check missing: The comment states "Verify connection exists and user has access" but the code only checks existence. Consider passing organizationId to findById() to verify user has access to this connection, preventing cross-organization token association.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/api/routes/downstream-token.ts, line 39:

<comment>Authorization check missing: The comment states &quot;Verify connection exists and user has access&quot; but the code only checks existence. Consider passing `organizationId` to `findById()` to verify user has access to this connection, preventing cross-organization token association.</comment>

<file context>
@@ -0,0 +1,144 @@
+  }
+
+  // Verify connection exists and user has access
+  const connection = await ctx.storage.connections.findById(connectionId);
+  if (!connection) {
+    return c.json({ error: &quot;Connection not found&quot; }, 404);
</file context>

✅ Addressed in a9a76f9

@vibegui vibegui force-pushed the feat/oauth-token-refresh branch from 572cd11 to 45c0bd4 Compare January 6, 2026 19:22
- Add migration 015 with clientId, clientSecret, tokenEndpoint fields
- Create DownstreamTokenStorage for persisting OAuth tokens with refresh info
- Add token refresh utility using OAuth2 refresh_token grant
- Add API endpoints for managing downstream OAuth tokens
- Update proxy to use cached tokens and auto-refresh on expiry
- Update frontend to save full token info via new API

This fixes the issue where re-authentication was needed after hot reload
or when access tokens expired. Now tokens are automatically refreshed
using the refresh_token grant when they expire.
Main branch migration 015-monitoring-properties takes priority.
OAuth branch migration is renumbered to 016 to maintain correct order.
@vibegui vibegui force-pushed the feat/oauth-token-refresh branch from 45c0bd4 to 7e20fbc Compare January 8, 2026 15:22
@vibegui vibegui merged commit dbd6024 into main Jan 8, 2026
5 checks passed
@vibegui vibegui deleted the feat/oauth-token-refresh branch January 8, 2026 18:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants