Two Factor / TOTP

Safeguard accounts with a two-factor implementation that supports Authenticator apps. Comes installed with industry best practices to deter bad actors.

v1.8.3 v1.7.12
Level: Easy
10
296
3

Install deps

Install the following dependencies to get started. You can find their documentation on HexDocs.

Configure TOTP

The "issuer" we configure here will be what shows up in user authenticator apps. Without it, they would not be able to tell the user which code is for which app.

Set up accounts context

There are a few capabilities that we need to add to the Accounts context (or Users context, depending on your project).

First, we must generate a new TOTP secret for a user that wants to set up 2FA. This secret is generated by NimbleTOTP , we need it to generate and verify the familiar six-digit codes for users. This is done by totp_secret() , and will be stored in the User struct later on.

We also need to know what state a user is in, regarding their TOTP status. We'll do this with two functions: totp_enabled?(user) and totp_verified?(user, opts) . A user has TOTP "enabled" if we've assigned a secret to them. Posessing a secret implies that they have enabled and verified it, as we otherwise would not save the secret to their record.

Checking if a user is currently "verified" with respect to TOTP involves a few checks. We check if they have TOTP enabled at all, and simply return true if they do not. If TOTP is enabled, we check if their current session has ever verified successfully. Finally, if they have verified at some time in the past, we can check to see if that date is within some duration. Here, we use a 24 hour period before verification is no longer valid.

So, how do we actually go about enabling TOTP? To do that we need to build an auth URI, and then encode that as a data URI that will render a QR code. This is what TOTP apps scan in order to obtain your secret, and generate codes. You can see how this works in totp_data_uri(user, totp_secret) .

Next, we'll need some way to verify that a TOTP is correct, prior to enabling it for a user. We'll use NimbleTOTP.valid?/3 to check validity in our verify_session_totp(token, totp_secret, verification_code) function. This function accepts parameters that allow us to check that a code is valid for the given secret, and was generated no more than 30 seconds ago. This time restriction is important, as we don't want to accept old codes. The function also updates the current session with the time of verification. Note that this value is stored on the session and not the user record. This means that new sessions will be required to go through TOTP verification.

Now, to enable TOTP officially for a user, we just have to use our verification function, and set the generated secret on our user if it succeeds.

If we ever want to remove TOTP for a user, we just have to delete the stored secret. You can see that this is exactly what's done in disable_user_totp(user) .

Add new fields to User schema

We have two fields to add to our User schema:

Add new fields to UserToken schema

Here we need to add :totp_verified_at to our token schema. As mentioned, this will indicate when TOTP was last verified for the current session. In the query that fetches our user by session token, we update the :select call to populate :totp_verified_at into the corresponding virtual field in our user struct.

We also make by_token_and_context_query/2 public so it can be called from the Accounts context when updating TOTP verification status.

Enabling & disabling in user settings

Here, we update the Settings LiveView to allow a user to enable, confirm, and disable TOTP.

We start with an "Enable" button if the user does not have TOTP enabled. When the button is clicked, generate a TOTP secret and change the page state to :verifying .

Now, the user will have to scan the QR displayed with their TOTP app, and submit a valid code to move to the next step.

Once a code has been verified and the secret saved to the users account, they may opt to disable TOTP, deleting the secret from their account.

Note that we also grab the user_token from the session in mount, as we need it to update the TOTP verification status.

Verifying TOTP codes

To verify TOTP for a users session, we accept a code from them and verify it on the backend. Because this updates the :totp_verified_at field on their session, we can redirect them to where they wanted to go and they should now be allowed.

Authentication in your router

In your router, we'll use new plugs and on_mount hooks to authenticate against TOTP status. We create a new live_session with the :require_verified_totp on_mount hook for routes that require TOTP verification.

The TOTP verification page itself only requires authentication (not TOTP verification), so it lives in the :require_authenticated_user live_session.

You can mix and match these so that some routes only require password authentication, while other, more secure, routes require TOTP. GitHub does this for accessing your settings.

Authentication helper functions

UserAuth is a great place to add authentication helpers, which is what we do here for checking for TOTP verification. We add both a plug function require_verified_totp/2 and an on_mount/4 clause for :require_verified_totp .

It's important to keep these functions simple, to help prevent holes from forming in your auth logic.

Database migration

By this point, you're familiar with these two new fields we need to add to our database. It's nice when a powerful feature such as this one requires no new tables, and only a couple new fields.