[GH-ISSUE #9198] MCP plugin: authorizeMCPOAuth issues auth codes before MFA/TOTP verification #19940

Open
opened 2026-04-15 19:18:45 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @mbochneak on GitHub (Apr 15, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/9198

Summary

The MCP plugin's authorizeMCPOAuth function (in src/plugins/mcp/authorize.ts) issues OAuth authorization codes as soon as a session exists, without checking whether multi-factor authentication (TOTP/two-factor) has been completed. This means a user who has logged in with a password or social provider but hasn't yet verified their TOTP token will receive a valid auth code that can be exchanged for access tokens.

Expected Behavior

When the two-factor plugin is enabled and the user has MFA configured, authorizeMCPOAuth should not issue an authorization code until the TOTP step is verified — the same way a normal login flow gates access on MFA completion.

Actual Behavior

authorizeMCPOAuth checks only for session existence:

const session = await getSessionFromCtx(ctx);
if (!session) {
    // redirect to login page
}
// ... immediately generates auth code and redirects

There is no check for twoFactorVerified, totpVerifiedAt, or any MFA-related session state. The auth code is generated at line 80 and the redirect happens at line 110 without any MFA gate.

Impact

This is a security concern for applications that rely on MFA — particularly in compliance-sensitive environments (e.g., CJIS, HIPAA). The OAuth MCP flow effectively bypasses the MFA requirement, issuing tokens that authenticate as the user without the second factor.

Current Workaround

Server-side applications can check for a TOTP-verified session at MCP request time (after the OAuth flow completes), rejecting requests where MFA wasn't completed:

const verifiedSession = await prisma.session.findFirst({
  where: {
    userId: mcpSession.userId,
    totpVerifiedAt: { not: null },
    expiresAt: { gt: new Date() },
  },
});
if (!verifiedSession) {
  res.status(403).json({ error: 'totp_required' });
  return;
}

This works but is an application-level workaround — the auth code should not be issued in the first place.

Suggested Fix

In authorizeMCPOAuth, after confirming the session exists, check whether the user has two-factor enabled and whether the session has completed TOTP verification. If MFA is required but not completed, redirect to the login/MFA page instead of issuing the auth code.

Environment

  • better-auth version: latest (as of April 2026)
  • Plugins: mcp(), twoFactor(), organization()
  • MFA provider: TOTP (authenticator app)

Thank you for the excellent work on better-auth and the MCP plugin!

Originally created by @mbochneak on GitHub (Apr 15, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/9198 ## Summary The MCP plugin's `authorizeMCPOAuth` function (in `src/plugins/mcp/authorize.ts`) issues OAuth authorization codes as soon as a session exists, without checking whether multi-factor authentication (TOTP/two-factor) has been completed. This means a user who has logged in with a password or social provider but hasn't yet verified their TOTP token will receive a valid auth code that can be exchanged for access tokens. ## Expected Behavior When the two-factor plugin is enabled and the user has MFA configured, `authorizeMCPOAuth` should not issue an authorization code until the TOTP step is verified — the same way a normal login flow gates access on MFA completion. ## Actual Behavior `authorizeMCPOAuth` checks only for session existence: ```js const session = await getSessionFromCtx(ctx); if (!session) { // redirect to login page } // ... immediately generates auth code and redirects ``` There is no check for `twoFactorVerified`, `totpVerifiedAt`, or any MFA-related session state. The auth code is generated at line 80 and the redirect happens at line 110 without any MFA gate. ## Impact This is a security concern for applications that rely on MFA — particularly in compliance-sensitive environments (e.g., CJIS, HIPAA). The OAuth MCP flow effectively bypasses the MFA requirement, issuing tokens that authenticate as the user without the second factor. ## Current Workaround Server-side applications can check for a TOTP-verified session at MCP request time (after the OAuth flow completes), rejecting requests where MFA wasn't completed: ```ts const verifiedSession = await prisma.session.findFirst({ where: { userId: mcpSession.userId, totpVerifiedAt: { not: null }, expiresAt: { gt: new Date() }, }, }); if (!verifiedSession) { res.status(403).json({ error: 'totp_required' }); return; } ``` This works but is an application-level workaround — the auth code should not be issued in the first place. ## Suggested Fix In `authorizeMCPOAuth`, after confirming the session exists, check whether the user has two-factor enabled and whether the session has completed TOTP verification. If MFA is required but not completed, redirect to the login/MFA page instead of issuing the auth code. ## Environment - better-auth version: latest (as of April 2026) - Plugins: `mcp()`, `twoFactor()`, `organization()` - MFA provider: TOTP (authenticator app) Thank you for the excellent work on better-auth and the MCP plugin!
GiteaMirror added the security label 2026-04-15 19:18:45 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Apr 15, 2026):

Hi @mbochneak! I'm Dosu and I’m helping the better-auth team.

This is a valid security gap. Looking at the code, authorizeMCPOAuth in authorize.ts calls getSessionFromCtx(ctx) and proceeds to issue an auth code if any valid session exists — there's no MFA check at all [1].

The root cause is that the two-factor plugin doesn't store MFA verification state on the session object itself [2]. Instead, it relies on the fact that a new session is only created after MFA succeeds during sign-in [3]. But authorizeMCPOAuth has no way to distinguish a fully-verified session from one that's still pending MFA — it just checks session existence.

A couple of things worth noting:

  1. The new OAuth Provider Plugin (/oauth2/* endpoints) was released to address OAuth 2.1 compliance gaps in the MCP plugin [4] [5], but it's unclear whether it includes MFA enforcement in its authorization flow either.

  2. Recent 2FA enforcement improvements expanded enforcement from path-based allowlists to session-based triggers covering all sign-in methods [6], but this enforcement happens during sign-in, not during OAuth authorization code issuance.

To fix this properly, the authorization endpoint would need to either:

  • Query the user's twoFactorEnabled field and verify MFA completion before issuing the code
  • Or extend the session schema with a field like twoFactorVerifiedAt that can be checked during authorization

Your workaround of checking for a TOTP-verified session at MCP request time is reasonable in the meantime.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other
Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:4248436862 --> @dosubot[bot] commented on GitHub (Apr 15, 2026): <!-- Greeting --> Hi @mbochneak! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is a valid security gap. Looking at the code, `authorizeMCPOAuth` in `authorize.ts` calls `getSessionFromCtx(ctx)` and proceeds to issue an auth code if any valid session exists — there's no MFA check at all [[1]](https://github.com/better-auth/better-auth/blob/a0caa1f6f8b2b9028b2917c8515280426ed2dbe4/packages/better-auth/src/plugins/mcp/authorize.ts#L44-L61). The root cause is that the two-factor plugin doesn't store MFA verification state on the session object itself [[2]](https://github.com/better-auth/better-auth/blob/a0caa1f6f8b2b9028b2917c8515280426ed2dbe4/packages/better-auth/src/plugins/two-factor/verify-two-factor.ts#L16-L126). Instead, it relies on the fact that a new session is only created *after* MFA succeeds during sign-in [[3]](https://github.com/better-auth/better-auth/blob/a0caa1f6f8b2b9028b2917c8515280426ed2dbe4/packages/better-auth/src/plugins/two-factor/verify-two-factor.ts#L61-L64). But `authorizeMCPOAuth` has no way to distinguish a fully-verified session from one that's still pending MFA — it just checks session existence. A couple of things worth noting: 1. **The new OAuth Provider Plugin** (`/oauth2/*` endpoints) was released to address OAuth 2.1 compliance gaps in the MCP plugin [[4]](https://github.com/better-auth/better-auth/issues/5459#issuecomment-3774841937) [[5]](https://github.com/better-auth/better-auth/discussions/7461), but it's unclear whether it includes MFA enforcement in its authorization flow either. 2. **Recent 2FA enforcement improvements** expanded enforcement from path-based allowlists to session-based triggers covering all sign-in methods [[6]](https://github.com/better-auth/better-auth/pull/9122), but this enforcement happens during sign-in, not during OAuth authorization code issuance. To fix this properly, the authorization endpoint would need to either: - Query the user's `twoFactorEnabled` field and verify MFA completion before issuing the code - Or extend the session schema with a field like `twoFactorVerifiedAt` that can be checked during authorization Your workaround of checking for a TOTP-verified session at MCP request time is reasonable in the meantime. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=brand-link).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-great_response#message_id=d647de59-9727-4b39-9dff-3e4711b321ec&feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-irrelevant_answer#message_id=d647de59-9727-4b39-9dff-3e4711b321ec&feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-incorrect_sources#message_id=d647de59-9727-4b39-9dff-3e4711b321ec&feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-too_verbose#message_id=d647de59-9727-4b39-9dff-3e4711b321ec&feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-hallucination#message_id=d647de59-9727-4b39-9dff-3e4711b321ec&feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-bug_report#message_id=d647de59-9727-4b39-9dff-3e4711b321ec&feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-other#message_id=d647de59-9727-4b39-9dff-3e4711b321ec&feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=chat-badge)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=This%20is%20a%20valid%20security%20gap.%20Looking%20at%20the%20code%2C%20%60authorizeMCPOAuth%60%20in%20%60authorize.ts%60%20calls%20%60getSessionFromCtx%28ctx%29%60%20and%20proceeds%20to%20issue%20an%20auth%20code%20if%20any%20valid%20session%20exists%20%E2%80%94%20there%27s%20no%20MFA%20check%20at%20all%20%5B%5B1%5D%5D%28https%3A//github.com/better-auth/better-auth/blob/a0caa1f6f8b2b9028b2917c8515280426ed2dbe4/packages/better-auth/src/plugins/mcp/authorize.ts%23L44-L61%29.%0A%0AThe%20root%20cause%20is%20that%20the%20two-factor%20plugin%20doesn%27t%20store%20MFA%20verification%20state%20on%20the%20session%20object%20itself%20%5B%5B2%5D%5D%28https%3A//github.com/better-auth/better-auth/blob/a0caa1f6f8b2b9028b2917c8515280426ed2dbe4/packages/better-auth/src/plugins/two-factor/verify-two-factor.ts%23L16-L126%29.%20Instead%2C%20it%20relies%20on%20the%20fact%20that%20a%20new%20session%20is%20only%20created%20%2Aafter%2A%20MFA%20succeeds%20during%20sign-in%20%5B%5B3%5D%5D%28https%3A//github.com/better-auth/better-auth/blob/a0caa1f6f8b2b9028b2917c8515280426ed2dbe4/packages/better-auth/src/plugins/two-factor/verify-two-factor.ts%23L61-L64%29.%20But%20%60authorizeMCPOAuth%60%20has%20no%20way%20to%20distinguish%20a%20fully-verified%20session%20from%20one%20that%27s%20still%20pending%20MFA%20%E2%80%94%20it%20just%20checks%20session%20existence.%0A%0AA%20couple%20of%20things%20worth%20noting%3A%0A%0A1.%20%2A%2AThe%20new%20OAuth%20Provider%20Plugin%2A%2A%20%28%60/oauth2/%2A%60%20endpoints%29%20was%20released%20to%20address%20OAuth%202.1%20compliance%20gaps%20in%20the%20MCP%20plugin%20%5B%5B4%5D%5D%28https%3A//github.com/better-auth/better-auth/issues/5459%23issuecomment-3774841937%29%20%5B%5B5%5D%5D%28https%3A//github.com/better-auth/better-auth/discussions/7461%29%2C%20but%20it%27s%20unclear%20whether%20it%20includes%20MFA%20enforcement%20in%20its%20authorization%20flow%20either.%0A%0A2.%20%2A%2ARecent%202FA%20enforcement%20improvements%2A%2A%20expanded%20enforcement%20from%20path-based%20allowlists%20to%20session-based%20triggers%20covering%20all%20sign-in%20methods%20%5B%5B6%5D%5D%28https%3A//github.com/better-auth/better-auth/pull/9122%29%2C%20but%20this%20enforcement%20happens%20during%20sign-in%2C%20not%20during%20OAuth%20authorization%20code%20issuance.%0A%0ATo%20fix%20this%20properly%2C%20the%20authorization%20endpoint%20would%20need%20to%20either%3A%0A%0A-%20Query%20the%20user%27s%20%60twoFactorEnabled%60%20field%20and%20verify%20MFA%20completion%20before%20issuing%20the%20code%0A-%20Or%20extend%20the%20session%20schema%20with%20a%20field%20like%20%60twoFactorVerifiedAt%60%20that%20can%20be%20checked%20during%20authorization%0A%0AYour%20workaround%20of%20checking%20for%20a%20TOTP-verified%20session%20at%20MCP%20request%20time%20is%20reasonable%20in%20the%20meantime.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=join-discord)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/9198)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#19940