Skip to content

@better-auth/oauth-provider breaks convex()'s /api/auth/convex/token at runtime (login bounces to /login) — OIDC-provider collision? #395

Description

@ParwarYasinQadr

Summary

Adding @better-auth/oauth-provider (to expose an MCP / OAuth 2.1 server) alongside the convex() plugin builds and deploys cleanly but breaks login at runtime: after a successful sign-in, GET /api/auth/convex/token returns 404 while authenticated, so the Convex client never receives its session JWT and the app bounces back to the login page. Removing oauthProvider fixes it instantly.

I believe this is a collision between @better-auth/oauth-provider and the deprecated oidc-provider that convex() registers internally to serve /convex/token. I'd love to know if there's a supported way to run them together, or if a standalone (non-plugin) OAuth server is the only path on Convex.

Environment

  • @convex-dev/better-auth: 0.12.5 (local install)
  • better-auth: 1.6.18
  • @better-auth/oauth-provider: 1.6.18
  • @better-auth/core: 1.6.18
  • convex: 1.42.0
  • Node: 24.13.1
  • Framework: Next.js (App Router), auth handler mounted at /api/auth/[...all]

Static JWKS (the experimental optimization) is enabled: convex({ authConfig, jwks: process.env.JWKS }) + getAuthConfigProvider({ jwks: process.env.JWKS }).

What I'm trying to do

Turn the Convex app into an OAuth 2.1 provider so MCP clients (Claude Desktop, etc.) can authenticate via the standard /.well-known discovery + PKCE flow, using @better-auth/oauth-provider.

Steps to reproduce

  1. oauthProvider requires a "jwt" plugin. Adding it alone throws:

BetterAuthError: jwt_config
at getJwtPlugin (@better-auth/oauth-provider/dist/utils-*.mjs)

Source — @better-auth/oauth-provider looks up a plugin by id "jwt":
const getJwtPlugin = (ctx) => {
const plugin = ctx.getPlugin("jwt");
if (!plugin) throw new BetterAuthError("jwt_config");
return plugin;
};
But convex() merges its JWT into a single plugin with id: "convex" (and ...jwt.schema) — it does not expose id: "jwt" (@convex-dev/better-auth/dist/plugins/convex/index.js). So ctx.getPlugin("jwt") is undefined. → I must add an explicit jwt().

  1. The explicit jwt() must be RS256. getAuthConfigProvider() returns type: "customJwt", algorithm: "RS256" (dist/auth-config.js), and Convex's customJwt only accepts RS256/ES256 — so a default-EdDSA jwt() would break Convex's session-token validation. Pinning RS256 reuses the existing key.

  2. Final config (this builds, generates schema, typechecks, and pushes cleanly):
    disabledPaths: ["/token"],
    plugins: [
    convex({ authConfig, jwks: process.env.JWKS }),
    organization({ /* ... / }),
    admin({ /
    ... */ }),
    // ...existing plugins, unchanged...
    jwt({ jwks: { keyPairConfig: { alg: "RS256" } }, disableSettingJwtHeader: true }),
    oauthProvider({ loginPage: "/login", consentPage: "/oauth/consent" }),
    ]
    npx auth generate --output generatedSchema.ts succeeds and adds the 4 OAuth tables; custom indexes preserved; tsc clean; convex dev reports "Convex functions ready!".

▎ Note: schema generation requires env vars (SITE_URL etc.) because oauthProvider.init calls new URL(baseURL) and throws Invalid URL with an empty base in the env-less CLI context.

Expected behavior

Login continues to work; /api/auth/convex/token returns the Convex session JWT for an authenticated user.

Actual behavior

Sign-in succeeds at the Better Auth level, but the Convex token endpoint 404s while authenticated, so the Convex client stays unauthenticated and the app redirects to /login. Next.js request log:

POST /api/auth/sign-in/email 200 ← Better Auth sign-in SUCCEEDS
GET /api/auth/organization/list 200 ← BA session valid
GET /api/auth/get-session 200
GET / 307 ← redirect
GET /api/auth/convex/token 404 ← ❌ Convex token endpoint broken (authenticated!)
GET /login 200 ← bounced back to /login
POST /api/auth/organization/set-active 200 ← BA session still works (misleading)
GET /api/auth/convex/token 404 ← still 404

The Convex deployment also logs this warning on every request once oauthProvider is active:
[WARN] [Better Auth]: Please ensure '/.well-known/oauth-authorization-server/api/auth' exists.
Upon completion, clear with silenceWarnings.oauthAuthServerConfig.

Removing oauthProvider (and the explicit jwt()) restores /convex/token → login works again.

Root-cause hypothesis

convex() serves /convex/token (and the Convex OIDC discovery) via the deprecated oidc-provider plugin it registers internally. @better-auth/oauth-provider is the successor to oidc-provider, and registering it appears to shadow/replace that internal OIDC handling — so /convex/token no longer resolves (404). This seems structural (two OIDC providers in one instance), not a config typo.

What I've ruled out

  • disabledPaths: ["/token"] is not the cause — Better Auth matches it exactly (disabledPaths.includes(normalizedPath)), and the Convex endpoint normalizes to /convex/token, not /token.
  • Static JWKS is not involved — the 404 happens regardless; removing it doesn't change anything (and it's unrelated to OIDC routing).
  • Not a build/type error — schema gen, tsc, and the convex dev push are all clean. The break is purely runtime.
  • GET /convex/token → 404 when logged out is normal — only the 404 while authenticated is the bug.

Questions

  1. Is running @better-auth/oauth-provider alongside convex() supported? If so, what's the correct configuration?
  2. Does the internal oidc-provider that convex() uses conflict with the newer oauth-provider by design? Is there an option to prevent the collision (e.g. namespacing, or having convex() expose its JWT under id: "jwt" so a second jwt() isn't needed)?
  3. If it's not supported, is there a roadmap for OAuth-provider / MCP support — or is a standalone OAuth Authorization Server (separate Convex HTTP routes / a Convex component, not a Better Auth plugin) the recommended approach?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions