Node.js: Keeping secrets and passwords secure

We’ve cautioned several times about the importance of safely handling user identification information. The intention to handle that data safely is one thing, but it is important to follow through and actually do so. While we’re using a few good practices so far, as it stands, the Notes application would not withstand any kind of security audit for the following reasons:

  • User passwords are kept in clear text in the database.
  • The authentication tokens for Twitter et al. are in clear text.
  • The authentication service API key is not a cryptographically secure anything; it’s just a clear text universally unique identifier (UUID).

If you don’t recognize the phrase clear text, it simply means unencrypted. Anyone could read the text of user passwords or the authentication tokens. It’s best to keep both encrypted to avoid information leakage.

Keep this issue in the back of your mind because we’ll revisit these—and other—security issues in Chapter 14, Security in Node.js Applications.

Before we leave this chapter, let’s fix the first of those issues: storing passwords in plain text. We made the case earlier that user information security is extremely important. Therefore, we should take care of this from the beginning.

The bcrypt Node.js package makes it easy to securely store passwords. With it, we can easily encrypt the password right away, and never store an unencrypted password.

To install bcrypt in both the notes and users directories, execute the following command:

$ npm install bcrypt@5.x –save 

The bcrypt documentation says that the correct version of this package must be used precisely for the Node.js version in use. Therefore, you should adjust the version number appropriately to the Node.js version you are using.

The strategy of storing an encrypted password dates back to the earliest days of Unix. The creators of the Unix operating system devised a means for storing an encrypted value in /etc/passwd, which was thought sufficiently safe that the password file could be left readable to the entire world.

Let’s start with the user information service.

1. Adding password encryption to the user information service

Because of our command-line tool, we can easily test end-to-end password encryption. After verifying that it works, we can implement encryption in the Notes application.

In cli.mjs, add the following code near the top:

import { default as bcrypt } from ‘bcrypt’;

const saltRounds = 10;

This brings in the bcrypt package, and then we configure a constant that governs the CPU time required to decrypt a password. The bcrypt documentation points to a blog post discussing why the algorithm of bcrypt is excellent for storing encrypted passwords. The argument boils down to the CPU time required for decryption. A brute-force attack against the password database is harder, and therefore less likely to succeed if the passwords are encrypted using strong encryption, because of the CPU time required to test all password combinations.

The value we assign to saltRounds determines the CPU time requirement. The documentation explains this further.

Next, add the following function:

async function hashpass(password) {

let salt = await bcrypt.genSalt(saltRounds);

let hashed = await bcrypt.hash(password, salt);

return hashed;

}

This takes a plain text password and runs it through the encryption algorithm. What’s returned is the hash for the password.

Next, in the commands for add, find-or-create, and update, we make this same change, as follows:

.action(async (username, cmdObj) => {

const topost = {

username,

password: await hashpass(cmdObj.password),

};

}) 

That is, in each, we make the callback function an async function so that we can use await. Then, we call the hashpass function to encrypt the password.

This way, we are encrypting the password right away, and the user information server will be storing an encrypted password.

Therefore, in user-server.mjs, the password-check handler must be rewritten to accommodate checking an encrypted password.

At the top of user-server.mjs, add the following import:

import { default as bcrypt } from ‘bcrypt’;

Of course, we need to bring in the module here to use its decryption function. This module will no longer store a plain text password, but instead, it will now store encrypted passwords. Therefore, it does not need to generate encrypted passwords, but the bcrypt package also has a function to compare a plain text password against the encrypted one in the database, which we will use.

Next, scroll down to the password-check handler and modify it, like so:

server.post(‘/password-check’, async (req, res, next) => {

try {

const user = await SQUser.findOne({

where: { username: req.params.username } });

let checked; if (!user) {

checked = {

check: false, username: req.params.username, message: “Could not find user”

};

} else {

let pwcheck = false;

if (user.username === req.params.username) {

pwcheck = await bcrypt.compare(req.params.password, user.password);

}

if (pwcheck) {

checked = { check: true, username: user.username };

} else { checked = {

check: false, username: req.params.username, message: “Incorrect username or password”

};

}

}

} catch (e) { .. }

});

