Skip to content

feat(auth): migrate to @better-auth/mcp; fix OAuth on fresh installs - #20

Merged
willynikes2 merged 1 commit into
masterfrom
feat/better-auth-mcp
Oct 5, 2026
Merged

willynikes2 merged 1 commit into
masterfrom
feat/better-auth-mcp

Conversation

@willynikes2

Copy link
Copy Markdown
Owner

Why

better-auth 1.7 moved the MCP plugin out of core into @better-auth/mcp, an OAuth 2.1 provider built on @better-auth/oauth-provider. We were pinned to 1.6.x to keep the old API. This moves to the supported plugin, and fixes two install problems found along the way.

What changes

  • Tokens: access tokens are EdDSA-signed JWTs bound to <BETTER_AUTH_URL>/mcp (or the bare origin). The auth middleware verifies them locally against the server's own JWKS.
  • Discovery: RFC 9728 protected resource metadata, RFC 8414 metadata at both /.well-known/oauth-authorization-server and its /api/auth issuer path, and OIDC config. A 401 now includes a WWW-Authenticate: Bearer resource_metadata=… challenge so MCP clients can start OAuth.
  • Login: new /consent page (shows which client is asking, Allow/Deny). The sign-in page resumes /oauth2/authorize. Dynamic client registration stays enabled for Claude; HTTPS redirect URIs are now required.
  • Fresh installs: the auth tables were never created, so OAuth didn't work at all. kb start now creates or upgrades the schema on every start.
  • Upgrades: databases from the old plugin have tables whose names collide with the new ones. They and their indexes are moved aside as legacy_* (nothing dropped) before migrating. Accounts and sessions are kept; MCP clients must reconnect.
  • Accounts: public sign-up stays off (fix(auth): disable public sign-up on the OAuth provider (critical) #19). New kb auth add-user <email> (masked prompt or --password-stdin) is the way to create one.
  • Versions: better-auth, @better-auth/mcp and @better-auth/oauth-provider pinned to ~1.7.7.

Testing

  • tests/auth-oauth.test.js: the real provider in-process. Sign-up refused, wrong password, anonymous authorize gets no code, full flow + refresh, bare-origin resource, consent denied, forged/tampered tokens rejected.
  • tests/auth-migrate.test.js: upgrade from a pre-1.7 production schema fixture (schema only) with a user and legacy rows. Legacy tables and indexes are archived with rows intact, the run is idempotent, the existing user signs in and authorizes, and old tokens are rejected. Caught an index-name collision that would have failed the upgrade.
  • Package smoke test now drives the full Claude connector flow over real HTTP against the installed tarball: 401 challenge → resource metadata → RFC 8414 discovery → dynamic registration → sign-in → browser redirect to consent → consent → token → MCP initialize with the token and REST access. It also checks public sign-up is refused.
  • Ran the upgrade on a copy of a real production auth.db: 1 user, 8 sessions, 22 old clients and 562 old tokens preserved (archived), integrity ok, 0 FK violations, second run a no-op.
  • npm test: 140/140. npm audit --omit=dev: 0 vulnerabilities.

Upgrade note

After deploying, previously connected MCP clients (e.g. the Claude connector) must be removed and re-added.

🤖 Generated with Claude Code

better-auth 1.7 moved the MCP plugin out of core into @better-auth/mcp,
an OAuth 2.1 provider built on @better-auth/oauth-provider. Move to it
(better-auth, @better-auth/mcp and @better-auth/oauth-provider ~1.7.7):

- Access tokens are EdDSA-signed JWTs bound to `<BETTER_AUTH_URL>/mcp`
  (or the bare origin). brainAuth verifies them locally against the
  server's own JWKS instead of looking up a session.
- Discovery: RFC 9728 protected resource metadata, RFC 8414 metadata at
  both /.well-known/oauth-authorization-server and its /api/auth issuer
  path, OIDC config. 401s carry a WWW-Authenticate resource_metadata
  challenge so MCP clients can start the flow.
- New /consent page; the sign-in page resumes /oauth2/authorize.
  Dynamic client registration stays enabled for Claude and other MCP
  clients; HTTPS redirect URIs are now required.

Fix two install problems found along the way:

- The auth tables were never created, so OAuth didn't work on a fresh
  install. `kb start` now creates or upgrades the schema on every start.
  On databases from the old plugin it moves the colliding tables and
  their indexes aside as legacy_* (nothing is dropped) before migrating;
  accounts and sessions are kept.
- With public sign-up disabled there was no way to create the owner
  account. Add `kb auth add-user <email>` (masked prompt or
  --password-stdin).

Tests: the real provider in-process (sign-up refused, wrong password,
anonymous authorize, full flow + refresh, bare-origin resource, consent
denied, forged/tampered tokens), an upgrade from a pre-1.7 production
schema fixture, and the package smoke test now drives the full Claude
connector flow over HTTP: 401 challenge, discovery, registration,
sign-in, browser redirect to consent, token, then MCP initialize.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread src/cli/auth-cli.js
console.log(`Created ${user.email}. Sign in with it when an MCP client asks you to authorize.`);
process.exit(0);
} catch (err) {
console.error(`kb auth add-user: ${err.message}`);
Comment thread src/server.js
// 2. OAuth schema: create it on first run, upgrade it after better-auth updates
const archivedAuthTables = await migrateAuthSchema();
if (archivedAuthTables.length) {
console.log(`OAuth schema upgraded; previous OAuth clients/tokens kept in ${archivedAuthTables.join(', ')}. Reconnect MCP clients.`);
Comment thread src/server.js
Comment on lines +121 to +123
app.get('/consent', (req, res) => {
res.sendFile(join(__dirname, 'public', 'consent.html'));
});
@willynikes2
willynikes2 merged commit 58cffa0 into master Oct 5, 2026
8 of 9 checks passed
@willynikes2
willynikes2 deleted the feat/better-auth-mcp branch October 5, 2026 00:46
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.

2 participants