Skip to content

Commit 81188c8

Browse files
committed
Fix OAuth token exchange 500 error for MCP clients
The token endpoint handler consumed the request body via request.text() to check for the `resource` parameter, but only reconstructed the Request when `resource` was missing. When MCP clients (e.g. Claude Code) sent `resource` in the token exchange, the body was consumed but never rebuilt, causing Better Auth to receive an empty body and return 500. Always reconstruct the request after reading the body. Also adds consent page redirect URL fallback and DB migration for new OAuth columns (require_pkce, auth_time).
1 parent 2b5a106 commit 81188c8

6 files changed

Lines changed: 7185 additions & 2 deletions

File tree

apps/web/src/routes/api/auth/$.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ export const Route = createFileRoute('/api/auth/$')({
4343
* Better-auth catch-all route handler
4444
*/
4545
POST: async ({ request }) => {
46-
// Rate-limit OAuth dynamic client registration to prevent spam/phishing
4746
const url = new URL(request.url)
47+
48+
// Rate-limit OAuth dynamic client registration to prevent spam/phishing
4849
if (url.pathname.endsWith('/oauth2/register')) {
4950
if (isRegistrationRateLimited(request)) {
5051
return Response.json(
@@ -54,6 +55,28 @@ export const Route = createFileRoute('/api/auth/$')({
5455
}
5556
}
5657

58+
// Ensure `resource` is present in token exchange requests.
59+
// Without it, better-auth issues opaque tokens instead of JWTs,
60+
// breaking `verifyAccessToken` in the MCP handler.
61+
// Reading the body consumes the stream, so we always reconstruct
62+
// the request to avoid passing a consumed body to better-auth.
63+
if (url.pathname.endsWith('/oauth2/token')) {
64+
const contentType = request.headers.get('content-type') ?? ''
65+
if (contentType.includes('application/x-www-form-urlencoded')) {
66+
const body = await request.text()
67+
const params = new URLSearchParams(body)
68+
if (!params.has('resource')) {
69+
const { config } = await import('@/lib/server/config')
70+
params.set('resource', `${config.baseUrl}/api/mcp`)
71+
}
72+
request = new Request(request.url, {
73+
method: request.method,
74+
headers: request.headers,
75+
body: params.toString(),
76+
})
77+
}
78+
}
79+
5780
const { auth } = await import('@/lib/server/auth/index')
5881
return await auth.handler(request)
5982
},

apps/web/src/routes/oauth/consent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ function ConsentPage() {
121121
}
122122

123123
const data = await response.json()
124-
const redirectTo = data.uri ?? data.redirectUrl
124+
const redirectTo = data.url ?? data.uri ?? data.redirectUrl
125125
if (redirectTo) {
126126
window.location.href = redirectTo
127127
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE "oauth_client" ADD COLUMN "require_pkce" boolean;--> statement-breakpoint
2+
ALTER TABLE "oauth_refresh_token" ADD COLUMN "auth_time" timestamp with time zone;

0 commit comments

Comments
 (0)