The bcrypt.compare function compares a plain text password, which will be arriving as req.params.password, against the encrypted password that we’ve stored. To handle encryption, we needed to refactor the checks, but we are testing for the same three conditions. And, more importantly, this returns the same objects for those conditions.

To test it, start the user information server as we’ve done before, like this:

$ npm start 

> user-auth-server@1.0.0 start /home/david/Chapter08/users

> DEBUG=users:* PORT=5858 SEQUELIZE_CONNECT=sequelize-sqlite.yaml node

./user-server.mjs 

users:service User-Auth-Service listening at http://127.0.0.1:5858

+0ms

 In another window, we can create a new user, as follows:

$ node cli.mjs add –password w0rd –family-name Einarsdottir –given- name Ashildr –email me@stolen.tardis me

Created {

id: ‘me’,

username: ‘me’, provider: ‘local’,

familyName: ‘Einarsdottir’, 

givenName: ‘Ashildr’, middleName: null,

emails: [ ‘me@stolen.tardis’ ], photos: []

} 

We’ve done both these steps before. Where it differs is what we do next. Let’s check the database to see what was stored, as follows:

$ sqlite3 users-sequelize.sqlite3

SQLite version 3.31.1 2020-01-27 19:55:54

Enter “.help” for usage hints. sqlite> select * from SQUsers;

1|me|$2b$10$stjRlKjSlQVTigPkRmRfnOhN7uDnPA56db0lUTgip8E6/n4PP7Jje|loca

l|Einarsdottir|Ashildr||[“me@stolen.tardis”]|[]|2020-02-05

20:59:21.042 +00:00|2020-02-05 20:59:21.042 +00:00

sqlite> ^D 

Indeed, the password field no longer has a plain text password, but what is—surely—encrypted text.

Next, we should check that the password-check command behaves as expected:

$ node cli.mjs password-check me w0rd

{ check: true, username: ‘me’ }

$ node cli.mjs password-check me w0rdy

{

check: false,

username: ‘me’,

message: ‘Incorrect username or password’

} 

We performed this same test earlier, but this time, it is against the encrypted password.

We have verified that a REST call to check the password will work. Our next step is to implement the same changes in the Notes application.

2. Implementing encrypted password support in the Notes application

Since we’ve already proved how to implement encrypted password checking, all we need to do is duplicate some code in the Notes server.

In users-superagent.mjs, add the following code to the top:

import { default as bcrypt } from ‘bcrypt’;

const saltRounds = 10;

async function hashpass(password) {

let salt = await bcrypt.genSalt(saltRounds);

let hashed = await bcrypt.hash(password, salt);

return hashed;

}

As before, this imports the bcrypt package and configures the complexity that will be used, and we have the same encryption function because we will use it from multiple places.

Next, we must change the functions that interface with the backend server, as follows:

export async function create(username, password,

provider, familyName, givenName, middleName, emails, photos)

{

var res = await request.post(reqURL(‘/create-user’)).send({

username,

password: await hashpass(password),

provider, familyName, givenName, middleName, emails, photos

})

}

export async function update(username, password,

provider, familyName, givenName, middleName, emails, photos)

{

var res = await request.post(reqURL(‘/update-user/${username}’))

.send({

username, password: await hashpass(password), provider, familyName, givenName, middleName, emails, photos

})

}

export async function findOrCreate(profile) {

var res = await request.post(reqURL(‘/find-or-create’)).send({

username: profile.id,

password: await hashpass(profile.password),

})

}

In those places where it is appropriate, we must encrypt the password. No other change is required.

Because the password-check backend performs the same checks, returning the same object, no change is required in the frontend code.

To test, start both the user information server and the Notes server. Then, use the application to check logging in and out with both a Twitter-based user and a local user.

We’ve learned how to use encryption to safely store user passwords. If someone steals our user database, cracking the passwords will take longer thanks to the choices made here.

We’re almost done with this chapter. The remaining task is simply to review the application architecture we’ve created.

Source: Herron David (2020), Node.js Web Development: Server-side web development made easy with Node 14 using practical examples, Packt Publishing.

Leave a Reply

Your email address will not be published. Required fields are marked *