We have already talked about authentication in an
earlier article about
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.
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
iex> 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.
iex> uri = NimbleTOTP.otpauth_uri("Acme:alice", secret, issuer: "Acme") "otpauth://totp/Acme:alice?secret=MFRGGZA&issuer=Acme" iex> 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 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
Verification code using Google Authenticator
You can compute the current verification code with:
iex> NimbleTOTP.verification_code(secret) "859020"
Or validate it using the
iex> NimbleTOTP.valid?(secret, "859020") true iex> 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?/3instead. The latter uses a secure string comparison algorithm to prevent timing attacks.
For Bytepack, we enforce 2FA right after login:
Requesting the verification code
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.