[GH-ISSUE #3561] twoFactor.sendOtp() not working after 1.3.0 #26970

Closed
opened 2026-04-17 17:43:57 -05:00 by GiteaMirror · 12 comments
Owner

Originally created by @dennisjnnh on GitHub (Jul 23, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/3561

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

#3302 I think this PR introduced an issue if a user wanted to enable 2FA and call twoFactor.sendOtp() afterwards, which would receive the following error: OTP_NOT_ENABLED.

Client Code:
` const { data, error } = await twoFactor.enable({
password: values.password,
})

  if (error) {
    if (error.code === 'INVALID_PASSWORD') {
      toast.error('Wrong Password')
      return
    }
    toast.error(
      `There was an error while enabling 2FA. Please try again later`,
    )
    return
  }

  if (values.twoFactorType === 'otp') {
    const { data, error } = await twoFactor.sendOtp()

    if (error) {
      toast.error(
        'There was an error sending the OTP password. Please try again later',
      )
      return
    }

    if (data.status) {
      setShowOTP(true)
    }
  }`

server setup:
` twoFactor({
issuer: process.env.BETTER_AUTH_NAME!,
otpOptions: {
async sendOTP({ otp, user }) {
const log = createLog('betterAuth')

    log.debug(`Sending 2FA token email to user ${user.id}`)
    await sendTwoFactorTokenEmail(user, otp)
  },
},

}),`

Current vs. Expected behavior

On version 1.2.12 this works and the user can verify 2FA using OTP, but in 1.3.0 the api call twoFactor.sendOtp() fails.

What version of Better Auth are you using?

1.3.0

Provide environment information

macOS, Safari

Which area(s) are affected? (Select all that apply)

Backend

Auth config (if applicable)


Additional context

Probably related to #3302

Originally created by @dennisjnnh on GitHub (Jul 23, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3561 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce #3302 I think this PR introduced an issue if a user wanted to enable 2FA and call twoFactor.sendOtp() afterwards, which would receive the following error: OTP_NOT_ENABLED. Client Code: ` const { data, error } = await twoFactor.enable({ password: values.password, }) if (error) { if (error.code === 'INVALID_PASSWORD') { toast.error('Wrong Password') return } toast.error( `There was an error while enabling 2FA. Please try again later`, ) return } if (values.twoFactorType === 'otp') { const { data, error } = await twoFactor.sendOtp() if (error) { toast.error( 'There was an error sending the OTP password. Please try again later', ) return } if (data.status) { setShowOTP(true) } }` server setup: ` twoFactor({ issuer: process.env.BETTER_AUTH_NAME!, otpOptions: { async sendOTP({ otp, user }) { const log = createLog('betterAuth') log.debug(`Sending 2FA token email to user ${user.id}`) await sendTwoFactorTokenEmail(user, otp) }, }, }),` ### Current vs. Expected behavior On version 1.2.12 this works and the user can verify 2FA using OTP, but in 1.3.0 the api call twoFactor.sendOtp() fails. ### What version of Better Auth are you using? 1.3.0 ### Provide environment information ```bash macOS, Safari ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript ``` ### Additional context Probably related to #3302
GiteaMirror added the lockedbug labels 2026-04-17 17:43:57 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Jul 23, 2025):

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

In Better Auth v1.3.0, the flow for enabling 2FA changed: after calling twoFactor.enable(), the user is not immediately considered to have 2FA enabled—verification (via TOTP or OTP) is now required before sendOtp will work. If you call sendOtp right after enable, you'll get OTP_NOT_ENABLED because user.twoFactorEnabled is still false until verification completes. This is a deliberate change for security and is reflected in the code and tests (see implementation, see tests).

If you want to restore the old behavior for backwards compatibility or testing, you can set the skipVerificationOnEnable option in your twoFactor config. Otherwise, update your client flow to include a verification step after enable, before calling sendOtp. The 1.3.0 release notes mention related changes, but there’s no direct evidence that PR #3302 specifically caused this—it’s just the new contract starting with 1.3.0.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Join Discord Share on X

<!-- gh-comment-id:3107558763 --> @dosubot[bot] commented on GitHub (Jul 23, 2025): <!-- Greeting --> Hi @dennisjnnh! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> In Better Auth v1.3.0, the flow for enabling 2FA changed: after calling twoFactor.enable(), the user is not immediately considered to have 2FA enabled—verification (via TOTP or OTP) is now required before sendOtp will work. If you call sendOtp right after enable, you'll get OTP_NOT_ENABLED because user.twoFactorEnabled is still false until verification completes. This is a deliberate change for security and is reflected in the code and tests ([see implementation](https://github.com/better-auth/better-auth/blob/602ab7c4c6913d601994de0ae679be6645a58d69/packages/better-auth/src/plugins/two-factor/index.ts), [see tests](https://github.com/better-auth/better-auth/blob/602ab7c4c6913d601994de0ae679be6645a58d69/packages/better-auth/src/plugins/two-factor/two-factor.test.ts)). If you want to restore the old behavior for backwards compatibility or testing, you can set the skipVerificationOnEnable option in your twoFactor config. Otherwise, update your client flow to include a verification step after enable, before calling sendOtp. The [1.3.0 release notes](https://github.com/better-auth/better-auth/blob/602ab7c4c6913d601994de0ae679be6645a58d69/docs/content/blogs/1-3.mdx) mention related changes, but there’s no direct evidence that PR #3302 specifically caused this—it’s just the new contract starting with 1.3.0. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/72368a89-4eed-403c-a6c7-f2a7ca2a6d70?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/72368a89-4eed-403c-a6c7-f2a7ca2a6d70?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/72368a89-4eed-403c-a6c7-f2a7ca2a6d70?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/72368a89-4eed-403c-a6c7-f2a7ca2a6d70?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/72368a89-4eed-403c-a6c7-f2a7ca2a6d70?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/72368a89-4eed-403c-a6c7-f2a7ca2a6d70?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/72368a89-4eed-403c-a6c7-f2a7ca2a6d70?feedback_type=other)</sup>&nbsp;&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&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/3561)
Author
Owner

@dennisjnnh commented on GitHub (Jul 23, 2025):

@dosu How would you want to complete the verification with OTP if sendOtp will not work?

<!-- gh-comment-id:3107593973 --> @dennisjnnh commented on GitHub (Jul 23, 2025): @dosu How would you want to complete the verification with OTP if sendOtp will not work?
Author
Owner

@dosubot[bot] commented on GitHub (Jul 23, 2025):

In v1.3.0, the OTP for verification is not sent via sendOtp after enable. Instead, the OTP should be delivered as part of the verification step itself—when you call the verifyOtp endpoint, it both sends the OTP (if needed) and verifies the code. The user is only considered to have 2FA enabled after successful verification. So, after enable, your client should prompt for verification and trigger verifyOtp, which will handle OTP delivery and verification in one step. The sendOtp method is only available after 2FA is fully enabled, not during enrollment. If you want the old behavior, you can set skipVerificationOnEnable to true in your config, but that's not the default or recommended flow now.
See implementation details

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Join Discord Share on X

<!-- gh-comment-id:3107614628 --> @dosubot[bot] commented on GitHub (Jul 23, 2025): <!-- Answer --> In v1.3.0, the OTP for verification is not sent via sendOtp after enable. Instead, the OTP should be delivered as part of the verification step itself—when you call the verifyOtp endpoint, it both sends the OTP (if needed) and verifies the code. The user is only considered to have 2FA enabled after successful verification. So, after enable, your client should prompt for verification and trigger verifyOtp, which will handle OTP delivery and verification in one step. The sendOtp method is only available after 2FA is fully enabled, not during enrollment. If you want the old behavior, you can set skipVerificationOnEnable to true in your config, but that's not the default or recommended flow now. [See implementation details](https://github.com/better-auth/better-auth/blob/602ab7c4c6913d601994de0ae679be6645a58d69/packages/better-auth/src/plugins/two-factor/otp/index.ts) <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/3c45e323-52b2-4690-8868-581987abd3f7?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/3c45e323-52b2-4690-8868-581987abd3f7?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/3c45e323-52b2-4690-8868-581987abd3f7?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/3c45e323-52b2-4690-8868-581987abd3f7?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/3c45e323-52b2-4690-8868-581987abd3f7?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/3c45e323-52b2-4690-8868-581987abd3f7?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/3c45e323-52b2-4690-8868-581987abd3f7?feedback_type=other)</sup>&nbsp;&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&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/3561)
Author
Owner

@samstoppani-triffin commented on GitHub (Aug 5, 2025):

@dosu how does verifyOtp both send and verify the code? verifyOtp requires a code.

Please show us an example of the expected flow for enabling 2FA.

<!-- gh-comment-id:3155511193 --> @samstoppani-triffin commented on GitHub (Aug 5, 2025): @dosu how does verifyOtp both send and verify the code? verifyOtp requires a code. Please show us an example of the expected flow for enabling 2FA.
Author
Owner

@dennisjnnh commented on GitHub (Aug 18, 2025):

@bairdj Do you mind taking a look at this? It may be related to your PR #3302.

<!-- gh-comment-id:3196972894 --> @dennisjnnh commented on GitHub (Aug 18, 2025): @bairdj Do you mind taking a look at this? It may be related to your PR #3302.
Author
Owner

@bairdj commented on GitHub (Aug 18, 2025):

This occurs if twoFactorEnabled if false in the user model 9a6a8d741b/packages/better-auth/src/plugins/two-factor/otp/index.ts (L174)

Presumably this is occurring when skipVerificationOnEnable is false, and it's failing on attempting to send it later. Fix is probably to remove the enabled check in sendOtp and just send it anyway, then it will update the user's twoFactorEnabled when they verify the code later

<!-- gh-comment-id:3197369511 --> @bairdj commented on GitHub (Aug 18, 2025): This occurs if `twoFactorEnabled` if `false` in the user model https://github.com/better-auth/better-auth/blob/9a6a8d741bf77c0d2e44c02281314b50a75cd6fc/packages/better-auth/src/plugins/two-factor/otp/index.ts#L174 Presumably this is occurring when `skipVerificationOnEnable` is false, and it's failing on attempting to send it later. Fix is probably to remove the enabled check in `sendOtp` and just send it anyway, then it will update the user's `twoFactorEnabled` when they verify the code later
Author
Owner

@frectonz commented on GitHub (Sep 4, 2025):

@dennisjnnh are you still facing this problem and if you are do you have a repo i can use to reproduce the issue you are facing.

<!-- gh-comment-id:3253878049 --> @frectonz commented on GitHub (Sep 4, 2025): @dennisjnnh are you still facing this problem and if you are do you have a repo i can use to reproduce the issue you are facing.
Author
Owner

@frectonz commented on GitHub (Sep 4, 2025):

From what i am seeing, you just have to set skipVerificationOnEnable to true in your two-factor plugin options.

<!-- gh-comment-id:3253896891 --> @frectonz commented on GitHub (Sep 4, 2025): From what i am seeing, you just have to set `skipVerificationOnEnable` to `true` in your `two-factor` plugin options.
Author
Owner

@dennisjnnh commented on GitHub (Sep 7, 2025):

From what i am seeing, you just have to set skipVerificationOnEnable to true in your two-factor plugin options.

While this does resolve the issue, i don't think it is a perfect solution for the problem.

If a user enables 2FA with TOTP with this setting enabled, and fails to set up the device correctly, he will not be able to recover from that point on, because 2FA was enabled right away without verifying if the setup worked correctly.

<!-- gh-comment-id:3264028599 --> @dennisjnnh commented on GitHub (Sep 7, 2025): > From what i am seeing, you just have to set `skipVerificationOnEnable` to `true` in your `two-factor` plugin options. While this does resolve the issue, i don't think it is a perfect solution for the problem. If a user enables 2FA with TOTP with this setting enabled, and fails to set up the device correctly, he will not be able to recover from that point on, because 2FA was enabled right away without verifying if the setup worked correctly.
Author
Owner

@gleandroj commented on GitHub (Oct 10, 2025):

+1

<!-- gh-comment-id:3391655673 --> @gleandroj commented on GitHub (Oct 10, 2025): +1
Author
Owner

@mateus4k commented on GitHub (Oct 10, 2025):

+1

<!-- gh-comment-id:3392307064 --> @mateus4k commented on GitHub (Oct 10, 2025): +1
Author
Owner

@himself65 commented on GitHub (Dec 2, 2025):

This is fixed in the latest version

<!-- gh-comment-id:3603828118 --> @himself65 commented on GitHub (Dec 2, 2025): This is fixed in the latest version
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#26970