Implementing Two-Factor Authentication with NodeJS and otplib

栏目: IT技术 · 发布时间: 4年前

内容简介: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 s
Implementing Two-Factor Authentication with NodeJS and otplib

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 authenticationrefers 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!

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 .


以上所述就是小编给大家介绍的《Implementing Two-Factor Authentication with NodeJS and otplib》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

1024·人与机器共同进化

1024·人与机器共同进化

东西文库 / 译言·东西文库/电子工业出版社 / 2013-12-20 / 55元

《1024》:国内第一本专注于科技文化的mook。 本期创刊号将目光定焦在“人与机器”这个超热点领域。 如果把机器获得思维能力看作是一种进化, 那人类具备不朽之躯同样也是一种进化。 这是一个野心勃勃但又充满不确定性的未来。 在我们一厢情愿地猜测机器将在不远的将来赶超自己而惶惶不可终日时,人类其实还有一个机会——变得更像机器。这并非科幻小说,而是正在发生的现实。人类创造......一起来看看 《1024·人与机器共同进化》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具