Skip to content
Code with Hou

Introduction to Node.js Part V - Authentication with Passport.js & JSON Web Tokens

Node.js, JavaScript, Express, Passport.js, JWT, intermediate, tutorial, intro-to-node-js8 min read

Introduction

Welcome to the fifth part of the six-part Introduction to Node.js tutorial series. By the end of this tutorial, you will be able to:

  • implement signup and login functionalities for our Express Recipes API
  • authenticate application users' requests using Passport.js and JSON Web Tokens

Tutorial Outline

  • Passport.js Overview
  • Complete Example
  • JSON Web Tokens Overview
  • Integrating Passport.js & JSON Web Tokens into Our API
    • Task 1: Install Dependencies
    • Task 2: Set up Files for User Authentication Routes
    • Task 3: Configure the Passport.js JWT Strategy
    • Task 4: Create the users Service
    • Task 5: Create the users Controller
    • Task 6: Create the users Router
    • Task 7 (CHALLENGE): Secure Routes for the recipes Resource
    • Task 8: Test Privileged Routes with Postman
    • BONUS: Logout
  • Additional Resources
  • Review
  • Key Takeaways
  • What's next

Passport.js Overview

From Passport.js:


"Passport is authentication middleware for Node. It is designed to serve a singular purpose: authenticate requests. When writing modules, encapsulation is a virtue, so Passport delegates all other functionality to the application. This separation of concerns keeps code clean and maintainable, and makes Passport extremely easy to integrate into an application.

In modern web applications, authentication can take a variety of forms. Traditionally, users log in by providing a username and password. With the rise of social networking, single sign-on using an OAuth provider such as Facebook or Twitter has become a popular authentication method. Services that expose an API often require token-based credentials to protect access."


Passport.js offers a wide variety of authentication mechanisms (known as "strategies") as individually-packaged modules. Currently, more than 500 authentication strategies exist in the Passport.js ecosystem. Strategies range from verifying a username and password, delegated authentication using OAuth or federated authentication using OpenID.

In this section, you'll be using the Passport.js authentication strategy based on JSON Web Tokens to add authentication to your Express Recipes API.

Starter Code

You can work off of the repo that you used in the previous tutorial. Or you can fork and clone this repo.

Complete Example

A Git branch with the complete code for this tutorial is available here.

JSON Web Tokens Overview

In REST architectures, client-server interactions are typically stateless. A stateless server does not store any history or state about the client session. After all, server-based sessions are often costly to implement and don't scale well.

