Two-factor authentication is now essential for a lot of web sites, as it ensures that - even if someone gets your access credentials - he/she cannot login, because a token from your mobile phone is required. Therefor, the hacker should also have access to your phone and to the code to unlock it, which is highly unlikely.

How does 2FA work?

Two-factor authentication (TFA) is just a generic concept, which means you need to provide two sets of security credentials (factors). These are usually username, password and a code (usually 6-digits long). This one may be sent you via text (SMS) by the website, or generated by an app on your phone.

The second system is now widely used. Putting it simple: based on a secret key provided at activation, the app generates a different code every 30 seconds, which you provide at login and the service will check if it matches.

Many people use Google Authenticator, which is the most famous app for token generation. However, most folks do not know that, even though the web site or service might ask you to install Google Authenticator, it really doesn’t matter what app you use: it just has to support the Time-based one-time password (TOTP) algorithm. I, for instance, use Microsoft Authenticator, which is still free of charge but provides better security and some backup options.

2FA in Perl

There are a couple of Perl modules to perform 2FA: Auth::GoogleAuth and Authen::TOTP. I chose the first one because it had more recent updates, however the name of the latter seems more appropriate as this kind of authentication system is not tied to the Google app, or any other one.

First off, you need to create a ID and secret for the user when he activated the 2FA.

my $gauth = Auth::GoogleAuth->new({
    issuer => 'My Web Site Name',  # No high chars, stay in ASCII set
    key_id => $username,
});

The issuer is a text string which identifies the service, so that the user can actually choose the correct code (also know as OTP, one-time password) among the ones generated by his authentication app. I saw that this field must only be ASCII chars, but I don’t know if it’s by definition or the problem lies in Google’s QR generation service or in my authentication app or whatever.

The key_id is something related to the user itself, such as the username or the e-mail address. This value must be provided also when veryfing the secret, so if it changes the secret key will no longer work and a new one will have to be generated.

Now let’s compute a secret unique to the user. This is something random, which Auth::GoogleAuth can generate for you (or you can pass it, see the module’s documentation for details).

my $secret32 = $gauth->generate_secret32;

# Store the secret somewhere user-related, i.e. in it's profile
$db->query(
    'update users set secret32 = ? where username = ?',
    $secret32, $username
);

Now we can create an HTML which shows the QR and other information, which should appear as this (sorry, it’s in Italian language 😊):

Example web page to activate 2FA authentication

The relevant part of the code is something like this:

<div>
    <% my $qrurl = $gauth->qr_code =~ s/200x200/500x500/xsr; %>
    <img src="<%= $qrurl %>" class="img-fluid">
    <br>
    <b><%= $gauth->secret32 %></b>
</div>

This CPAN module supports generating QR Codes using Google’s service, but it always passes 200x200 as dimension, so in the above code I tweaked the URL a bit. You can also use modules such as Imager::QRCode to generate the QR code by yourself. I may submit a patch in the near future to at least support passing the dimensions to qr_code().

Now the user can scan the QR with his app (or enter the code manually) and have it appear in his app. He will then have to enter one code/OTP in order to go through verification and enable the whole this: this process should be well known to most Internet users.

The verification process, usually when logging in, is something like this:

my $gauth = Auth::GoogleAuth->new({
    # No high chars, stay in ASCII set
    issuer => 'My Web Site Name', 
    key_id => $username
});

# Retrieve the secret
my $secret32 = $db->query(
    'select secret32 from where username = ?',
    $iduser
)->hash->{secret32};
$gauth->secret32($secret32);

my $isvalid = $gauth->verify(
    $q->param('otp'),     # code
    1,                    # range
);

The range can be a value of 0 or greater. This tells the verification method how many “nearby” codes (OTPs) to accept as valid besides the exact one of this moment. Since device clocks are not always perfectly synchronized, it’s possible that the client has one code when the server already has another one: or that the user loses some time in typing it in and it expires. A value of 1 is ideal, as it accepts also the code generated before and the one generated after the current time of the machine which checks it.

And that’s it!

The module has a series of other interesting options which I encourage you to check.