Skip to content

Self-host: resume the MCP OAuth authorize after login#1233

Merged
RhysSullivan merged 1 commit into
mainfrom
selfhost-mcp-oauth-resume
Jul 1, 2026
Merged

Self-host: resume the MCP OAuth authorize after login#1233
RhysSullivan merged 1 commit into
mainfrom
selfhost-mcp-oauth-resume

Conversation

@RhysSullivan

Copy link
Copy Markdown
Collaborator

Problem

When an MCP client connects to a self-host instance and the user is signed out, Better Auth's mcp authorize 302s to /login?response_type=code&client_id=... with the original OAuth params (no returnTo). login.tsx only honored a returnTo query param, so after sign-in it sent the user to / and the authorize never resumed. The MCP client waiting at its loopback callback never received a code, so the connection silently failed.

This is distinct from the integration OAuth callback recovery, which arrives as returnTo; the MCP authorize to login to resume path was never wired through the login page. The e2e suite did not catch it because it signs in before authorizing, skipping this path.

Fix

  • mcpAuthorizeResumeTarget(search) in return-to.ts: a pure helper that rebuilds /api/auth/mcp/authorize?... from the OAuth params Better Auth left on /login. The target is the same-origin authorize endpoint, which validates client_id/redirect_uri, so it is not an open redirect.
  • login.tsx: after sign-in, resume the authorize if the URL carries one, else honor a safe returnTo, else land on the dashboard.

Tests

return-to.test.ts covers mcpAuthorizeResumeTarget (rebuilds the authorize URL from a real redirect, ignores non-authorize searches).

Better Auth's MCP authorize bounces an unauthenticated client to /login
carrying the original OAuth request as query params, with no returnTo. The
login page only honored returnTo, so after sign-in it landed on the
dashboard and the authorization never resumed: the MCP client waiting at its
callback never got its code. After sign-in, if the URL carries an MCP
authorize request, hand it back to /api/auth/mcp/authorize so the now
authenticated request issues a code (and the consent shim takes over). The
target is the same-origin authorize endpoint, which validates the client and
redirect_uri, so it is not an open redirect.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 30, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
executor-cloud 4103ecb Jun 30 2026, 11:46 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 30, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
executor-marketing 4103ecb Commit Preview URL

Branch Preview URL
Jun 30 2026, 11:45 PM

@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown

Greptile Summary

Wires up the missing MCP OAuth authorize resume path in the self-host login flow. When Better Auth redirects an unauthenticated MCP client to /login with OAuth params, the login page now detects them and redirects back to /api/auth/mcp/authorize after sign-in instead of dropping the user on /.

  • mcpAuthorizeResumeTarget in return-to.ts detects the OAuth params (requires response_type=code, client_id, and redirect_uri) and rebuilds the same-origin authorize URL; the hardcoded /api/auth/mcp/authorize target means it cannot produce an open redirect.
  • login.tsx inserts the new helper at the top of the postLogin priority chain (MCP resume → returnTo/), with tests covering the key null-return cases.

Confidence Score: 4/5

Safe to merge; the change is narrow and well-contained, with the only redirect target being a hardcoded same-origin path.

The new mcpAuthorizeResumeTarget helper is small, well-commented, and the test suite covers the important detection cases. The one gap is a missing symmetric test assertion (redirect_uri present, client_id absent), which is a minor coverage omission rather than a functional defect in the implementation.

No files require special attention beyond the minor test gap in return-to.test.ts.

Important Files Changed

