Introducing NimbleTOTP

We have already talked about authentication in an earlier article about mix phx.gen.auth. This short post follows up on the topic by describing the general idea behind Two-factor Authentication and how to use our recently released NimbleTOTP library to generate and validate Time-based One Time Passwords (TOTP).

Two-factor authentication (2FA)

The concept of 2FA is quite simple. It’s an extra layer of security that demands a user to provide two pieces of evidence (factors) to the authentication system before access can be granted.

One way to implement 2FA is to generate a random secret for the user and whenever the system needs to perform a critical action it will ask the user to enter a verification code. This verification code is a Time-Based One-Time Password (TOTP) based on the user’s secret and can be provided by an authentication app like Google Authenticator or Authy, which should be previously installed and configured on a compatible device such as a smartphone.

Note: A critical action can mean different things depending on the application. For instance, while in a banking system the login itself is already considered a critical action, in other systems a user may be allowed to log in using just the password and only when trying to update critical data (e.g. its profile) 2FA will be required.

Using NimbleTOTP

In order to allow developers to implement 2FA, NimbleTOTP provides functions to:

  • Generate secrets composed of random bytes.
  • Generate URIs to be encoded in a QR Code.
  • Generate Time-Based One-Time Passwords based on a secret.
  • Validate generated passwords using secure string comparison.

Generating the secret

The first step to set up 2FA for a user is to generate (and later persist) its random secret. You can achieve that using NimbleTOTP.secret/1.

Example:

secret = NimbleTOTP.secret()
#=> <<63, 24, 42, 30, 95, 116, 80, 121, 106, 102>>

By default, a binary with 10 random bytes is generated. This is the secret you would store in the database once the user validates it.

Generating URIs for QR Code

Before persisting the secret, you need to make sure the user has already configured the authentication app in a compatible device. The most common way to do that is to generate a QR Code that can be read by the app.

You can use NimbleTOTP.otpauth_uri/3 along with eqrcode to generate the QR code as SVG.

Example:

uri = NimbleTOTP.otpauth_uri("Acme:alice", secret, issuer: "Acme")
#=> "otpauth://totp/Acme:alice?secret=MFRGGZA&issuer=Acme"
uri |> EQRCode.encode() |> EQRCode.svg()
#=> "<?xml version=\\"1.0\\" standalone=\\"yes\\"?>\\n<svg version=\\"1.1\\" ...

You can also wrap the code that generates the SVG into a function so you can use it in any view/component. Something like:

def generate_qrcode(uri) do
  uri
  |> EQRCode.encode()
  |> EQRCode.svg(width: 264)
  |> Phoenix.HTML.raw()
end

The resulting SVG can then be injected directly into your Phoenix template using:

<%= generate_qrcode(uri) %>

Here’s how it looks on Bytepack’s website:

The generated QR Code
The generated QR Code on Bytepack's website

Generating/validating a Time-Based One-Time Password

After successfully scanning the QR Code, your device will generate a different 6 digit code every 30s.

The generated verification code
Verification code using Google Authenticator

You can compute the current verification code with:

NimbleTOTP.verification_code(secret)
#=> "859020"

Or validate it using the valid?/3 function:

NimbleTOTP.valid?(secret, "859020")
#=> true

NimbleTOTP.valid?(secret, "012345")
#=> false

After validating the code, you can finally persist the user’s secret in the database. Whenever you need to authorize a critical action, you will request an up-to-date verification code from the user and use the same NimbleTOTP.valid?/2 function to validate the code against the secret stored in the DB.

Note: Although you could validate the password directly against NimbleTOTP.verification_code(secret) using the standard == operator, we strongly recommend to always use NimbleTOTP.valid?/2 instead. The latter uses a secure string comparison algorithm to prevent timing attacks.

For Bytepack, we enforce 2FA right after login:

Requesting the verification code
Requesting the verification code

Wrapping up

NimbleTOTP allows developers to easily add 2FA using Time-Based One-Time Password (TOTP) to their applications. TOTP is just one of many methods to provide 2FA, albeit the simplest one. The API is minimal and provides a complete solution for most of the cases you might need. We hope you enjoy it.

Happy coding!