Instead, session state is often stored on the client (e.g., in a browser's cookie), so the client is responsible for transferring all the information needed to execute a request to the server.

JWT (JSON Web Tokens) is a lightweight and secure approach to transfer state from the client to the server in a REST framework. JWT (pronounced "JOT") is an open-standard authentication strategy that relies on the exchange of encoded and crytopgraphically signed JSON strings between client and server.

JWTs allow you to delegate authentication logic to an authorization server. In fact, you can delegate login/signup of a cluster of applications to a single Authorization server.

To understand how JWT works, let's consider a typical login flow that uses JWT-based authentication:

JWT-Based Authentication

  1. The client makes a HTTP request to the authorization server, sending along the user's login credentials (e.g., email and password).

  2. The authorization server validates the user's credentials. If authentication is successful, the server sends a JSON response to the client that includes a JWT access token.

  3. The client receives the JWT and stores it in the browser (e.g., cookie).

  4. The next time the client makes a HTTP request to a route that requires authentication, it will first attach the JWT to the request's Authorization headers before sending the request to the application server that's hosting the protected API.

  5. On receiving the request, the application server extracts the JWT from the request and verifies the embedded signature. If the verification is successful, the server fulfills the request. Otherwise, the request is blocked and the user will receive an error denying their request to access the resource on the application server.

As you can see, the application server does not have to keep access tokens in-memory in between requests, so it can be stateless.

JWTs function like temporary user credentials so the user does not have to repeatedly provide their password to gain access to resources on a server.

Later on in the tutorial, you'll take a look at what a JWT access token actually looks like.

Integrating Passport.js & JSON Web Tokens into Express Recipes API

Let's authenticate our users using JWT before allowing them access to privileged routes in the Express Recipes API! To support user authentication, you will need to implement routes to handle user signup and login.

As part of the signup process, the user will typically provide a password that the user can use to log into the application going forward. As a best practice, the user's password will need to be encrypted before it is stored in the database. You can use the bcrypt library to do that.

Then, during the login process, the server will need to validate the user's credentials and generate as well as validate JWTs accordingly. You can use the popular jsonwebtoken library to do that.

The passport and passport-jwt libraries will allow you to implement a Passport.js authentication strategy based on JSON Web Tokens.

For simplicity, you will implement the JWT authentication mechanism directly in the Express Recipes application server, rather than in a separate authorization server.

Task 1: Install Dependencies

  1. Navigate to your project folder.

  2. Install the following dependencies:

    1$ npm install --save bcrypt express jsonwebtoken passport passport-jwt dotenv

Task 2: Set up Files for User Authentication Routes

  1. In the previous tutorial, you set up a router, controller, and service for the recipes resource in the Express Recipes API. Let's follow a similar pattern to organize our user authentication logic:

    1$ touch src/routers/users.js src/services/users.js db/users.json src/controllers/users.js
  2. Create a new folder to store middleware functions and within it, create a file called auth.js:

    1$ mkdir src/middleware
    2$ touch src/middleware/auth.js

    Later on, you will configure the Passport.js strategy inside src/middleware/auth.js.

  3. You will be storing the users data in db/users.json. Note that this is not a robust database solution since there's no built-in data validation, but it is sufficient for demonstration purposes.

    Add a user to the file to serve as an example of what an entry in the file might look like:

    db/users.json
    1[
    2 {
    3 "id": 1,
    4 "name": "Hou",
    5 "email": "hou@mail.com",
    6 "password": "$2b$10$u2r3EK7.p4l9bzQd74nGNepvIHw5gF84j4Dy6/kJmtNR4OC.A5t5O"
    7 }
    8]

    Notice that the user's password is hashed, for security reasons. Later on, you will learn how to use bcrypt to hash a user's password before storing it in the database.

  4. Create a .env file in the project root folder to store the JWT-related configuration variables:

    1JWT_SECRET=##%%MyS3cr3tK3Y%%##

    JWT_SECRET will be used to create a signature for signing and validating the JWTs.

    Keep in mind that since the .env file typically contains sensitive information (e.g., API secret keys), you will not want to commit it to Git, so it should be added to the .gitignore file.

    You will use the JWT_SECRET variable later on to configure the Passport.js strategy.

Task 3: Configure the Passport.js JWT Strategy

  1. Before Passport.js can authenticate a request, you must set up the Passport.js authentication middleware and configure Passport.js to use the JWT authentication strategy. Add the following code to src/middleware/auth.js:

    src/middleware/auth.js
    1// import the libraries and classes we need
    2require("dotenv").config(); // loads the environment variables into process.env
    3const passport = require("passport");
    4const { Strategy, ExtractJwt } = require("passport-jwt");
    5
    6const { find: findUser } = require("../services/users");
    7
    8const { JWT_SECRET } = process.env;
    9
    10// Create a new instance of the JWT Passport.js strategy
    11const strategy = new Strategy(
    12 // Pass an object literal as the first argument
    13 // to new Strategy to control how a token is extracted from the request
    14 {
    15 // Extract the 'bearer' token from the authorization header,
    16 // where the encoded JWT string is stored
    17 jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    18 // A string for verifying the token's signature
    19 secretOrKey: JWT_SECRET,
    20 },
    21 async (jwtPayload, done) => {
    22 try {
    23 // jwtPayload contains the decoded JWT payload,
    24 // which includes the user's id
    25 // Find that user in the database
    26 const user = await findUser({ id: jwtPayload.id });
    27
    28 if (!user) {
    29 const err = new Error("User not found");
    30 err.statusCode = 404;
    31 throw err;
    32 }
    33
    34 // done is an error-first callback with signature done(error, user, info)
    35 // pass the found user to the route handler
    36 done(null, user);
    37 } catch (error) {
    38 done(error);
    39 }
    40 }
    41);
    42
    43// Register the strategy configured above so that Passport.js can use it authentication
    44passport.use(strategy);
    45
    46// A middleware for initializing passport
    47const initialize = () => {
    48 return passport.initialize();
    49};
    50
    51// Add this middleware to privileged routes later on
    52const authenticate = () => {
    53 // Since you are using JWTs, you don't need to create a session, so set it to false here.
    54 // Otherwise, Passport.js will attempt to create a session.
    55 return passport.authenticate("jwt", { session: false });
    56};
    57
    58module.exports = {
    59 initialize,
    60 authenticate,
    61};
  2. In src/index.js, import and load the auth middleware when starting the server, like this:

    1const express = require("express");
    2const path = require("path");
    3const cors = require("cors");
    4
    5const recipesRouter = require("./routers/recipes");
    6const { handleError } = require("./utils/error");
    7const auth = require('./middleware/auth.js');
    8
    9const app = express();
    10
    11app.use(cors());
    12
    13app.use((req, res, next) => {
    14 const { method, path } = req;
    15 console.log(
    16 `New request to: ${method} ${path} at ${new Date().toISOString()}`
    17 );
    18 next();
    19});
    20
    21app.use(express.json());
    22app.use(express.urlencoded({ extended: true }));
    23
    24app.use(auth.initialize());
    25
    26app.get("/", (req, res) => {
    27 res.redirect("/api/v1/recipes");
    28});
    29
    30app.use("/api/v1/recipes", recipesRouter);
    31
    32app.use(handleError);
    33
    34const port = process.env.PORT || 8080;
    35
    36app.listen(port, () => {
    37 console.log(`Server is up on port ${port}.`);
    38});

Task 4: Create the users Service

Inside src/services/users, create three functions: authenticate() and create(), which authenticate and create users with JWT authorization tokens respectively, and find() which retrieves a user by id or email:

src/services/users.js
1require("dotenv").config();
2const bcrypt = require("bcrypt");
3const fs = require("fs").promises;
4const jwt = require("jsonwebtoken");
5const path = require("path");
6
7const { JWT_SECRET } = process.env;
8const usersFilePath = path.join(__dirname, "./users.json");
9
10// Authenticate the user and return an authorization token for the user.
11// Use this function to authenticate a user who's logging in.
12const authenticate = async ({ id, email, password }) => {
13 const user = await find({ email });
14 // Hash the user's password and compare the result with the hash
15 // saved in the database to see if the password is correct.
16 const isPasswordValid = await bcrypt.compare(password, user.password);
17
18 // Call jwt.sign(), which returns an authentication token.
19 // The first argument is an object that contains the data to
20 // be embedded in the token. You can pass in a unique identifier for
21 // the user, such as the user's id stored in the database.
22 // The second argument is a string, which could be any random series
23 // of characters used to sign the token to ensure the token has not been tampered with
24 // when it is sent back to the server later on.
25 // The third argument is a configuration object for the token.
26 const token = jwt.sign({ id: user.id }, JWT_SECRET, {
27 expiresIn: 24 * 60 * 60, // Expire tokens after a certain amount of time so users can't stay logged in forever
28 });
29
30 return { token };
31};
32
33// Save the new user to the database and return an authorization token for the user
34const create = async ({ email, name, password }) => {
35 const users = JSON.parse(await fs.readFile(usersFilePath));
36
37 const newUser = {
38 id: users.length + 1, // Not a robust database incrementor; don't use in production
39 email,
40 name,
41 // Here, pass the user's password to bcrypt's hash function to create a hash,
42 // which is stored in the database instead of the user's original password.
43 // Hashing is a one-way operation, so the hash cannot be reversed to its original form.
44 // The first argument is the password to be encrypted, and the second argument
45 // is the number of salt rounds. The higher the number of salt rounds used,
46 // the stronger the resulting hashed password becomes
47 password: await bcrypt.hash(password, 10),
48 };
49
50 // Generate the JWT with jwt.sign. The return value is an
51 // authentication token
52 const token = jwt.sign({ id: newUser.id }, JWT_SECRET, {
53 expiresIn: 24 * 60 * 60, // Expire tokens after a certain amount of time so users can't stay logged in forever
54 });
55
56 users.push(newUser);
57
58 // save the new user to our database
59 await fs.writeFile(usersFilePath, JSON.stringify(users));
60
61 return { token };
62};
63
64const find = async ({ id, email }) => {
65 const users = JSON.parse(await fs.readFile(usersFilePath));
66 return users.find((user) => user.id === parseInt(id) || user.email === email);
67};
68
69module.exports = {
70 authenticate,
71 create,
72 find,
73};

Task 5: Create the users Controller

Imagine if the user is required to provide their login credentials every time they want to access our coveted recipes. This would not be an ideal user experience. Instead, let's have the server issue a JWT to the client when a user signs up for the first time or logs in successfully.

  1. Inside src/controllers/users.js, add route handlers to handle user signup and login:

    src/controllers/users.js
    1const { create, authenticate } = require("../services/users");
    2
    3const handleSignup = async (req, res, next) => {
    4 try {
    5 const { name, email, password } = req.body;
    6 const user = await find({ email });
    7
    8 if (user) {
    9 throw new Error("Email already exists!");
    10 }
    11 // Create a token for the user
    12 const { token } = await create({ name, email, password });
    13
    14 // Send a token to the client when a user signs up
    15 res.json({ token });
    16 } catch (error) {
    17 next(error);
    18 }
    19};
    20
    21const handleLogin = async (req, res, next) => {
    22 try {
    23 const { email, password } = req.body;
    24 const user = await find({ email });
    25
    26 if (!user) {
    27 throw new Error("Unable to login");
    28 }
    29
    30 // Create a token for the user, if successfully authenticated
    31 const { token } = await authenticate({ email, password });
    32 res.json({ token });
    33 } catch (error) {
    34 next(error);
    35 }
    36};
    37
    38module.exports = {
    39 handleSignup,
    40 handleLogin,
    41};

After the user signs up initially or logs in successfully, the server returns a token to the client that the client can attach to any future requests that need to be authenticated, so there is no need for the user to provide their login credentials anymore until the token expires.

Task 6: Create the users Router

  1. Inside src/routers/users.js, route the requests to the appropriate controllers:

    src/routers/users.js
    1const express = require("express");
    2const { handleSignup, handleLogin } = require("../controllers/users");
    3const router = express.Router();
    4
    5router.post("/signup", handleSignup);
    6router.post("/login", handleLogin);
    7
    8module.exports = router;
  2. Inside src/index.js, mount the user authentication /api/v1/users routes, like this:

    1const express = require("express");
    2const path = require("path");
    3const cors = require("cors");
    4
    5const recipesRouter = require("./routers/recipes");
    6const usersRouter = require('./routers/users');
    7const { handleError } = require("./utils/error");
    8const auth = require('./middleware/auth.js');
    9
    10const app = express();
    11
    12app.use(cors());
    13
    14app.use((req, res, next) => {
    15 const { method, path } = req;
    16 console.log(
    17 `New request to: ${method} ${path} at ${new Date().toISOString()}`
    18 );
    19 next();
    20});
    21
    22app.use(express.json());
    23app.use(express.urlencoded({ extended: true }));
    24
    25app.use(auth.initialize());
    26
    27app.get("/", (req, res) => {
    28 res.redirect("/api/v1/recipes");
    29});
    30
    31app.use("/api/v1/recipes", recipesRouter);
    32app.use("/api/v1/users", usersRouter);
    33
    34app.use(handleError);
    35
    36const port = process.env.PORT || 8080;
    37
    38app.listen(port, () => {
    39 console.log(`Server is up on port ${port}.`);
    40});

Task 7 (CHALLENGE): Secure Routes for the recipes Resource

  1. You can now make use of the authenticate() middleware to restrict access to privileged API operations. For example, to allow only authenticated users to create and save a new recipe to the database, you can pass auth.authenticate() as the first argument to the POST handler:

    src/routers/recipes.js
    1+ const auth = require('../middleware/auth');
    2...
    3- router.route('/').get(getAll).post(save);
    4+ router.route('/').get(getAll).post(auth.authenticate(), save);

    CHALLENGE: Restrict any PUT and DELETE routes to authenticated users only.

Task 8: Test Privileged Routes with Postman

Test your API in Postman to ensure only authorized users can perform mutative database operations!

  1. Send a request to the POST /api/v1/recipes route (remember to set the Content-Type in the Headers tab to application/json):

    POST request

    You should see an Unauthorized message if you tried to send the request above.

    Because POST /api/v1/recipes is now a protected route, you'd have to sign up for an account first before you can use this route!

  2. Send a request to the POST /api/v1/users/signup route (remember to set the Content-Type in the Headers tab to application/json):

    POST signup

    In the image above, you can see an access token is included in the response. That's our JWT!

    1{
    2 "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiaWF0IjoxNTk0NTIxMDIwLCJleHAiOjE1OTQ2MDc0MjB9.pcdjNFwezYNdZTYjwWndGowLDkhjcl-debci5BLS8AY"
    3}

    If you look carefully, you can see that a JWT is made up of three distinct parts separated by a period (.).

    • The first part of the JWT (i.e., eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9) represents the header, which is a base64-encoded JSON string. The string contains information about the type of token it is (i.e., JWT) and the algorithm that was used to generate it (i.e., HS256)

    • The second part of the JWT (i.e., eyJpZCI6MiwiaWF0IjoxNTk0NTIxMDIwLCJleHAiOjE1OTQ2MDc0MjB9) represents the payload or claims, which is also a base64-encoded JSON string. The string contains the data that you provided to create the token (i.e., user.id, expiry time)

    • The third part of the JWT (i.e., pcdjNFwezYNdZTYjwWndGowLDkhjcl-debci5BLS8AY) represents the signature, which is used to securely validate the token and is calculated using the encoded header, encoded payload, secret, and the algorithm specified in the header; this signature can only be created by somebody in possession of all four pieces of information.

    Head over to base64decode.org to decode the token you received from the server:

    JWT Decoded

    As you can see, a JWT is not encrypted, so anybody who steals the token can still read its contents, so be careful not to put sensitive information in the payload.

    It's hard to tamper with a JWT, as long as the secret used for the signature is kept private. You use the exact same secret that the token was created with to validate it.

    Check the db/users.json file. You should see your data, including the hashed password, in the file.

  3. Send a request to the POST /api/v1/recipes route again, this time attaching the token to the Authorization headers as a Bearer Token, like this:

    POST authorized

    You should see a 201 Created message indicating successful recipe creation!

  4. Test the PUT and DELETE routes, to make sure that you are able to perform these operations as long as your request contains the token.

BONUS: Logout

A detailed discussion of token invalidation is outside the scope of this course. Take a look at this article if you're interested in learning more!

Additional Resources:

Review

Take a moment to reflect on what you’ve learned in this tutorial and answer the following questions:

  • What is Passport.js?

  • What is a JWT? Why are JWTs useful for implementing RESTful architectures?

  • How can you secure user passwords that are stored in the database?

Key takeaways

  • Passport.js is an authentication middleware for Node.js. It offers a wide variety of authentication mechanisms, known as "strategies", as individually-packaged modules.

  • In REST architectures, client-server interactions are typically stateless. The server does not store any history or state about the client session. JWT (JSON Web Tokens) is a lightweight and secure approach to transferring state from the client to the server in a REST framework. JWT (pronounced "JOT") relies on the exchange of encoded and crytopgraphically signed JSON strings between client and server.

  • A user's password is typically hashed (using a library like bcrypt) before being stored in the database, for security reasons.

What’s next?

In Part 6, you’ll learn how to build a live game using Express and Socket.io.

Continue to Part 6

Want more content like this? Subscribe to get the latest updates in your inbox

Share your feedback

What did you like or didn't like about this post? Let me know what worked well and what can be improved. Your feedback is much appreciated!