Monday, 22 July, 2019 UTC


Summary

Identity theft is a serious issue nowadays. With so many accidents happening on the web, it is a great time to learn about providing an additional layer of security for our users. In this article, we go through the principles and implementation of Node.js Two-Factor Authentication (2FA). We do that by using Google Authenticator and a Node.js server.
The source code is available in the GitHub repository in the part-11 branch. Feel free to give it a star.
Implementing Node.js Two-Factor Authentication
With Two-Factor Authentication, the user needs to prove his identity in two ways. A straightforward example of that is using an ATM. You need a bank card – something you possess – and the PIN code – something you know. Another example is combining your regular password with a one-time code that your smartphone generates.

Generating a secret key

First, we need to create a secret key, unique for every user. f
For effortless Node.js Two-Factor Authentication handling, we use the speakeasy library. Its first job is to generate a secret key for us.
npm install speakeasy @types/speakeasy
authentication.service.ts
import * as speakeasy from 'speakeasy';

function getTwoFactorAuthenticationCode() {
  const secretCode = speakeasy.generateSecret({
    name: process.env.TWO_FACTOR_AUTHENTICATION_APP_NAME,
  });
  return {
    otpauthUrl : secretCode.otpauth_url,
    base32: secretCode.base32,
  };
}
Here we add a new environment variable – it determines the name that will be visible in the Google Authenticator application.
There are two essential things that our 
getTwoFactorAuthenticationCode
 returns. One of them is the secret code in the base32 format. We use it to validate the identity of the user later.
The second thing is 
otpauth_url
. It is a One Time Password Authentication (OTPA) compatible with Google Authenticator. We can use it to generate a Quick Response (QR) code that we display for the user.

Creating a QR image

Applications like the Google Authenticator allow users to add a page that they authenticate to either by manually entering a key, or scanning a QR code. The latter is way faster and is a standard right now. To generate QR images, we use a library called qrcode.
npm install qrcode @types/qrcode
The most suitable function for us that it has is called 
toFileStream
. It writes the QR code to a writable stream. An example of such is the response object!
If you want to dive deeper, check out Writable streams, pipes, and the process streams
authentication.service.ts
import * as QRCode from 'qrcode';

function respondWithQRCode(data: string, response: Response) {
  QRCode.toFileStream(response, data);
}
Once we have all of the above, we can create our new endpoint in the authentication controller.
authentication.controller.ts
private generateTwoFactorAuthenticationCode = async (
  request: RequestWithUser,
  response: express.Response,
) => {
  const user = request.user;
  const {
    otpauthUrl,
    base32,
  } = this.authenticationService.getTwoFactorAuthenticationCode();
  await this.user.findByIdAndUpdate(user._id, {
    twoFactorAuthenticationCode: base32,
  });
  this.authenticationService.respondWithQRCode(otpauthUrl, response);
}

private initializeRoutes() {
  this.router.post(`${this.path}/2fa/generate`, authMiddleware(), this.generateTwoFactorAuthenticationCode);
}
As you can see, we also save the generated code in the database. It later comes in handy when turning on Two-Factor Authentication. Please note that a user needs to be logged in for it to work.
Since we have the generated code, we can use the Google Authenticator now:
We now have a fully functional workflow of generating a secret code and presenting it to the user!
Turning on Node.js Two-Factor Authentication
Currently, we only generate secret codes, but we haven’t yet turned on the Node.js Two-Factor Authentication for a user. For it to happen, we need a separate endpoint that the user sends his first verification code to. To begin, we need a function that validates the upcoming verification code.
authentication.service.ts
public verifyTwoFactorAuthenticationCode(twoFactorAuthenticationCode: string, user: User) {
  return speakeasy.totp.verify({
    secret: user.twoFactorAuthenticationCode,
    encoding: 'base32',
    token: twoFactorAuthenticationCode,
  });
}
The 
speakeasy.totp.verify
 method verifies our Time-based One-time Password (TOTP) that user got from the Google Authenticator app against the secret code that we generated and saved in the database previously. Once we got that, we can create an endpoint that turns on the Two-Factor Authentication.
authentication.controller.ts
private turnOnTwoFactorAuthentication = async (
  request: RequestWithUser,
  response: express.Response,
  next: express.NextFunction,
) => {
  const { twoFactorAuthenticationCode } = request.body;
  const user = request.user;
  const isCodeValid = await this.authenticationService.verifyTwoFactorAuthenticationCode(
    twoFactorAuthenticationCode, user,
  );
  if (isCodeValid) {
    await this.user.findByIdAndUpdate(user._id, {
      isTwoFactorAuthenticationEnabled: true,
    });
    response.send(200);
  } else {
    next(new WrongAuthenticationTokenException());
  }
}

private initializeRoutes() {
  this.router.post(
    `${this.path}/2fa/turn-on`,
    validationMiddleware(TwoFactorAuthenticationDto),
    authMiddleware(),
    this.turnOnTwoFactorAuthentication,
  );
}
If you want to know how the validationMiddleware works, check out Error handling and validating incoming data
In the 
turnOnTwoFactorAuthentication
 function, we check if the provided code is valid. If that’s the case, we enable the Two-Factor Authentication by setting the
isTwoFactorAuthenticationEnabled
 flag to true.
