{ Password Hashing with bcrypt. }

Objectives:

By the end of this chapter, you should be able to:

  • Define essential terms like one-way encryption, hashing, and salting
  • Explain how to securely store passwords in a database
  • Add authentication to an express app

Auth Intro / Terms

  • bcrypt - The library we will be using for hashing, salting, and decrypting passwords is the bcrypt library. The library is based on the blowfish cipher, a widely recognized algorithm for secure one way hashing.

  • one-way encryption - With one-way encryption, also called hashing, encrypted information is not meant to be decrypted. This is quite common when thinking about passwords. Ideally the only person who should be aware of their password is the person who created it, as this is much more secure. As you'll see, we'll be hashing passwords and storing the hashes in a database. When a user then attempts to log in, we can hash the password they provide and see if there is a match between hashes. This allows us to authenticate a user without knowing what their password is. While this is a good start, hashing alone is not secure as it opens yourself up to dictionary attacks.

  • dictionary attack - A dictionary attack occurs when a hacker takes all possible combinations of characters and runs it through a hashing algorithm. Once they have found a hash that matches yours, they can look up the text in the dictionary they have built and voila - you are hacked!

  • salting - In order to prevent dictionary attacks, we add a randomized string of characters to the password (the salt) and then hash the entire password. This prevents someone from successfully creating a dictionary.

  • two-way encryption - In contrast with one way encryption, two-way encryption is meant to be decrypted, often with some sort of key that the parties can use for decryption. This is very common when establishing a secure connection between another party (SSH keys on GitHub).

Auth with bcrypt

To get started with bcrypt, let's first mkdir learn-bcrypt && cd learn-bcrypt and then install the module using npm install bcrypt. Instead of building a new express app, let's start by playing around in a node console, or create a file and add the following:

// npm packages
const bcrypt = require("bcrypt");

// globals
const password = "secret";
const saltRounds = 10;

bcrypt
  .hash(password, saltRounds)
  .then(hashedPassword => {
    console.log("hash", hashedPassword);
    return hashedPassword; // notice that all of these methods are asynchronous!
  })
  .then(hash => {
    return bcrypt.compare(password, hash); // what does this method return?
  })
  .then(res => {
    console.log("match", res);
  });

Let's step through this code a bit. First, bcrypt.hash will hash our password. We include a number (saltRounds), which you can roughly think of as measuring how many steps are involved in creating the hash. (Try changing 10 to 16, and see how much longer it takes to run the above code!)

Once the password is hashed, it is logged to the console. You should see something like this:

hash $2a$10$Ns876QMLlCV4nT5ctzDHJeRMrvbVvZeGHn3gtJ6sJn5fILfEivZGa

The bcrypt hash consists of four parts:

2a -> prefix
10 -> work factor
Ns876QMLlCV4nT5ctzDHJe -> salt
RMrvbVvZeGHn3gtJ6sJn5fILfEivZGa -> hashed password

The prefix just indicates that bcrypt was used to generate the string. The salt is a random string that is then combined with the password to generate the hashed password. In particular, the hash that we get from bcrypt includes the salt in it! This is what allows us to check passwords when users attempt to log in later: the combination of salt, work factor, and password uniquely determines the hash.

You should see match true get logged to the console, since we're checking the string 'secret' against the hash corresponding to that string. But if you try comparing hash to any other string, you should see that the console outputs match false instead.

Adding Auth to our applications

Now that we have a good idea of how to hash passwords, let's build a small API that allows for a user to send a username (which will be unique) and password (which will be required) to our Express server. We will then take that plain text password and hash it to store it in the database.

You can find the code for this application here

Let's start in Terminal

mkdir node-bcrypt-sql
cd node-bcrypt-sql
touch app.js
npm init -y
npm install express body-parser morgan pg bcrypt
mkdir routes db
touch routes/users.js
touch db/index.js
psql

And in psql

DROP DATABASE IF EXISTS "node-bcrypt-sql";
CREATE DATABASE "node-bcrypt-sql";
\c "node-bcrypt-sql"
CREATE TABLE users (id SERIAL PRIMARY KEY, username TEXT NOT NULL UNIQUE, password TEXT NOT NULL);
\q

