11Jun

There’s probably no better time to integrate two-factor authentication into your a than today.

Two-factor authentication (often abbreviated TFA or 2FA) is a method of authenticating clients that involves ‘two factors’ when verifying a user – a password and something the user can physically access – like a fingerprint or a random SMS code (or even better, a one-time password!).

Single-factor authentication refers to the kind of login that only requires a username (or email) and password. This is the traditional method of logging in that you’re probably used to.

Before we get lost in a whole mess of new words, let’s break down some of the important vocabulary that will make it much easier to discuss upcoming concepts.

One Time Password

A one-time password is a kind of token (a code or word) that can only be used once. Regardless of how it’s implemented, such a password should only ever be valid for a single use case, then gets disregarded. It can never be used twice.

You may already have spotted the giant bottleneck in implementing a robust OTP system. How can we ensure we only ever generate a unique token?

The answer lies in the HMAC-based One-Time Password algorithm.

Hmac-based One-Time Password algorithm (HOTP)

The HOTP algorithm depends on two pieces of information in order to produce a token we can reliably use – a moving factor and a secret key.

The secret key is probably something you’ve encountered in one form or another on some corner of the internet. It’s a token that’s shared between a server and client so we can be sure the client has somehow been granted access. Since the server is responsible for generating the tokens, the first problem we have is sharing the secret key with the client.

The second problem we have is generating a random string on the server as our counter moves upwards. This counter is the mysterious-sounding ‘moving-factor’.

In pseudocode:

when counter = 1
     secret = xyz
     token = xyxyxyxyxyxy

when counter = 2
     secret - xyz
     token = xxyyxxyyxxyy

and so on...

The ‘secret’ is a HmacSHA1 hash (published as RFC4226 by the Internet Engineering Task Force) of the actual secret string passed to the algorithm. The resulting output is about 20 bytes long. The second step is making the output short enough to be easily read by human eyes. This truncation is handled by the TOTP algorithm and can produce any string of our desired length, eg, “556 221″

Great! Now we have a nice readable token that can be given to the user. The only problem that remains is the counter. Take a second to consider it before you throw in a simple auto-incrementing counter and call it a day

The counter has to be implemented such that it never repeats itself or the same secret token will be generated twice at some point. If this happens, it’s a huge security vulnerability. To keep it short, implementing your own counter is a pain and you shouldn’t do it unless you’re sure you know what you’re doing.

The solution to a bulletproof counter lies in literally using time’ as our counter. This then gives birth to the fabled time-based one-time password (TOTP)

Time-based One Time Password (TOTP)

Now we know the specifics of how a OTP comes about, let’s break it down even further to make sure they’re no blind spots in our implementation. If you’re interested in the technical bits, The “Time-Based One-Time Password” spec was published as RFC6238 by the IETF.
The HOTP and TOTP algorithm only differ in the fact that the latter uses time as a counter. Since both server and client have access to time, there’s no need to create and manually keep track of a counter. The epoch time (when the counter begins, can be specified as a unix timestamp if time zones are going to be a problem).

Time step

Unix time is defined in seconds (which means our counter changes every second), which isn’t ideal. A better idea would be to generate the token after a significant time interval. We will call this time interval the time step.

In order to create the required interval, we’ll redefine our counter as

counter = currentUnixTime/30

A larger time step means that your app (and your users) will have a longer time to validate a new token before the old one is invalidated. However, this also means a larger attack opportunity for attackers – the key is balance.

Delay window

Another important concept that needs to be introduced is the delay window. What happens if the client sends a TOTP that’s close to expiry, but due to a latency issue, it arrive at the server after expiry? Do we reject such a token and require the user to generate a new one? We could, but a better idea is to create a delay window.

A delay window makes the TOTP validation function not only check tokens that are valid for the current step but also for the last S steps. Since the counter we are using is predictable, we can also predict what tokens will be generated in the future. The delay window can thus verify tokens from both the past and the future.