In this part of the series, we’ve made some minor change to the User model. You can look it up in the repository.
Logging in using our Node.js Two-Factor Authentication
The last part is logging in and authenticating using the Node.js Two-Factor Authentication. To implement it, we use the 
verifyTwoFactorAuthenticationCode
 function again. To make it more clear, let’s review the whole flow of authentication:
  1. The user attempts to log in using his email and a valid password, and we give him a JWT token.
    • If he doesn’t have the 2FA turned on, this gives him full access.
    • If he does have the 2FA turned on, we provide him with access just to the 
      /2fa/authenticate
       endpoint.
  2.  The user sends a valid code to the 
    /2fa/authenticate
     endpoint and is given a new JWT token with full access
That being said, let’s create a new route that allows the user to authenticate using a JWT token.
authentication.controller.ts
private secondFactorAuthentication = async (
  request: RequestWithUser,
  response: express.Response,
  next: express.NextFunction,
) => {
  const { twoFactorAuthenticationCode } = request.body;
  const user = request.user;
  console.log('user', user);
  const isCodeValid = await this.authenticationService.verifyTwoFactorAuthenticationCode(
    twoFactorAuthenticationCode, user,
  );
  if (isCodeValid) {
    const tokenData = this.authenticationService.createToken(user, true);
    response.setHeader('Set-Cookie', [this.createCookie(tokenData)]);
    response.send({
      ...user.toObject(),
      password: undefined,
      twoFactorAuthenticationCode: undefined
    });
  } else {
    next(new WrongAuthenticationTokenException());
  }
}
In the code above, we validate the upcoming 
twoFactorAuthenticationCode
. If it is valid, we create and send back a new token. We respond with the user details, excluding the password and the Two-Factor Authentication code. To do this, we modify the 
createToken
 function:
authentication.service.ts
public createToken(user: User, isSecondFactorAuthenticated = false): TokenData {
  const expiresIn = 60 * 60; // an hour
  const secret = process.env.JWT_SECRET;
  const dataStoredInToken: DataStoredInToken = {
    isSecondFactorAuthenticated,
    _id: user._id,
  };
  return {
    expiresIn,
    token: jwt.sign(dataStoredInToken, secret, { expiresIn }),
  };
}
Now the JWT token also carries the information about the Two-Factor Authentication. The only thing left to do is to alter the authMiddleware.

The authMiddleware

We need the authMiddleware to check the
isSecondFactorAuthenticated
 flag and throw an error if it is set to false when the user has 2FA turned on. Also, since there is one endpoint that should work even with the 2FA turned off, we need an option to omit it.
auth.middleware.ts
function authMiddleware(omitSecondFactor = false): RequestHandler {
  return async (request: RequestWithUser, response: Response, next: NextFunction) => {
    const cookies = request.cookies;
    if (cookies && cookies.Authorization) {
      const secret = process.env.JWT_SECRET;
      try {
        const verificationResponse = jwt.verify(cookies.Authorization, secret) as DataStoredInToken;
        const { _id: id, isSecondFactorAuthenticated } = verificationResponse;
        const user = await userModel.findById(id);
        if (user) {
          if (!omitSecondFactor && user.isTwoFactorAuthenticationEnabled && !isSecondFactorAuthenticated) {
            next(new WrongAuthenticationTokenException());
          } else {
            request.user = user;
            next();
          }
        } else {
          next(new WrongAuthenticationTokenException());
        }
      } catch (error) {
        next(new WrongAuthenticationTokenException());
      }
    } else {
      next(new AuthenticationTokenMissingException());
    }
  };
}
For a step-by-step explanation of authentication with the email and the password, check out Registering users and authenticating with JWT
In the code above, we demand the 
isSecondFactorAuthenticated
 to be true, if the user has the Two-Factor Authentication enabled and the 
omitSecondFactor
flag isn’t set to false.
Now we can use the authMiddleware with the 
omitSecondFactor
flag for the 
/auth/2fa/authenticate
 endpoint.
authentication.controller.ts
private initializeRoutes() {
  this.router.post(
    `${this.path}/2fa/authenticate`,
    validationMiddleware(TwoFactorAuthenticationDto),
    authMiddleware(true),
    this.secondFactorAuthentication,
  );
}

Modify the basic logging in logic

So far, the 
/auth/login
 acts the same way regardless of the 2FA being turned on or not. Let’s modify it by responding just with the 
isTwoFactorAuthenticationEnabled
 flag if it is turned on. Thanks to that, we don’t give the details of a user just yet. We also avoid sending the 
twoFactorAuthenticationCode
.
private loggingIn = async (request: express.Request, response: express.Response, next: express.NextFunction) => {
  const logInData: LogInDto = request.body;
  const user = await this.user.findOne({ email: logInData.email });
  if (user) {
    const isPasswordMatching = await bcrypt.compare(logInData.password, user.password);
    if (isPasswordMatching) {
      user.password = undefined;
      user.twoFactorAuthenticationCode = undefined;
      const tokenData = this.authenticationService.createToken(user);
      response.setHeader('Set-Cookie', [this.createCookie(tokenData)]);
      if (user.isTwoFactorAuthenticationEnabled) {
        response.send({
          isTwoFactorAuthenticationEnabled: true,
        });
      } else {
        response.send(user);
      }
    } else {
      next(new WrongCredentialsException());
    }
  } else {
    next(new WrongCredentialsException());
  }
}
Summary
By doing all of the above, we set up a basic flow for the Node.js Two-Factor Authentication. We implement a way to generate a secret key and a QR image, turn on the Two-Factor Authentication, and validate upcoming requests. It might use some tweaks, like additional error handling, and a way for the users to deal with a lost device. This article covers just one way to implement Node.js 2FA. Feel free to change the flow and implement additional features, if needed.
The post TypeScript Express tutorial #11. Node.js Two-Factor Authentication appeared first on Marcin Wanago Blog - JavaScript, both frontend and backend.