Filename Overview
apps/host-selfhost/src/auth/return-to.ts Adds mcpAuthorizeResumeTarget helper that detects MCP OAuth params on /login and rebuilds the same-origin authorize URL; logic is sound and redirect target is hardcoded.
apps/host-selfhost/web/login.tsx Threads mcpAuthorizeResumeTarget into the post-login redirect priority chain (MCP resume > returnTo > /); change is minimal and correct.
apps/host-selfhost/src/auth/return-to.test.ts Tests cover the main happy path and several null-return cases; one symmetric null case (redirect_uri present, client_id absent) is not exercised.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant C as MCP Client
    participant BA as Better Auth (/api/auth/mcp/authorize)
    participant L as /login
    participant U as User

    C->>BA: "GET /api/auth/mcp/authorize?response_type=code&client_id=...&redirect_uri=..."
    BA-->>L: "302 /login?response_type=code&client_id=...&redirect_uri=..."
    U->>L: Submits email + password
    Note over L: mcpAuthorizeResumeTarget(search)<br/>detects OAuth params → builds resume URL
    L->>BA: "window.location.href = /api/auth/mcp/authorize?response_type=code&..."
    BA-->>C: "302 redirect_uri?code=..."
    Note over C: OAuth flow completes
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant C as MCP Client
    participant BA as Better Auth (/api/auth/mcp/authorize)
    participant L as /login
    participant U as User

    C->>BA: "GET /api/auth/mcp/authorize?response_type=code&client_id=...&redirect_uri=..."
    BA-->>L: "302 /login?response_type=code&client_id=...&redirect_uri=..."
    U->>L: Submits email + password
    Note over L: mcpAuthorizeResumeTarget(search)<br/>detects OAuth params → builds resume URL
    L->>BA: "window.location.href = /api/auth/mcp/authorize?response_type=code&..."
    BA-->>C: "302 redirect_uri?code=..."
    Note over C: OAuth flow completes
Loading

Reviews (1): Last reviewed commit: "Self-host: resume the MCP OAuth authoriz..." | Re-trigger Greptile

Comment on lines +68 to +72
// response_type alone is not enough: client_id and redirect_uri are required.
expect(mcpAuthorizeResumeTarget("?response_type=code&client_id=abc")).toBeNull();
expect(
mcpAuthorizeResumeTarget("?response_type=token&client_id=abc&redirect_uri=x"),
).toBeNull();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The test comment says "client_id and redirect_uri are required" but only exercises the direction where redirect_uri is absent — the symmetric case of redirect_uri present but client_id absent is not covered. Adding it makes the required-fields contract explicit in both directions.

Suggested change
// response_type alone is not enough: client_id and redirect_uri are required.
expect(mcpAuthorizeResumeTarget("?response_type=code&client_id=abc")).toBeNull();
expect(
mcpAuthorizeResumeTarget("?response_type=token&client_id=abc&redirect_uri=x"),
).toBeNull();
// response_type alone is not enough: client_id and redirect_uri are required.
expect(mcpAuthorizeResumeTarget("?response_type=code&client_id=abc")).toBeNull();
expect(mcpAuthorizeResumeTarget("?response_type=code&redirect_uri=x")).toBeNull();
expect(
mcpAuthorizeResumeTarget("?response_type=token&client_id=abc&redirect_uri=x"),
).toBeNull();

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Cloudflare preview

Torn down — the PR is closed.

@pkg-pr-new

pkg-pr-new Bot commented Jun 30, 2026

Copy link
Copy Markdown

Open in StackBlitz

@executor-js/cli

npm i https://pkg.pr.new/@executor-js/cli@1233

@executor-js/config

npm i https://pkg.pr.new/@executor-js/config@1233

@executor-js/execution

npm i https://pkg.pr.new/@executor-js/execution@1233

@executor-js/sdk

npm i https://pkg.pr.new/@executor-js/sdk@1233

@executor-js/codemode-core

npm i https://pkg.pr.new/@executor-js/codemode-core@1233

@executor-js/runtime-quickjs

npm i https://pkg.pr.new/@executor-js/runtime-quickjs@1233

@executor-js/plugin-file-secrets

npm i https://pkg.pr.new/@executor-js/plugin-file-secrets@1233

@executor-js/plugin-graphql

npm i https://pkg.pr.new/@executor-js/plugin-graphql@1233

@executor-js/plugin-keychain

npm i https://pkg.pr.new/@executor-js/plugin-keychain@1233

@executor-js/plugin-mcp

npm i https://pkg.pr.new/@executor-js/plugin-mcp@1233

@executor-js/plugin-onepassword

npm i https://pkg.pr.new/@executor-js/plugin-onepassword@1233

@executor-js/plugin-openapi

npm i https://pkg.pr.new/@executor-js/plugin-openapi@1233

executor

npm i https://pkg.pr.new/executor@1233

commit: 4103ecb

@RhysSullivan
RhysSullivan merged commit 0a6fe93 into main Jul 1, 2026
16 checks passed
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.

1 participant