Back in our index.js:

const { Client } = require("pg");

const client = new Client({
  connectionString: "postgresql://localhost/node-bcrypt-sql"
});

client.connect();

module.exports = client;

Let's now work on our routes/users.js

const express = require("express");
const router = express.Router();
const db = require("../db");
const bcrypt = require("bcrypt");

router.get("/", async (req, res, next) => {
  try {
    const result = await db.query("SELECT * FROM users");
    return res.json(result.rows);
  } catch (e) {
    return next(e);
  }
});

router.post("/", async (req, res, next) => {
  try {
    const hashedPassword = await bcrypt.hash(req.body.password, 10);
    const result = await db.query(
      "INSERT INTO users (username, password) VALUES ($1,$2) RETURNING *",
      [req.body.username, hashedPassword]
    );
    return res.json(result.rows[0]);
  } catch (e) {
    return next(e);
  }
});

module.exports = router;

And finally our app.js

const express = require("express");
const app = express();
const bodyParser = require("body-parser");
const morgan = require("morgan");
const usersRoutes = require("./routes/users");

app.use(morgan("tiny"));
app.use(bodyParser.json());
app.use("/users", usersRoutes);

// catch 404 and forward to error handler
app.use((req, res, next) => {
  var err = new Error("Not Found");
  err.status = 404;
  return next(err);
});

// development error handler
// will print stacktrace
if (app.get("env") === "development") {
  app.use((err, req, res, next) => {
    res.status(err.status || 500);
    return res.json({
      message: err.message,
      error: err
    });
  });
}

app.listen(3000, () => {
  console.log("Getting started on port 3000!");
});

Let's run nodemon and then in another tab test this API out!

http localhost:3000/users
http POST localhost:3000/users username='elie' password='secret'
http localhost:3000/users

Now that we have that working, let's add an endpoint where a user can submit a username and password and the API will respond with a successful message if both of those are correct.

const express = require("express");
const router = express.Router();
const db = require("../db");
const bcrypt = require("bcrypt");

router.get("/", async (req, res, next) => {
  try {
    const result = await db.query("SELECT * FROM users");
    return res.json(result.rows);
  } catch (e) {
    return next(e);
  }
});

router.post("/", async (req, res, next) => {
  try {
    const hashedPassword = await bcrypt.hash(req.body.password, 10);
    const result = await db.query(
      "INSERT INTO users (username, password) VALUES ($1,$2) RETURNING *",
      [req.body.username, hashedPassword]
    );
    return res.json(result.rows[0]);
  } catch (e) {
    return next(e);
  }
});

router.post("/login", async (req, res, next) => {
  try {
    // try to find the user first
    const foundUser = await db.query(
      "SELECT * FROM users WHERE username=$1 LIMIT 1",
      [req.body.username]
    );
    if (foundUser.rows.length === 0) {
      return res.json({ message: "Invalid Username" });
    }
    // if the user exists, let's compare their hashed password to a new hash from req.body.password
    const hashedPassword = await bcrypt.compare(
      req.body.password,
      foundUser.rows[0].password
    );
    // bcrypt.compare returns a boolean to us, if it is false the passwords did not match!
    if (hashedPassword === false) {
      return res.json({ message: "Invalid Password" });
    }
    return res.json({ message: "Logged In!" });
  } catch (e) {
    return res.json(e);
  }
});

module.exports = router;

Let's test the new endpoint we made! In the terminal let's try:

http localhost:3000/users
http POST localhost:3000/users/login username='elie' password='secret'
http POST localhost:3000/users/login username='eliez' password='secret'
http POST localhost:3000/users/login username='elie' password='secretz'

You can find the code for this application here

Logging in

So far we have a good start on the app, but once a user has logged in we need some way of remembering who they are and that they've logged in! In the next section we'll introduce a technology called JSON Web Tokens or JWTs, which is what we will send back from the server to mark a user as logged in.

When you're ready, move on to Authentication with JWTs

Continue

Creative Commons License