Delay widow and time step are both important concepts to have at your fingertips. Since our client is going to be Google Authenticator or a similar 2FA app, we don’t have to worry about them. These are built-in. (GA’s time-step is 30 seconds).

Sharing the secret with the user

Finally, we need a way to share the secret generated on the server with our client. This should ideally be a random (secure) string. Since we want it to be very long, having the user input it manually would be a pain. Instead, we’ll leverage QR codes!

Implementing Two Factor Authentication with NodeJS

Now that all the theory is done, you should have a solid understanding of one of the most important algorithms on the web. We should get down to implementing it with express and NodeJS. We’re going to use a fairly old and well-supported library – otplib.

For the sake of keeping this tutorial short and to the point, we won’t implement actual login logic for this app. That would mean setting up a database in addition to a server, and that’s outside the scope of this article.

Here’s how the app is going to work

Setting up

Step 1: A user enables 2FA

It’s important to note that the user has to explicitly enable two-factor authentication for it to work. Recall that the secret has to be shared between the server and client.

When the user enables 2FA, we generate a unique secret key for them:

import {authenticator} from ‘otplib’
const secret = authenticator.generate() // only available for google authenticator or similar
                                        // use crypto.randomBytes otherwise

The secret should be stored in the database together with the user that owns it.

Step 2: Share the secret with the user

Note: Google Authenticator ignores the ‘algorithm’, ‘digits’, and ‘step’ options supported by otplib. Cross-check with your authenticator app in case of errors.

The second step is finding a way we can give the client access to the secret generated by the server. Since most modern phones have cameras, a QR code is a convenient way of encoding our (inconveniently long) string so that the user doesn’t have to type it out.

To do this, we’ll use the qrcode module.

  {
   //...
   generateQRCode: (user, secret) => {

        const otp = authenticator.keyuri(user, "Our TFA App", secret);
        let imagePath = '';

        qrcode.toDataURL(otp, (err, imageUrl) => {
            if (err) {
                console.log('Could not generate QR code', err);
                return;
            }
            imagePath = imageUrl;
        });
        return imagePath;

    }
//...
}

Once the image path is generated, share it with the user over your API.

In order to confirm the QR code was scanned, you could ask the user to input their fresh TOTP code. This can be verified using:

{
//...
    verify: (token, secret)=> authenticator.verify({secret, token}),
//...
}

Logging In

Step 1: Username and password

The first step of a 2FA flow resembles everything you’re probably used to by now: the user first provides their username and password. The server is then responsible for verifying that the specified user exists in the database. If both username and email are correct, the API should next check if 2FA is enabled for the account.

Should 2FA be active for the account, the server should serve a simple reply such as “2FAEnabled:true”. This tells the client to send the same request again (this time to a new ‘/verify’ route) with the necessary token. The client might have to persist the credentials, but since you’re dealing with potentially sensitive data, be careful with how you store it.

If you’re uncomfortable with having a password that exposed, the server could also generate a special short-lived token that’s only valid for the ‘/verify’ route. This token (and response body) is sent to the client, who saves it.

app.post('/login', (req, res) => {
    const user = req.user;

    //Do some validation to make sure username and password exist
    // Check that the email exists
    if (user.existsInDatabase){
        // Check to see if email and password are valid
        // If email and password are valid, check if 2fa is enabled
        if (user.hasCorrectCredentials) {
            if (user.is2FAEnabled) {
                return res.status(200).send({
                    shortLivedToken: 'xxyy', // this shouldn't be a normal access token that can be used elsewhere in the app
                    is2FAEnabled: true
                })
            }
        }
    }
});

Step 2: Verifying the TOTP

Once the client has received a response indicating 2FA has been enabled, the site or app should redirect the user to a second page or show a dialog requesting a TOTP code. Depending on how you’ve decided to implement it, the server authorizes the client based on the short-lived token or by re-sending the username and password.

The final step will look something like:

app.post('/login/verify', (req, res)=> {
    const otpToken = req.body.otpToken;
    const accessToken = req.headers["Authorization"];
    const user = req.user;

    if (accessToken.isValid) {
        const isOTPTokenValid = verify(otpToken, user.secret);
        if (isOTPTokenValid) {
            res.status(200).send({
                accessToken: '' //jwt generate token. This is a normal token that can be used to log in.
            })
        }
    }
})

If the user provides the right TOTP code, we provide them with a normal access token that can be used to access other parts of the app!

Bonus update: Generating recovery keys

What happens if one of your users says they lost their phone, and thereby access to their authenticator app, so they can’t log in? The most common practice is to allow users to generate recovery keys. If they no longer have access to their authenticator app, they are allowed to use one of these recovery keys to effectively reset the two-factor authentication status as tracked by your application.

There are a few simple rules to be followed when it comes to generating and handling recovery keys.

  • The recovery keys should be long enough that they aren’t easy to guess or crack through a dictionary attack.
  • They should be treated like one-time passwords. Once they are used, they need to be discarded.
  • Just like one-time passwords, they should be encrypted before being stored on the database. This way, if an intruder ever gains access to your database, the codes they gain access to are practically useless. The downside to this is that your users can only ever see (or download) their codes once. If the codes are lost, they have to be regenerated.

Conclusion

Two-factor authentication adds a layer of protection to your authentication mechanism. However, it will ultimately become useless if you attempt to treat it as a solution to all your problems.

  • Both the normal login and ‘/verify’ routes should be protected from brute forcing. If someone decides to take down your 2FA system, logging in might turn out to be impossible.
  • Be careful with the delay window and time step options. You don’t want your TOTP codes to be valid for too long.

Link to the Github Repository with a project.

Building Mobile Apps With Vue3 and Ionic

Ionic is an open-source UI framework and toolkit that helps developers build highly efficient and performant apps for native iOS, Android, and the web (PWA). On October 15th, 2020, the Ionic team announced the official release of Ionic-vue, a new way of building applications using the two technologies you already love: Ionic and Vuejs.

3 Replies to “Implementing Two-Factor Authentication with NodeJS and otplib”

  1. Hey, very nice site. I came across this on Google, and I am stoked that I did. I will definitely be coming back here more often. Wish I could add to the conversation and bring a bit more to the table, but am just taking in as much info as I can at the moment. Thanks for sharing.

  2. Thanks for this very useful info you have provided us. I will bookmark this for future reference and refer it to my friends. More power to your blog.

  3. Sure, here’s a brief guide on how to implement Two-Factor Authentication with NodeJS and otplib:

    Install the otplib package by running the following command in your NodeJS project:
    css

    npm install otplib –save
    Import the otplib library and generate a secret key. You can use the authenticator.generateSecret() method to generate a secret key.
    javascript

    const { authenticator } = require(‘otplib’);
    const secret = authenticator.generateSecret();
    console.log(‘Your secret key is:’, secret);
    Display the secret key to the user, or save it to your database.

    When the user logs in, prompt them to enter the one-time password (OTP) generated by their authenticator app. You can use the authenticator.check() method to verify the OTP entered by the user.

    javascript

    const isValid = authenticator.check(otp, secret);
    if (isValid) {
    // Authentication successful
    } else {
    // Authentication failed
    }
    To generate a time-based OTP, you can use the authenticator.generate() method.

    javascript

    const otp = authenticator.generate(secret);
    console.log(‘Your one-time password is:’, otp);

    You can also customize the OTP generation process by passing options to the authenticator.generate() method. For example, you can set the number of digits in the OTP or the algorithm used to generate it.

    javascript

    const options = { digits: 6, algorithm: ‘sha512’ };
    const otp = authenticator.generate(secret, options);
    console.log(‘Your one-time password is:’, otp);

    By implementing Two-Factor Authentication with NodeJS and otplib, you can add an extra layer of security to your application, making it more difficult for hackers to gain access to your user’s data.

    Source: https://smsala.com/bulk-sms-singapore/

Leave a Reply