[GH-ISSUE #9198] authorize endpoint doesn't verify MFA state when issuing auth codes #28632

Open
opened 2026-04-17 20:03:32 -05:00 by GiteaMirror · 3 comments
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-17 20:03:32 -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)
Author
Owner

@bytaesu commented on GitHub (Apr 17, 2026):

Image

Hi @mbochneak,

FYI the MCP plugin will be deprecated in favor of OAuth provider plugin. Note that the same gap exists there too (authorizeEndpoint also only checks session existence, not MFA state)


And this issue seems related to these:

@dvanmali @gustavovalverde Could you take a look?

<!-- gh-comment-id:4265579316 --> @bytaesu commented on GitHub (Apr 17, 2026): > <img width="500" alt="Image" src="https://github.com/user-attachments/assets/4aa87a31-9cfb-4b6f-b955-b93f32cb46aa" /> Hi @mbochneak, FYI the MCP plugin will be deprecated in favor of OAuth provider plugin. Note that the same gap exists there too (`authorizeEndpoint` also only checks session existence, not MFA state) --- And this issue seems related to these: - https://github.com/better-auth/better-auth/blob/main/packages/oauth-provider/src/token.ts#L142-L145 - #9205 @dvanmali @gustavovalverde Could you take a look?
Author
Owner

@dvanmali commented on GitHub (Apr 17, 2026):

The deprecation is correct, the MCP plugin is no longer used and support is supplied through the OAuth provider plugin. There is also a migration guide at the bottom of the docs if needed.

<!-- gh-comment-id:4265602465 --> @dvanmali commented on GitHub (Apr 17, 2026): The deprecation is correct, the MCP plugin is no longer used and support is supplied through the OAuth provider plugin. There is also a migration guide at the bottom of the docs if needed.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#28632