Password Login with SMS code 2FA

Hi, we would like to add a SMS verification code to password login. IE:

  • user enters password
  • system sends them a code via SMS
  • user enters the code to finish logging in

We already have the capability to send SMS notifications in our application, so wondering if there is any way to integrate this into a password login flow?

I’ve looked at accounts-2fa, but feel requiring an authenticator app is too complicated for our user base.

In one of our projects, we were using a customized login and we developed a method to allow users to log in with more than one 2fa method. I will try to explain it briefly with examples below.

First of all, you can create a custom login handler using Accounts.registerLoginHandler.

Ex: /server/config/account.js

Accounts.registerLoginHandler("loginMethodName", function (loginRequest) {
  if (!loginRequest.loginMethodName) {
    return undefined
  }

  const obj = {
    identity: loginRequest.identity.toLowerCase(),
  }

  const user = Meteor.users.findOne(obj)

  if (!user) {
    throw new Meteor.Error("error", "error message")
  }

  const isPasswordOk = compareSync(loginRequest.password, user.password)

  if (!isPasswordOk) {
    throw new Meteor.Error("error", "error message")
  }

  const stampedLoginToken = Accounts._generateStampedLoginToken()
  const is2faVerifyMethodExists = Object.values(user.services?._2fa || {}).some((method) => method.isVerified)
  stampedLoginToken.isVerified = !is2faVerifyMethodExists

  if (!stampedLoginToken.isVerified) {
    stampedLoginToken._2fa = {}
  }

  Accounts._insertLoginToken(user._id, stampedLoginToken)

  return {
    userId: user._id,
    stampedLoginToken,
    options: {
      _2fa: {
        is2faVerifyMethodExists,
      },
    },
  }
})

You need to write a mix in for method and sub throughout the application, where you can check the 2fa validity by finding the token that the user has logged in on the connection.

Ex: mixin/UserLoginMixin

UserTypeMixin = function (methodOptions) {
  const requiredUserType = methodOptions.requiredUserType
  const runFunc = methodOptions.run

  methodOptions.run = function () {
    const user = Meteor.user()

    if (!user) {
      throw new Meteor.Error('unauthorized', "error message")
    }
    
    if (!user.services.loginMethodName.verificationToken.isVerified) {
      throw new Meteor.Error('not-verified', "error message")
    }

    if (this.connection) {
      const hashedToken = Accounts._getLoginToken(this.connection.id)
      const token = user.services.resume.loginTokens.find((token) => token.hashedToken === hashedToken)

      if (!token?.isVerified) {
        throw new Meteor.Error('_2fa-not-verified', "error message")
      }
    }

    return runFunc.call(this, ...arguments)
  }
  return methodOptions
}

The idea is that what you need here is a mixin.

1 Like

I think you can add some codes to make accounts-2fa works with SMS. I will write my idea here so I can do it later :smile:

  1. On client side, at the page you display the QR code, you can add a textbox allows user to enter their phone number.
  2. When user submit their phone number, you call a method on the server, let’s say 2fa.registerPhoneNumber. This method need to do 3 things:
    • Check if 2fa enabled, throw error. This method should work only 2fa NOT enabled.
    • Add/Update that number to services.twoFactorAuthentication. Let’s say services.twoFactorAuthentication.phoneNumber
    • Fetch user’s secret then call Accounts._generate2faToken(secret) to generate token and send this token to user’s phone number.
  3. When 2fa enabled and user login. We will need a button to trigger a method call to send SMS to registered phone number. Let’s say 2fa.smsToken. This method need to do these things:
    • Check if 2fa enabled for this user
    • Fetch user’s secret then call Accounts._generate2faToken(secret) to generate token and send this token to user’s registered phone number.
  4. If you want to make an option that let user change their phone number.
  • You can create a form which has 2 steps:
    • Step 1: A textbox to enter new phone number and submit button which calls a method, let’s say 2fa.changePhoneNumber
    • Step 2: A textbox to enter token and submit button which calls a method, let’s say 2fa.verifyPhoneNumber
  • 2fa.changePhoneNumber: This method will
    • Check if 2fa enabled for this user
    • Set services.twoFactorAuthentication.newPhoneNumber to submitted new number.
    • Generate and send a token to this number
  • 2fa.verifyPhoneNumber: This method will
    • Check if 2fa enabled for this user
    • Verify token
    • Set services.twoFactorAuthentication.phoneNumber to newPhoneNumber and newPhoneNumber to null or undefined.
4 Likes

The easiest route will be forking the current 2FA implementation to send the OTP through SMS instead of displaying the authenticator QR code. And maybe adjusting the OTP validity applicable for SMS.

2 Likes

Thanks for all the ideas!

Looks like I can copy the 2fa code to my app’s packages/ directory and modify it there:

https://guide.meteor.com/writing-atmosphere-packages#overriding-atmosphere-packages

accounts-password has some references to 2fa, but perhaps those references can stay the same.