Requiring TOTP on Sign-Up in AWS Cognito

Chander Ramesh
4 min readJun 14, 2020

Most services won’t mandate TOTP as the primary 2FA mechanism, but I was recently creating a security-focused application, and given how insecure SMS is as a second-factor mechanism, I felt compelled to make TOTP the only mechanism and to mandate 2FA for all logins.

I briefly considered Auth0, but it turns out one-time passwords aren’t included in their free tier. Fair enough, everyone needs to make a living. Let’s check out their pricing plans to see if it’s reasonable.

Note that it’s not Developer — they strictly mandate the Pro version, at just north of $1,000 each month.

The pricing speaks for itself, so let’s move on.

I’ve been gradually immigrating to AWS (from GCP) for my various hobby projects, and it seemed a perfect opportunity to wade into the Cognito waters. While the UI is not quite as sleek as the Firebase Auth options, it has a far larger and more configurable set of options, and they support all the same SDKs.

And sure enough, the options I needed were present.

I’m easily able to require MFA for all logins.

And I’m easily able to select TOTP as the only mechanism I support. It seems simple enough at first glance, but it was only an hour later during the testing of my signup flow that I saw an oddity:

setupTOTP requires the user to already be logged in!

Naturally, we fail to setupTOTP :

In my mind, I was envisioning the following flow:

  1. The user clicks “Sign Up”.
  2. The user enters their email address and provides a password.
  3. Upon pressing enter, the user is presented with a QR code.
  4. The user adds this so their TOTP app of choice.
  5. The user verifies the code.
  6. User is now, truly, signed up.

Try as I might, it seemed most people added TOTP as an optional requirement, so Stack Overflow and Google searches were fairly fruitless. Apparently, I was one of the first to try mandating it during the signup process. (If I’m not and I missed an amazing resource documenting this very thing, please let me know! Because what follows is an ugly workaround that I would love to avoid.)

Due to this API design, I was forced to maintain more state about the user. So instead, the login flow looks like the following:

  1. The user clicks “Sign Up”.
  2. The user enters their email address and provides a password.
  3. [Optional] Upon pressing enter, the user is sent a one-time code to verify their email.
  4. [Optional] The user then inputs this code from their email, and if the code is valid, we present a “successful sign up” screen.
  5. The user now navigates to the “Sign In”/“Login” option and logs in. The user is signed in — but in a limited, not truly active, state.
  6. To force the user to then choose a TOTP, we then gate all further API calls the user can make (and functionality on the frontend) contingent upon them setting TOTP as their MFA. We do this by constantly Auth.getPreferredMFA(user) and verifying that it’s TOTP.
  7. Once the user chooses to select TOTP, we present them with the QR code. Remember to transform the response string from setupTOTP to a format that Google Authenticator (and others) will accept.
  8. The user validates the code, and after calling verifyTotpToken with the user’s input, and after success — most importantly — we set their preferred MFA option to TOTP, never allowing them to change this.

Note that steps 3 and 4 are optional — but if you choose to omit them, you’ll need to confirm the user in some other way, either through SMS or Lambda trigger, as unconfirmed users cannot sign in.

--

--