View Source NimbleTOTP (NimbleTOTP v1.0.0)

NimbleTOTP is a tiny library for Two-factor authentication (2FA) that allows developers to implement Time-Based One-Time Passwords (TOTP) for their applications.

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 validation code. This validation 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, e.g. 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 (TOTPs) based on a secret.

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()
#=> <<178, 117, 46, 7, 172, 202, 108, 127, 186, 180, ...>>

By default, a binary with 20 random bytes is generated per the HOTP RFC.

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\\" ...

Generating a Time-Based One-Time Password

After successfully reading the QR Code, the app will start generating a different 6 digit code every 30s. You can compute the verification code with:

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

The code can be validated using the valid?/3 function. Example:

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

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

After validating the code, you can finally persist the user's secret so you use it later whenever you need to authorize any critical action using 2FA, by using the same valid?/2 function.

Grace period

When you generate a verification code, the code will be valid between 0..period seconds, where the default period is 30s. This means that, in the worst case scenario, you may generate a verification code that will become invalid in the next second.

Depending on how you require users to generate codes, it might be beneficial to allow for a larger validity window for the codes. For example, that might be useful if you deliver codes through potentially-slow mediums (like SMS). In this case, consider a number of "previous codes" also valid. To do this, use the :time option in valid?/3 (see the function documentation for more examples).

Preventing codes from being reused

The TOTP RFC requires that a valid code can only be used once. This is a security feature that prevents codes from being reused. For example, a user could legitimately log in with a code, but in the validity window an attacker could gain access to the code and also log in.

To ensure codes are only considered valid if they have not been used, you need to keep track of the last time the user entered a valid TOTP code. For example, you can do that in a database column. Then, you can use the :since option in valid?/3:

NimbleTOTP.valid?(user.totp_secret, code, since: user.last_totp_at)

Assuming the code itself is valid for the given secret:

  • If :since is nil, the code will be considered valid.

  • If since is given, it will not allow codes in the same time period (30 seconds by default) to be reused. The user will have to wait for the next code to be generated.

Preventing enumeration attacks

If you only store the last time a user entered a valid TOTP code, you can prevent a valid code from being reused. However, this approach by itself doesn't generally prevent an attacker from attempting to "guess" a valid code by enumerating codes. TOTP codes are somewhat short, at only one million possible values. You'll likely have other rate-limiting mechanisms in place that would prevent an attacker from attempting codes in rapid sequence, but if you don't, you'll also want to limit the number of attempts in some way.

Summary

Functions

Generate the URI to be encoded in the QR code.

Generate a binary composed of random bytes.

Checks if the given otp code matches the given secret.

Generate Time-Based One-Time Password (TOTP).

Types

@type option() :: {:time, time()} | {:period, pos_integer()}

Options for verification_code/2 and valid?/3.

@type time() :: DateTime.t() | NaiveDateTime.t() | integer()

Unix time in seconds, DateTime.t/0 or NaiveDateTime.t/0.

@type validate_option() :: {:since, time() | nil}

Options for valid?/3.

Functions

Link to this function

otpauth_uri(label, secret, uri_params \\ [])

View Source
@spec otpauth_uri(String.t(), String.t(), keyword()) :: String.t()

Generate the URI to be encoded in the QR code.

Examples

iex> NimbleTOTP.otpauth_uri("Acme:alice", "abcd", issuer: "Acme")
"otpauth://totp/Acme:alice?secret=MFRGGZA&issuer=Acme"
@spec secret(non_neg_integer()) :: binary()

Generate a binary composed of random bytes.

The number of bytes is defined by the size argument. Default is 20 per the HOTP RFC.

Examples

NimbleTOTP.secret()
#=> <<178, 117, 46, 7, 172, 202, 108, 127, 186, 180, ...>>
Link to this function

valid?(secret, otp, opts \\ [])

View Source
@spec valid?(binary(), String.t(), [option() | validate_option()]) :: boolean()

Checks if the given otp code matches the given secret.

Options

  • :time - The time (either NaiveDateTime.t/0, DateTime.t/0, or Unix format in seconds) to be used. Default is System.os_time(:second).

  • :since - The last time the secret was used, see "Preventing TOTP code reuse" next. Same possible types as the :time option.

  • :period - The period (in seconds) in which the code is valid. Default is 30. If this option is given to verification_code/2, it must also be given to valid?/3.

Preventing TOTP code reuse

The :since option can be used to prevent TOTP codes from being reused. When set to the time when the last code was entered, the code generated from within that period is no longer considered valid. Periods are counted from the Unix epoch. This means a user may have to wait, in the worst case scenario, for the duration of :period before they can enter a valid code again. This implementation meets the TOTP RFC requirements.

Grace period

In some cases it is preferable to allow the user more time to validate the code. Generated codes are valid between 0..period seconds, which means in the worst case scenario a generated code may be about to expire. You can increase this interval, the so-called grace period, by using the :time option:

def valid_code?(secret, otp) do
  time = System.os_time(:second)

  NimbleTOTP.valid?(secret, otp, time: time) or
    NimbleTOTP.valid?(secret, otp, time: time - 30)
end

In this example by validating first against the current time, but also against 30 seconds ago, we allow the previous code, to be still valid.

Link to this function

verification_code(secret, opts \\ [])

View Source
@spec verification_code(binary(), [option()]) :: String.t()

Generate Time-Based One-Time Password (TOTP).

Options

  • :time - The time (either NaiveDateTime.t/0, DateTime.t/0, or Unix format in seconds) to be used. Default is System.os_time(:second).
  • :period - The period (in seconds) in which the code is valid. Default is 30. If this option is given to verification_code/2, it must also be given to valid?/3.

Examples

secret = Base.decode32!("PTEPUGZ7DUWTBGMW4WLKB6U63MGKKMCA")
NimbleTOTP.verification_code(secret)
#=> "569777"