oauth-provider: auth.api.oauth2Consent() always fails "request not found" without a raw Request
Is this suited for github?
- Yes, this is suited for github
Reproduction
The @better-auth/mcp docs describe a custom consentPage. Completing that consent server-side via auth.api.oauth2Consent always fails, so a custom consent page can never accept a connection.
Minimal steps:
- Configure auth with
jwt()+mcp()(a bareoauthProvider()shows the same) andconsentPage: "/consent". - Drive the authorize flow until the browser lands on the custom consent page (
/consent?response_type=code&…&sig=…). Keep the signed query string and the session cookie. - From a server route that has both, complete consent:
await auth.api.oauth2Consent({
headers: new Headers({ cookie }),
body: { accept: true, oauth_query: oauthQuery },
});- Observe the throw:
{
"error": "invalid_request",
"error_description": "request not found"
}accept: false works; only the accept path fails.
Current vs. Expected behavior
Current
auth.api.oauth2Consent({ headers, body })throws"request not found". Passingheaders(including the session cookie) does not help — the missing piece isctx.request, not auth.- Every custom-consent accept is impossible through the
auth.apisurface.
Root cause:
consentEndpoint()inpackages/oauth-provider/src/consent.tsends withreturn await authorize(ctx, { postLogin: … }).authorizeisrunOAuth2Authorize, which re-dispatches the authorize endpoint (dispatchAuthEndpoint(oauth2AuthorizeEndpoint, …)).authorizeEndpoint()inpackages/oauth-provider/src/authorize.tsbegins with:
if (!ctx.request) {
throw new APIError('UNAUTHORIZED', {
error_description: 'request not found',
error: 'invalid_request',
});
}- When the endpoint is invoked through
auth.api.*instead ofauth.handler(request), better-call builds the endpoint context withoutrequest, so this guard always trips. The consent handler already has everything the re-dispatched authorize needs (ctx.body.oauth_queryis verified andctx.queryis set), so the guard is not required for this internal re-dispatch.
Expected
- Completing consent via
auth.api.oauth2Consent()should work from a server route with a valid session — or the API/types should requirerequestexplicitly and document it, instead of failing at runtime with a genericinvalid_request.
What version of Better Auth are you using?
1.7.6 (also reproducible against main as of 2026-09-30; the guard and the consent re-dispatch are unchanged there).
System info
npx auth info --json is not runnable in this Bun app; versions from the lockfile:
better-auth: 1.7.6
@better-auth/oauth-provider: 1.7.6
@better-auth/mcp: 1.7.6
runtime: Bun 1.4.2
os: Linux x86_64Which area(s) are affected? (Select all that apply)
- Backend
- Package
- Types
Auth config (if applicable)
import { betterAuth } from 'better-auth';
import { jwt } from 'better-auth/plugins';
import { cimd } from '@better-auth/cimd';
import { mcp } from '@better-auth/mcp';
export const auth = betterAuth({
plugins: [
jwt(),
mcp({
loginPage: '/login',
consentPage: '/consent',
resource: 'https://example.com/mcp',
}),
cimd({ fetchClientMetadataResource, metadataProfile: 'mcp-2026-07-28' }),
],
});Additional context
Verified workarounds (both complete the consent and return the redirect carrying the authorization code):
- Pass a
RequestandasResponse: false:
const request = new Request('http://localhost:3000/api/auth/oauth2/consent', {
method: 'POST',
headers: { accept: 'application/json', 'content-type': 'application/json', cookie },
body: JSON.stringify({ accept: true, oauth_query: oauthQuery }),
});
await auth.api.oauth2Consent({
headers: request.headers,
body: { accept: true, oauth_query: oauthQuery },
request,
asResponse: false,
});- Or run the same endpoint through
auth.handler(request)and read the JSON response.
Related: #8806 (auth.api.oauth2UserInfo → "request not found", closed/locked). The answer there — "pass headers explicitly" — does not apply here: headers are already passed, and the failing check is ctx.request.
Impact: the documented @better-auth/mcp custom-consent path is unusable. Every external MCP host (Claude Code, Cursor, custom clients) that goes through a custom consent page hits this on Approve.
Source: better-auth/better-auth