Two Factor / TOTP

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

Level: Easy
11
286
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 respec 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:

1. :totp_secret — Stores the TOTP secret for a user, if present this indicates that they have enabled TOTP. Note that this is sensitive info, so include redact: true to omit this field value from logs.

2. :totp_verified_at — Indicates that time at which TOTP was last verified. This is a virtual field (does not exist on your users table) because it is loaded dynamically from the users current session, as you will see later.

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 add a :select_merge call, which populates :totp_verified_at into the corresponding virtual field in our user struct.

Enabling & disabling in user settings

Here, we create a LiveView that allows 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 :confirming.

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.

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, which you'll see later. Here, we're applying these to every route that also requires an authenticated user.

However, you can just as easily 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.

You can even have routes that use varying expiration times for verification. This would require changing the provided context functions, and adding new plug functions and on_mount hooks. This allows for fine-grained control over when TOTP is required.

Authentication helper functions

UserAuth is a great place to add authentication helpers, which is what we do here for checking for TOTP verification. It's important to keep these functions simple, to help prevent holes from forming in your auth logic.