Skip to content

Commit 9270e52

Browse files
Ripwordsclaude
andcommitted
fix(mcp): correct JWT issuer and consent response parsing
Two bugs found by the integration test: 1. /api/mcp handlers verified JWT with issuer = BETTER_AUTH_URL but better-auth sets iss = ctx.context.baseURL which includes the /api/auth path prefix. Fix: use BETTER_AUTH_URL + "/api/auth" as the expected issuer in all three mcp.*.ts handlers. 2. /api/oauth/consent forwarded the Allow/Deny to better-auth but: a) Missing Origin header caused a MISSING_OR_NULL_ORIGIN 403. Fix: forward the incoming Origin or fall back to BETTER_AUTH_URL's origin. b) Response body was read as { redirect_uri } but better-auth returns { redirect: true, url: "..." }. Fix: read json.url as the redirect URI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1784c96 commit 9270e52

4 files changed

Lines changed: 31 additions & 5 deletions

File tree

apps/dashboard/server/api/mcp.delete.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@ import { env } from "../lib/env"
55
import { buildContextFromJwt } from "../mcp/context"
66
import { buildMcpServer } from "../mcp/server"
77

8+
// better-auth mounts the auth handler at /api/auth, so the JWT issuer is
9+
// BETTER_AUTH_URL + "/api/auth" (better-auth sets iss = ctx.context.baseURL,
10+
// and baseURL in auth.ts is configured as env.BETTER_AUTH_URL which Nitro
11+
// then appends "/api/auth" to during the handler registration).
12+
const ISSUER = `${env.BETTER_AUTH_URL}/api/auth`
13+
814
const handler = mcpHandler(
915
{
1016
jwksUrl: `${env.BETTER_AUTH_URL}/api/auth/jwks`,
1117
verifyOptions: {
12-
issuer: env.BETTER_AUTH_URL,
18+
issuer: ISSUER,
1319
audience: `${env.BETTER_AUTH_URL}/api/mcp`,
1420
},
1521
},

apps/dashboard/server/api/mcp.get.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@ import { env } from "../lib/env"
55
import { buildContextFromJwt } from "../mcp/context"
66
import { buildMcpServer } from "../mcp/server"
77

8+
// better-auth mounts the auth handler at /api/auth, so the JWT issuer is
9+
// BETTER_AUTH_URL + "/api/auth" (better-auth sets iss = ctx.context.baseURL,
10+
// and baseURL in auth.ts is configured as env.BETTER_AUTH_URL which Nitro
11+
// then appends "/api/auth" to during the handler registration).
12+
const ISSUER = `${env.BETTER_AUTH_URL}/api/auth`
13+
814
const handler = mcpHandler(
915
{
1016
jwksUrl: `${env.BETTER_AUTH_URL}/api/auth/jwks`,
1117
verifyOptions: {
12-
issuer: env.BETTER_AUTH_URL,
18+
issuer: ISSUER,
1319
audience: `${env.BETTER_AUTH_URL}/api/mcp`,
1420
},
1521
},

apps/dashboard/server/api/mcp.post.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@ import { env } from "../lib/env"
55
import { buildContextFromJwt } from "../mcp/context"
66
import { buildMcpServer } from "../mcp/server"
77

8+
// better-auth mounts the auth handler at /api/auth, so the JWT issuer is
9+
// BETTER_AUTH_URL + "/api/auth" (better-auth sets iss = ctx.context.baseURL,
10+
// and baseURL in auth.ts is configured as env.BETTER_AUTH_URL which Nitro
11+
// then appends "/api/auth" to during the handler registration).
12+
const ISSUER = `${env.BETTER_AUTH_URL}/api/auth`
13+
814
const handler = mcpHandler(
915
{
1016
jwksUrl: `${env.BETTER_AUTH_URL}/api/auth/jwks`,
1117
verifyOptions: {
12-
issuer: env.BETTER_AUTH_URL,
18+
issuer: ISSUER,
1319
audience: `${env.BETTER_AUTH_URL}/api/mcp`,
1420
},
1521
},

apps/dashboard/server/api/oauth/consent.post.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,19 @@ export default defineEventHandler(async (event) => {
3232
const body = await readValidatedBody(event, Body.parse)
3333

3434
const url = new URL(`${env.BETTER_AUTH_URL}/api/auth/oauth2/consent`)
35+
// better-auth's oauthProvider requires an Origin header on the consent
36+
// endpoint to protect against CSRF. Forward the incoming Origin if present,
37+
// or use the BETTER_AUTH_URL origin as a fallback (this request comes from
38+
// our own frontend, so the origin is known and trusted).
39+
const origin = event.headers.get("origin") ?? new URL(env.BETTER_AUTH_URL).origin
3540
const res = await auth.handler(
3641
new Request(url, {
3742
method: "POST",
3843
headers: {
3944
"Content-Type": "application/json",
4045
accept: "application/json",
4146
cookie: event.headers.get("cookie") ?? "",
47+
origin,
4248
},
4349
body: JSON.stringify({
4450
accept: body.allow,
@@ -54,6 +60,8 @@ export default defineEventHandler(async (event) => {
5460
})
5561
}
5662

57-
const json = (await res.json()) as { redirect_uri: string }
58-
return { redirectUri: json.redirect_uri }
63+
// better-auth's /oauth2/consent handler returns { redirect: true, url: "..." }
64+
// (not { redirect_uri }) — the `url` field is the authorization code redirect.
65+
const json = (await res.json()) as { redirect: boolean; url: string }
66+
return { redirectUri: json.url }
5967
})

0 commit comments

Comments
 (0)