×
By the end of this chapter, you should be able to:
one-way encryption
, hashing
, and salting
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).
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.
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
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