HomeBlogLearn

The Only PassportJS Tutorial you will ever need

A Comprehensive PassportJS Tutorial

PassportJS is difficult to understand at first. The documentation isn't the best, and it's hard to know what's what, and you'll see a variety of tutorials doing things different ways. In this tutorial we're going to break everything down and go step-by-step. We'll also look at a variety of ways to do the same thing so you understand exactly what's happening in your Passport.js code.

If you're looking for another way to authenticate, be sure to check out our Authentication Tutorial Master page where we document other ways to authenticate and authorize an application.

Source Code for Passport.js Tutorials

Sessions Nodejs Tutorial

PassportJS Sessions Tutorial Simple

PassportJS Sessions Custom Callback

JWT Basics (Without Passport.js)

Passport JWT Tutorial

What we're going to cover:

Source Code for Completed Passport JS Tutorial

  1. How Sessions work (by themselves)
  2. The entire flow of Passport with Sessions using fake users.
    1. The difference between built in verification and custom callbacks
    2. Failure & success redirects, and when to use them.
    3. How the callback object works & what to pass in
    4. How to access optional messages you pass into the info object
  3. What a Json Web Token is
    1. How to integrate JWT into passportjs
    2. Why we sometimes need TWO passport strategies.
  4. And maybe some other stuff? Let's get to it.

Prerequisites for this Tutorial:

  • Familiarity with JavaScript
  • A little familiarity with Node.js

How Sessions Work:

What is a Session?

Sessions are server-side files that contain user data. Cookies are pieces of data that store a session ID which identifies a particular session. a Session ends on the lifetime set by the user. Think of a session as a "trip to the club" You get to the door and "log in". In the club example you'd get checked by the bouncer and pay the fee. You go in and dance for a bit, and then leave. Some clubs will give you a stamp on your hand so you can come back in. Similar to how you can leave a website, come back a few hours later and hey you're still logged in!

Before we start looking at Passport Authentication, it's important to understand Sessions. We could skip it and go straight to JWT, but future tutorials/docs may use Sessions so it's important to understand how they work.

Starter Code for our Sessions Project:

Create a sessionsTutorial folder and create a server/ folder inside.

run npm init -y inside server/ to create the package.json file. To avoid having to restart the app on every change we'll use nodemon, which you can install globally with:

npm install -g nodemon

Create a /server/app.js file with the following code in it:

app.js

const express = require("express");
const { v4: uuidv4 } = require("uuid");
const session = require("express-session");
const FileStore = require("session-file-store")(session);

const app = express();

// app.use gets run on every request.
app.use(
  session({
    genid: (req) => {
      console.log("1. in genid req.sessionID: ", req.sessionID);
      return uuidv4();
    },
    // where we store the session data. Normally a Database like MongoDB or PostGreSQL
    // session-file-store package defaults to ./sessions
    store: new FileStore(),
    // think of the secret as a "missing puzzle piece".
    secret: "a private key",
    resave: false,
    saveUninitialized: true,
  })
);

app.get("/", (req, res) => {
  console.log("get / req.sessionId: ", req.sessionID);
  res.send("get index route. /");
});

app.listen(3000, () => {
  console.log("listening on localhost:3000");
});

Next we need to install all the dependencies to run the program. cd into /server and run:

npm install express uuid express-session session-file-store

Now run app.js with nodemon app.js

A sessions/ folder should have been created automatically for you thanks to the session-file-store package. Open it up and you'll see that it's empty.

New Sessions

Now, open a different browser than the one you're using and visit http://localhost:3000

Now look in the sessions/ folder and you'll see a json file with a long name, which happens to be your session data. Mine had this name:

da7d6689-27b8-4f86-bf5c-dc95bdd16371.json

Because a "client" (Your web browser) made a request to the index route (/) on the server, the server created a new "session" and stored it in the sessions/ folder. Now close the web browser you used, and re-open it and re-visit localhost:3000. Now you'll see ANOTHER session file. Mine looks like this:

f3dc30a3-a594-4e7b-a7ee-e89179347b8b.json

During this "session", the express-session package will tack on the current "sessionID" to the req object on every request. If the client disconnects from the server and reconnects, it'll get a new session, unless the client has a cookie that matches a current valid session.

Remembering Login / Session Info

So how do websites remember that you're logged in? With Cookies! The Cookie will have your session info stored somewhere in the web browser or client you're using. We will use cURL to demonstrate a browser "remembering" a user.

You may have to do a little work on your own to get cURL installed and make sure its in your path so you can run curl. I'll hopefully have a tutorial on that soon. Until then you're on your own with that part. Luckily I have the commands written in windows and Unix (Mac / Linux) systems.

First, close out of the localhost:3000 in your browser and delete all the sessions in your sessions/ folder.

Now open a new terminal window and CD into the sessionsTutorial/ and run the following command with cURL.

Windows: curl.exe -X GET http://localhost:3000 -c my-cookie.txt

*Nix: curl -X GET http://localhost:3000 -c my-cookie.txt

You'll notice a new my-cookie.txt file with a connect.sid and some long string after it. This is a cookie with the session data inside! We can now use this to remember our session.

the -c flag "created / overwrote" the my-cookie.txt file. Next we're going to use the -b flag to use the existing contents of the my-cookie.txt file to say "Hey server, we were already here. Let's pick up where we left off!"

Run the following command with cURL:

Windows: curl.exe -X GET http://localhost:3000 -b my-cookie.txt

*Nix: curl -X GET http://localhost:3000 -b my-cookie.txt

We hit the index route on the server, but we didn't get a new session because the session ID in our cookie (in my-cookie.txt) matched the Session in our sessions/ folder, and the session is still valid. Run the command with the -b flag 3 more times or 50 more times, and we'll still be using the same session, so no more .json files are created in the sessions/ folder.

Expired Sessions, Invalid Cookies, etc.

Let's simulate an expired or invalid session or cookie by slightly renaming the json file for the session we've been using in our sessions/ folder.

Once the file is renamed, run this command again:

Windows: curl.exe -X GET http://localhost:3000 -b my-cookie.txt

*Nix: curl -X GET http://localhost:3000 -b my-cookie.txt

We get a new session because our old session didn't work for whatever reason. Try it 3 more times, attempting to use the same cookie (which doesn't match any valid sessions) and we'll get 3 more new sessions because the cookie didn't match, so the server created a new session. We could also have changed a character in our sid (session ID) in the my-cookie.txt, and that would also invalidate the session and create a new one.

Storing information in cookie / session

This "session" is great, but how does it actually identify you specifically? Sessions are able to identify you by storing data inside of them. Sometimes you'll store just a user ID in the session, which is used to query the database for the user. Other times you'll store the whole user in the session. Let's give it a try.

Delete all the existing / old sessions so we can start clean.

Create a users array in the global scope of your app. Under const app = express() is a good spot. Then inside the index route we'll add a console log to print the current req.session.user, and we'll add a user to the req.session object.

app.js

const app = express();

const users = [{ id: 1, email: "bob@bob.com" }];

//....

app.get("/", (req, res) => {
  console.log("get / req.sessionId: ", req.sessionID);
  console.log("req.session.user: ", req.session.user);
  req.session.user = users[0];
  res.send("get index route. /");
});

Now, run the cURL command with the -c flag to begin a new session and overwrite your my-cookie file.

Windows: curl.exe -X GET http://localhost:3000 -c my-cookie.txt

*Nix: curl -X GET http://localhost:3000 -c my-cookie.txt

The first time we hit the index route, it prints out an undefined req.session.user

But we have a session in our sessions folder, and we stored a user into the current session with the following code:

req.session.user= users[0];

Go look inside the session json file in the sessions/ folder and you'll see your user object in the session:

Assuming we had a secure auth system, we could use the session data to tailor the app to the currently logged in user. run the cURL command again with the -b flag and you'll see the console log prints the user this time.

Windows: curl.exe -X GET http://localhost:3000 -b my-cookie.txt

*Nix: curl -X GET http://localhost:3000 -b my-cookie.txt

My console log printed this:

req.session.user: { id: 1, email: 'bob@bob.com' }

You should also take note that the genid function only gets run when it needs to create a new session. If it already has a valid session matching a valid cookie, then it uses the existing session and there's no need to call genid.

Note: Notice the my-cookie.txt file's connect.sid matches the session's filename if you ignore the first few characters in the connect.sid.

How do Sessions work in a browser?

In a browser we typically store cookies in localStorage or somewhere similar, and then send those in the HEADERs of a request. The cookie has the session ID in it. A browser and cURL both send the session ID along with the request, they just do it in different ways.

If you want to see this in more detail, try running the curl request with a -v at the end.

Windows: curl.exe -X GET http://localhost:3000 -b my-cookie.txt -v

*Nix: curl -X GET http://localhost:3000 -b my-cookie.txt -v

In the terminal window where you ran the cURL request, you should see something like this:

curl output

> User-Agent: curl/7.83.1
> Accept: */*
> Cookie: connect.sid=s%3Abd744d52-1e78-4bd6-8027-0cd8c52e5017.R2FDfeY%2FAG2fcg9pmDQA%2B28vjFMmovnpvC%2FkqK4kpUM

Then the server checks to see if the session ID matches a valid session. If it does, we use the valid session. If not, the server creates a new session.

Here's the full source code so far for our sessions project WITHOUT any Passport code:

app.js

const express = require("express");
const { v4: uuidv4 } = require("uuid");
const session = require("express-session");
const FileStore = require("session-file-store")(session);

const app = express();

const users = [{ id: 1, email: "bob@bob.com" }];

// app.use gets run on every request.
app.use(
  session({
    genid: (req) => {
      console.log("1. in genid req.sessionID: ", req.sessionID);
      return uuidv4();
    },
    // where we store the session data. Normally a Database like MongoDB or PostGreSQL
    // session-file-store package defaults to ./sessions
    store: new FileStore(),
    // think of the secret as a "missing puzzle piece".
    secret: "a private key",
    resave: false,
    saveUninitialized: true,
  })
);

app.get("/", (req, res) => {
  console.log("get / req.sessionId: ", req.sessionID);
  console.log("req.session.user: ", req.session.user);
  req.session.user = users[0];
  res.send("get index route. /");
});

app.listen(3000, () => {
  console.log("listening on localhost:3000");
});

Passport.JS Authentication with Sessions

Now that we understand how sessions work it will be MUCH easier to wrap our heads around how Passport.js helps us authenticate our applications. So let's begin the Passport.js tutorial section now.

High Level look at a login flow

Before we start writing code, let's get a high level overview of how logging in and out of an application works. Step by step, here's one way logins work:

  1. Visit a login page
  2. Enter email and password
  3. click submit
  4. Client sends the server the email & password.
  5. Server runs the /login endpoint with the email & password
  6. Server finds the user in the database by the provided email
    1. If the user is not found, end the login flow.
  7. Convert the entered password to a jumbled string with whatever hashing tool was used to store the password
  8. compare the jumbled password with the password in the database.
  9. Assuming the passwords match, create a session with user data in it, and store the session somewhere in the DB.
  10. Create a cookie in the client with the session ID.

After all that, the user is logged in. Every request has the cookie in the request header, so the server can find the matching session and treat the user like they're logged in. Let's write some code and it'll hopefully make more sense.

Passport JS Tutorial Setup:

I'm going to show you both the default setup of Passport Auth, as well as using custom callbacks to make a more flexible login system in your express app.

First, duplicate the current sessionsTutorial folder and rename one of them passport-js-tutorial.

Next, change into passport-js-tutorial/server/ and install all the packages we need for the project.

npm install ejs express express-session passport passport-local session-file-store uuid passport-jwt jsonwebtoken bcrypt

Okay, we're ready to begin. If you get stuck check out the complete source code.

Using the Passport.js Defaults:

Passport.js provides some "default" options that you can use for simple authentication needs. We're going to look at this simple setup first because you're going to see it in other express login tutorials around the web.

The next thing we'll do is setup a simple Express POST route and a the Express View engine EJS so we can have a simple login page to look at. Add a views/login.ejs file in the server folder, and change the app.js file to have the below code:

login.ejs

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Passport.js Tutorial</title>
  </head>
  <body>
    <h1>Login</h1>
    <form action="/login" method="post">
      <!-- You might get a strange error if you don't fill in the input fields so we're hard-coding and requiring the values. -->
      <!-- I honestly don't know what the problem is. Something to do with passport needing a "messages" property -->
      <!-- Just make a proper html form and it should hopefully work. -->
      <input
        id="username"
        name="username"
        type="text"
        value="username"
        required
      />
      <input
        id="password"
        name="password"
        type="text"
        value="password"
        required
      />
      <button type="submit">Login</button>
    </form>
  </body>
</html>

app.js

const express = require("express");
const { v4: uuidv4 } = require("uuid");
const session = require("express-session");
const FileStore = require("session-file-store")(session);
const path = require("path");
const bodyParser = require("body-parser");

const app = express();

app.set("views", path.join(__dirname, "views"));
app.set("view engine", "ejs");
app.use(bodyParser.urlencoded({ extended: false }));

app.use(
  session({
    genid: (req) => {
      return uuidv4();
    },
    store: new FileStore(),
    secret: "a private key",
    resave: false,
    saveUninitialized: true,
  })
);

app.get("/", (req, res) => {
  res.send("Nothing to see here. go to /login");
});

app.get("/login", (req, res) => {
  res.render("login");
});

app.post("/login", (req, res) => {
  res.send("login form submitted!");
});

app.listen(3000, () => {
  console.log("listening on localhost:3000");
});

Now we have a simple login form and a POST route to handle login form submission. Visit localhost:3000/login, fill out the form, and hit submit. After hitting "submit", the browser should display "login form submitted!".

So far, this has all been very basic Node.js and Express code. If you're having a hard time following along, I recommend checking out some of our Node.js and Express.js tutorials & Courses.

Redirecting & Routing in Passport

Now we'll setup our first "Login Flow" using Passport.js. Add the following code to your app.js file:

app.js

const localStrategy = require("passport-local").Strategy;
const passport = require("passport");

//...

// app.use(...)

passport.use(
  new localStrategy((username, password, done) => {
    if (username == "fail") {
      return done(null, false);
    } else {
      return done(null, { id: "123", email: username });
    }
  })
);

app.get("/failed", (req, res, next) => {
  res.send("failed login!");
});

app.get("/success", (req, res, next) => {
  res.send("login success!");
});

app.post(
  "/login",
  function (req, res, next) {
    console.log("useless function");
    next();
  },
  passport.authenticate("local", {
    session: false,
    failureRedirect: "/failed",
    successRedirect: "/success",
  })
);

Now visit the /login route and enter into the username field anything but the word "fail", and hit submit.

The first thing that happens is the client (web browser) makes a request to the server's POST /login endpoint. That triggers a middleware function that just console logs "useless function" and calls next() which runs the next function in the chain, which is the passport.authenticate("local") bit.

The first parameter of passport.authenticate is the name of the block of Passport code to look for. Because we gave it "local", it's going to find the localStrategy without a name.

The second parameter is an object.

session: false turns off sessions so we don't get serialization errors.

failureRedirect and successRedirect are just the routes that passport will send you to upon success or failure of authentication.

Next, the new localStrategy is called in passport.use(). It takes a callback function with the username, password, and a done/next/callback parameters.

Inside this function our job is to determine if the user has given us the correct information. In order to log them in they need to have a valid username and password.

The done() function will always take us to the next step. If successful, pass the user object you want to attach to the req as the second parameter.

If the user did NOT pass, or was not found, or any other user input error, you pass false as the second parameter, and an optional object to provide a message as the third parameter. The third parameter becomes more useful later with custom callbacks.

I entered a valid username, so the else block runs. The done() function received a user object, so we know the user entered valid input and should be logged in.

Passport.js takes care of the rest by redirecting our Node.js app to the successRedirect route, which is /success. If you entered the username "fail", then the failureRedirect route is hit.

The only thing the "strategy" part does is verify the user entered the correct login and password.

Error Handling in Node.js & Passport

If you want to learn more about Error handling in JavaScript & Express/Node, then check out our tutorials on Error handling in JavaScript page.

For Application errors, you'll only need the first parameter of done().

You can do a try catch or whatever you want, but you'll end up passing an Error object as the first parameter, and Passport will take care of the rest.

I still have things to learn about Error handling, but from what I understand, "Application Errors" are generally official "crashes" of the application. Frontends like React have pretty UI's to get the user back on track, but In Express, Node.js, and Passport, there's not an easy way to handle an error with a redirect, so we're just going to accept that for now.

Simply pass an error to done() and Passport takes care of the rest. It doesn't redirect the user. We updated the code so passing in a username of "apperror" will trigger an application crash:

app.js

passport.use(
  new localStrategy((username, password, done) => {
    if (username == "fail") {
      return done(null, false);
    } else if (username === "apperror") {
      let myErr = new Error("something bad happened!");
      done(myErr);
    } else {
      return done(null, { id: "123", email: username });
    }
  })
);

Named Passport Functions allow you to trigger different Passport functions at different times. By default, "local" will call an unnamed localStrategy.

app.js

passport.use(
  "login",
  new localStrategy((username, password, done) => {
    if (username == "fail") {
//...

app.post(
  "/login",
  function (req, res, next) {
    console.log("useless function");
    next();
  },
  passport.authenticate("login", {
    session: false,
    failureRedirect: "/failed",
    successRedirect: "/success",
  })
);

But with the code above, now we're explicitly naming the passport login function "login" and to call it we run:

passport.authenticate("login", //...).

Logging in with Custom Fields

By default, Passport FORCES you to use "username" and "password" as the names of the key-value or name-value pairs. Whether you use cURL, PostMan, or an HTML Form, Passport will not give you the expected results if you try to send it "email" when it expects a "username". To use your own, we can specify like this:

app.js

passport.use(
  "login",
  new localStrategy(
    { usernameField: "email", passwordField: "password" },
    (username, password, done) => {
      if (username == "fail") {

Try to submit the form and you'll get a failureRedirect, without any indication of why. This can be a frustrating bug, so if you aren't making it to the localStrategy code, it's a sign to check your form input names. Let's fix this and use email from now on. I'm going to create a NEW .ejs file so we can keep the old one around for testing.

duplicate your existing /views/login.ejs and rename it /views/emailpasslogin.ejs

It should look exactly the same as the original .ejs file, with the exception of the changes below. While we're at it, we'll change the login route to render the new login template file:

emailpasslogin.ejs

<input id="email" name="email" type="text" value="email" required />
<input
  id="password"
  name="password"
  type="text"
  value="password"
  required
/>

app.js

// make sure you render the new .ejs file
app.get("/login", (req, res) => {
  res.render("emailpasslogin");
});

Save the code and submit the form again, and the login functionality should be working again. You can give the usernameField a value of anything you want, as long as it has a corresponding name= attribute in the html.

Submit the login form again and your Passport Authentication app should be back in business.

Serializing & Deserializing & Sessions

Up until now we've been going through the login flow, but we're not actually logging in. We need a way to store the logged in data persistently. We'll do that with Sessions and JWT in this tutorial. Update your app.js code to the below code. Also we'll update the package.json file's script.

app.js

//...app.use(
session({
    //...
    saveUninitialized: false,
  })
);

app.use(passport.initialize());
app.use(passport.session());

passport.serializeUser((user, done) => {
  console.log("in serialize. user: ", user);
  done(null, user);
});

passport.deserializeUser((user, done) => {
  console.log("in deserialize. user: ", user);
  done(null, user);
});

//... passport.use("login"...

//...

app.get("/", (req, res) => {
  console.log("req.user: ", req.user);
  console.log("req.login: ", req.login);
  console.log("req.logout:", req.logout);
  console.log("req.isAuthenticated: ", req.isAuthenticated());
  res.send("Nothing to see here. go to /login");
});

//...
app.get("/success", (req, res, next) => {
  console.log("req.user: ", req.user);
  console.log("req.login: ", req.login);
  console.log("req.logout:", req.logout);
  console.log("req.isAuthenticated: ", req.isAuthenticated());
  res.send("login success!");
});

//...
passport.authenticate("login", {
    // session: false
    failureRedirect: "/failed",
    successRedirect: "/success",
  })

package.json

//... rest of the code above
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "nodemon --ignore sessions/ app.js"
  },

//... rest of code below

we turn saveUninitialized to false because having it set to "true" would create a new session any time the user logs in, if they don't already have one. Setting it to false makes it a bit easier to learn because the session is only created when the user officially makes an attempt to log in or do something that would "save" a session.

app.use(passport.initialize()); is some mysterious "setup" code that passport apparently needs.

app.use(passport.session()); tells the app that we're using sessions and sets up passport accordingly. I'm not sure the details, but if you're using sessions, add this line.

passport.serializeUser() is code that takes an object and 'serializes' it (basically turns it into a string) and stores the user object into the session.

passport.deserializeUser is run pretty much on every request to the server. When you're authenticated, every request will look to see if the user exists in the session and "deserialize" it back into an object, and make it available in the req.user object.

The done() inside the serializers is how you pass the user back to the rest of the app. The second parameter is where the user goes, and I'm going to guess that the first parameter is where an error object would be placed.

The app.get("/") and app.post("/") routes have some console.logs to demonstrate that Passport has done some nice things for us. Passport stores the user object on the request, as well as a login, logout, and isAuthenticated function that we can take advantage of.

We also want to get rid of that sessions: false bit because we are using sessions to authenticate right now.

Then in the package.json file we're adding a flag to ignore changes to the sessions/ folder. We don't want the app to restart every time a new session starts. Submit the login form, and try visiting the routes with the console.log() commands. Also visit the session file, and you'll see the user in a Passport object. Mine looks like this:

ac2xxxx.json

{
  "cookie": {
    "originalMaxAge": null,
    "expires": null,
    "httpOnly": true,
    "path": "/"
  },
  "passport": { "user": { "id": "123", "email": "snowshredder101" } },
  "__lastAccess": 1667279101908
}

You're successfully authenticated and you have a persistent session. Kill the server and restart it. visit the "/" or "/login" route and you'll see the previous user still being logged in the terminal.

Let's kill the server and restart with npm run dev to apply the --ignore sessions/

Redirecting with Failure & Success Messages

You can redirect to the failureRedirect and successRedirect routes with custom messages, but it's a bit of a pain with passport's built in settings. This is where you start realizing the need for another way, but let's give it a try. Make the following changes:

app.js

// in passport.use("login"...
      if (username == "fail") {
        return done(null, false, { message: "Invalid credentials!" });
      } else if (username === "apperror") {
        let myErr = new Error("something bad happened!");
        done(myErr);
      } else {
        return done(
          null,
          { id: "123", email: username },
          { message: "You logged in!" } // this doesn't seem to work.
        );
      }

// cleaned up the / route
app.get("/", (req, res) => {
  res.send("Nothing to see here. go to /login");
});

app.get("/failed", (req, res, next) => {
  console.log("req.session: ", req.session);
  res.send("failed login!");
});

app.get("/success", (req, res, next) => {
  console.log("req.query: ", req.query);
  res.send("login success!");
});

// in app.post(...
passport.authenticate("login", {
    // session: false
    failureRedirect: "/failed",
    successRedirect: `/success?message=success!%20You%20logged%20in!`,
    failureMessage: true,
    successMessage: true, // I can't get it to work reliably
  })

The first change is making use of the "info" or third parameter. We can access the messages in an array in the sessions, but it doesn't work reliably, especially with the success route. I've noticed the array doesn't update after two or so messages are added.

I removed the console.logs from the / route

in the app.get("/failed" we logged the req.session to see that message. You can now use that in the /failed route to render the message on the page.

in the app.get("/success" we logged a req.query. I couldn't get the success message to work reliably, so we're doing a bit of a hack with the query parameters.

We also added failureMessage: true and successMessage: true in the object in the POST /login route to indicate that passport should expect custom message objects.

the successRedirect property has a query parameter at the end of it because if the user reaches that page, they've successfully logged in, and we can take the query parameter and display it to the users.

Delete the sessions/ files and restart the server. Now submit the form twice to hit the failure route and the success route. Check the logs and you should see we have our "custom messages" that we can display to the user in both success and failure cases.

Terminal log on failure and success:

Log on Failure

req.session:  Session {
  cookie: { path: '/', _expires: null, originalMaxAge: null, httpOnly: true },
  messages: [ 'Invalid credentials!' ],
  __lastAccess: 1667319101273
}

Log on Success

req.query:  { message: 'success! You logged in!' }

Signing Up Users

Signing up users in an Express.js and Passport application is ALMOST the same as our login. The only extra piece is saving the user to a database.

Let's add a signup view for our signup form, a signup route, and adjust our /success and /failed routes to be reused:

server/views/signup.ejs

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Passport.js Tutorial</title>
  </head>
  <body>
    <h1>Signup</h1>
    <form action="/Signup" method="post">
      <input id="email" name="email" type="text" value="yoyoyo" required />
      <input
        id="password"
        name="password"
        type="text"
        value="password"
        required
      />
      <button type="submit">Sign Up</button>
    </form>
  </body>
</html>

app.js

app.get("/signup", (req, res) => {
  res.render("signup");
});

app.get("/failed", (req, res, next) => {
  console.log("req.session: ", req.session);
  res.send("failed!");
});

app.get("/success", (req, res, next) => {
  console.log("req.query: ", req.query);
  res.send("You're in!");
});

Save the files and we now have a signup form we can send a POST request to. We need to create another passport.authenticate function for our signup route, which will get called anytime the 'signup' endpoint is triggered:

app.js

// under app.post("/login"...

app.post(
  "/signup",
  passport.authenticate("signup", {
    failureRedirect: "/failed",
    successRedirect: `/success?message=success!%20You%20signed%20up!`,
    failureMessage: true,
  })
);

Our express app is trying to run code that doesn't exist. There is no "signup" passport function. We need to create it just like we created our login function.

app.js

const users = require("./users.json");
const bcrypt = require("bcrypt");
const fs = require("fs");

// under passport.use("login", ...)

passport.use(
  "signup",
  new localStrategy(
    { usernameField: "email", passwordField: "password" },
    async (email, password, done) => {
      try {
        if (password.length <= 4 || !email) {
          done(null, false, {
            message: "Your credentials do not match our criteria..",
          });
        } else {
          const hashedPass = await bcrypt.hash(password, 10);
          let newUser = { email, password: hashedPass, id: uuidv4() };
          users.push(newUser);
          await fs.writeFile("users.json", JSON.stringify(users), (err) => {
            if (err) return done(err); // or throw err;
            console.log("updated the fake database");
          });

          done(null, newUser);
        }
      } catch (err) {
        return done(err);
      }
    }
  )
);

This is mostly the same stuff we did with our login. The only notable difference is we put the users password into a bcrypt hashing function which spits out a jumbled string that represents the password.

We also put the keyword async in front of the callback function so we can wait for bcrypt to finish.

We also save the user to a json file which acts as our database, then call the done() function to be redirected to the success or failure page.

Now we need to create a users.json file with an empty array in it.

/server/users.json

[]

Test Your Signup Feature

Just to be safe, delete the sessions files and restart the server. Then Visit localhost:3000/signup and submit the form with a password LESS than 4 characters to see the failure message in the terminal log. Check the session and the file should have the message.

Now try a successful signup. You should get the message in the req.query object and be directed to the success page.

Fixing Our Login Feature

Up to now our login has been hardcoded data. Let's fix it to work with the users in our database and require a correct password.

app.js

passport.use(
  "login",
  new localStrategy(
    { usernameField: "email", passwordField: "password" },
    async (email, password, done) => {
      try {
        const user = users.find((user) => user.email === email);

        if (!user) {
          return done(null, false, { message: "User not found!!" });
        }

        const passwordMatches = await bcrypt.compare(password, user.password);

        if (!passwordMatches) {
          return done(null, false, { message: "Invalid credentials!" });
        }

        return done(null, user);
      } catch (error) {
        return done(error);
      }
    }
  )
);

We made our callback function async, moved the code into a try catch block just to see a different way, (not necessary) and since we're using bcrypt to store passwords, we need to pass the password the user entered into bcrypt so it can be spit out as a long jumbled string. If the long jumbled string matches the long jumbled string in the database, then the user can log in.

We don't actually spit out the long jumbled string. bcrypt.compare does this comparison for us and returns a boolean.

If the password matches, we log them in and passport handles the redirection for us.

Secure Routes & Logging Out

Now that we have a logged in user, they should be able to access authenticated routes. We should also be able to restrict these routes so logged out users are redirected away. And obviously we need a way to log out users. Let's do that with the code below:

app.js

app.get("/", (req, res) => {
  console.log("req.user", req.user);
  console.log("req.isAuthenticated: ", req.isAuthenticated());
  res.send("Nothing to see here. go to /login");
});

app.get("/secureroute", (req, res) => {
  if (req.isAuthenticated()) {
    res.send(`welcome to the top secret place ${req.user.email}`);
  } else {
    res.send("Must log in first. visit /login");
  }
});

app.get("/logout", (req, res) => {
  req.logout(function (err) {
    if (err) {
      return next(err);
    }
    res.redirect("/");
  });
});

In the /secureroute endpoint we're using the isAuthenticated() function to check if the user is authenticated, and routing the app to a page based on their logged in status. This isAuthenticated() function is given to us by Passport.

the logout endpoint calls the req.logout() function, also given to us by Passport.js. It takes a callback, where we can route the user or throw an error.

Congrats! You have a full working authentication system with Passport.js and Express. But we can actually make this code better with Custom Callbacks.

Custom Callbacks in Passport.js

If you want more fine-grain control over the authentication flow, I recommend you use the custom callback setup instead. With custom callbacks you run the authentication inside the function in the route, and you end up with access to the req, res, and next object.

I'm going to duplicate the project so we can look at both versions easily. I've renamed the copy "custom-callback-version".

app.js

// in "login" localStrategy
        if (!passwordMatches) {
          return done(null, false, { message: "Invalid credentials!" });
        }

        return done(null, user, { message: "You Logged in!" });

//...

app.get("/success", (req, res, next) => {
  console.log("req.query: ", req.query);
  res.send(`You're in! ${req.query.message}`);
});

app.post("/login", async (req, res, next) => {
  console.log("passport.authenticate is inside a function body now.");

  passport.authenticate("login", async (err, user, info) => {
    console.log("err: ", err, "user: ", user, "info: ", info);
    req.login(user, async (error) => {
      return res.redirect(`/success?message=${info.message}`);
    });
  })(req, res, next);
});

Before, we had passport.authenticate do the redirections and logging in for us. Now we have to do that ourselves.

The failureRedirect and other options are now disabled so we can get rid of that object.

This new function body with the req.login() function in it is what passport use to do for us, but now we have to manage it. It gives us the 3 parameters we pass from the Passport.js "login" code.

We now have to call req.login() and handle the redirection ourselves. Or we can call next() and continue calling other functions. You have a lot more flexibility with custom callbacks.

req.login() takes a callback function where we can just redirect after the user is done logging in.

Finishing up the Sessions Auth

Let's finish converting the login and signup functions to the custom callback version. There's nothing new in the code below. We're passing the object from our localStrategy to the custom callback now, so we have to handle the logins and redirects for the signup process. I also added a flag to ignore the users.json file in the package.json.

app.js

// right after the await fs.writeFile, we're just adding the message object.

return done(null, newUser, { message: "signed up msg" });

//...
app.get("/failed", (req, res, next) => {
  console.log("req.session: ", req.session);
  res.send(`failed! ${req.query?.message}`);
});

//...

  passport.authenticate("login", async (err, user, info) => {
    if (err) {
      return next(err);
    }
    if (!user) {
      return res.redirect(`/failed?message=${info.message}`);
    }

//...

app.post("/signup", async function (req, res, next) {
  passport.authenticate("signup", async function (err, user, info) {
    if (err) {
      return next(err);
    }

    if (!user) {
      return res.redirect(`/failed?message=${info.message}`);
    }

    req.login(user, async (error) => {
      if (error) {
        return next(err);
      }
      return res.redirect(`/success?message=${info.message}`);
    }); // logging them in is optional.
  })(req, res, next);
});

package.json

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "nodemon --ignore sessions/ --ignore users.json app.js"
  },

And that's it for Session based Authentication with Passport.js and Node.js

We're going to quickly cover Json Web Tokens now and how they work with Passport.

A Brief Intro to JWT Json Web Tokens

Let's duplicate the finished project, and rename the copy to jwt-basics.

Our entire app will only have the following code:

fakeLocal.json

{}

app.js

const express = require("express");
const bodyParser = require("body-parser");
const path = require("path");
const app = express();
const fakeLocal = require("./fakeLocal.json");
const jwt = require("jsonwebtoken");
const fs = require("fs");

app.set("views", path.join(__dirname, "views"));
app.set("view engine", "ejs");

app.use(bodyParser.urlencoded({ extended: false }));

app.get("/", (req, res) => {
  res.send("nothin to see here. visit /createtoken to create your token");
});

app.get("/createtoken", async (req, res) => {
  let user = { name: "Joey", favColor: "blue", id: "123" };

  const token = jwt.sign({ user: user }, "TOP_SECRET_KEY");

  console.log("token is: ", token);

  await fs.writeFile(
    "fakeLocal.json",
    JSON.stringify({ Authorization: `Bearer ${token}` }),
    (err) => {
      if (err) throw err;
      console.log("updated the fake localstorage in the fake browser.");
    }
  );

  res.send(
    "You made a token and stored it in a json file. now visit /profile and /wrongsecret"
  );
});

app.get("/profile", async (req, res) => {
  console.log("authorization token: ", fakeLocal.Authorization);

  const result = await jwt.verify(
    fakeLocal.Authorization.substring(7),
    "TOP_SECRET_KEY"
  );
  result.message =
    "We were able to decrypt the token because we have a valid secret in the app, and the token. The users data is inside the token";

  console.log("result: ", result);

  res.json(result);
});

app.get("/wrongsecret", async (req, res, next) => {
  // console.log("authorization token: ", fakeLocal.Authorization);
  try {
    const result = await jwt.verify(
      fakeLocal.Authorization.substring(7),
      "INCORRECT_SECRET"
    );

    res.send("/profile");
  } catch (err) {
    return res
      .status(400)
      .send(
        "you failed to hack me because you do not have the secret key, so the token could not be decrypted "
      );
    // next(err.message);
  }
});

app.listen(3000, () => {
  console.log("listening login");
});

JSON web tokens simply store a users data in an encrypted or unreadable format. That jumbled string which is the token actually has valuable information like the user ID or user email or whatever you put inside it. There's some fancy algorithm that jumbles all the data using a "secret key". So if you don't have the key, or have an incorrect key, the data will be jumbled differently, and come out as invalid. The only way to get the correct data is to have the correct key.

Let's look at the code above.

First, look at the createtoken route. We create a user object, and then we create a token with the jwt.sign() function, passing in our user object and a string "TOP_SECRET_KEY" If you want the user to be able to login across multiple apps or devices, then each of those codebases need access to this top secret key.

Next, we log the JWT token so we can see it's all jumbled. If you were to have an authentication system, you'd send this token to the client, and then the client would store it somewhere safe. On every request to the server, the client would pass along the token in the HEADER of the HTTP request.

The server would see the token, decrypt it, and see the user is valid and logged in. The server would be able to use the data inside the token to get the users profile, and other logged in stuff.

We don't have a real client, so we fake it by creating a "fakeLocal" file and writing the JWT token to the file.

Next we can visit /profile and see that with the correct secret key we can decrypt the data.

Try visiting the /wrongsecret route, and since for whatever reason the token is invalid, we don't get the user data.

Passport JWT Tutorial

Before we get started, let's look at JWT authentication with an analogy:

JWT Analogy / Example:

The Local Strategy is like verifying the user has permissions to create a key. Say you go to the home improvement store and ask them to create you a house key. They might ask for proof you own the house. This is equivalent to entering your email and a matching password.

Next, they actually "Log you in". This is the Custom callback where you were calling req.login(), or in the house example, they'd create a key. This could be the session, or a Json Web Token. They create a key and give it to you. Note: It doesn't seem like req.login is necessary when using JWT. See updates & final code.

Finally, you have to use the key to gain access. This is the JWT Strategy. Sure, you're logged in, but you still have to use your key any time you want to gain access to a room. Just like you have to use your JWT or session every time you request data from the server.

Passport JWT Tutorial Setup

Don't worry, we'll keep this brief. Let's duplicate our custom-callback-version of the project, and we're just going to swap out our current sessions authentication strategy with the JWT authentication strategy. Rename the copy passport-jwt-tutorial

Let's remove the sessions portion of the code and add a few packages, and create a fakeLocal.json file

app.js

// add these:
const JWTstrategy = require("passport-jwt").Strategy;
const jwt = require("jsonwebtoken");
const fakeLocal = require("./fakeLocal.json");

// remove from here
app.use(
  session({
    genid: (req) => {
      return uuidv4();
    },
    store: new FileStore(),
    secret: "a private key",
    resave: false,
    saveUninitialized: false,
  })
);
// to here.

app.use(passport.session()); // remove this

// remove this function
passport.serializeUser((user, done) => {
  console.log("in serialize. user: ", user);
  done(null, user);
});

// remove this function
passport.deserializeUser((user, done) => {
  console.log("in deserialize. user: ", user);
  done(null, user);
});

// Change app.post("/signup"... to this:

app.post("/signup", async function (req, res, next) {
  passport.authenticate("signup", async function (error, user, info) {
    if (error) {
      return next(error);
    }

    if (!user) {
      return res.redirect(`/failed?message=${info.message}`);
    }

    res.send("Didnt log in. Only signed up.");
 
  })(req, res, next);
});

server/fakeLocal.json

{}

After writing the above code, test that you can SIGN UP. You will not be logged in.

We added a few packages that we're going to need soon. Then we removed the app.use() session block because we won't be using sessions anymore. It also appears that we don't need the serializeUser and deserializeUser functions anymore. Some people online claim its optional or used, but I've tested it and they aren't running anymore, so I got rid of them.

Then we adjusted the signup function to only signup the user. I'm not sure if req.login() is even used anymore with the jwt strategy. I've left some notes in the final code, but req.isAuthenticated() and req.user don't work in non-authenticated routes like app.get("/") regardless of whether req.login() is used or not.

Below, we update the post "/signup" endpoint to login the user during the signup process. Notice that we're not using req.login() anymore. It doesn't appear to be necessary.

app.js

app.post("/signup", async function (req, res, next) {
  passport.authenticate("signup", async function (error, user, info) {
    if (error) {
      return next(error);
    }

    if (!user) {
      return res.redirect(`/failed?message=${info.message}`);
    }

    // I don't think req.login() is needed anymore, which is strange because they have an optional object where you can pass { session: false}
    // req.login(user, { session: false }, async function (err) {
    //   if (err) {
    //     return next(err);
    //   }

    const body = { _id: user.id, email: user.email };
    const token = jwt.sign({ user: body }, "TOP_SECRET");

    await fs.writeFile(
      "fakeLocal.json",
      JSON.stringify({ Authorization: `Bearer ${token}` }),
      (err) => {
        if (err) throw err;
      }
    );

    return res.redirect(`/success?message=${info.message}`);
    // }); // closing brackets for req.login() which I don't think we need.
  })(req, res, next);
});

After writing the above code, Try signing up again, and the fakeLocal.json file should be updated with an Authorization bearer token.

Just like with the sessions implementation of Passport, we have to manually log the user in, but we also have to tell the login function HOW to log in with JWT.

We store the users info in a body const, and pass that data into the JWT token with our secret key. Since we don't have a frontend, we don't have a cookie or anything to store the token in. We're going to use the fakeLocal json file and store the token there instead.

Then we redirect and the JWT login was successful! Check to see that the fakeLocal.json file has been populated with an Authorization property.

We're going to do basically the same thing in our passport LOGIN code, so I'm just going to paste it below.

app.js

app.post(
  "/login",
  function (req, res, next) {

    passport.authenticate("login", async (err, user, info) => {
      console.log("err: ", err);
      console.log("user: ", user);
      console.log("info: ", info);

      if (err) {
        return next(err);
      }

      if (!user) {
        return res.redirect(`/failed?message=${info.message}`);
      }

      // It doesn't seem like the req.login() does anything for us when using JWT.
      // I could be wrong though. You'll have to play around with it yourself.
      // req.login(user, { session: false }, async (error) => {
      // console.log("using req.login...");

      const body = { _id: user.id, email: user.email };

      const token = jwt.sign({ user: body }, "TOP_SECRET");

      await fs.writeFile(
        "fakeLocal.json",
        JSON.stringify({ Authorization: `Bearer ${token}` }),
        (err) => {
          if (err) throw err; // we might need to put this in a try catch, but we'll ignore it since it's unrelated to passport and auth.
        }
      );

      return res.redirect(`success?message=${info.message}`);
      // }); // this is the closing brackets for the req.login
    })(req, res, next);
  },
  (req, res, next) => {
    res.send("Hello"); // able to add functions after the authenticate call now.
  }
);

Finally, we need a way to verify the user is logged in anytime a request comes to the server. So far, the local strategy has taken care of us.

The "local strategy" in Passport simply verifies that the user has entered the correct information. Did their password meet the right criteria? Is their email unique when they signed up? (we're not checking this but you should) If so, we can save their information in the database on signup.

In the case of logging in, we SEARCH the database to see if the user can be found. Then we compare the password they entered with the password in the database. Is the information correct? If so, find the user, and pass it to the next step. That's all the local strategy does. In the analogy, this is equivalent to getting approval or permission from the locksmith to create a key to your house.

The next step is the callback. We MIGHT be able to use req.login() to log in the user if we want. With sessions you just call req.login() with any callback because Passport knows what to do. With Json Web Tokens, you need to actually walk the express.js server step-by-step through the login process. It seems like req.login() does not get used when doing authentication with JWT and Passport together, but I may be mistaken. Let's just keep going.

What does it mean to be logged in? In the case of JWT, have a token somewhere that can be decoded.

So to log in with JWT, we create the token with jwt.sign(), and pass in the secret key and the user object. Then we store the token in a file (or give it to a client), and redirect.

The client has a valid JWT, therefore they are a logged in User.

In our analogy, this callback step is the equivalent of the Locksmith GIVING us a key that we can use to unlock our house, and the doors inside.

Accessing Secure Routes with JWT

This is where the SECOND strategy comes in. the Local Strategy verified the user entered correct info, and the callback created a token and gave it to the client, but the JWT strategy will actually decode the Json Web Token and determine if the login / JWT is valid or not. This "verifying the user" will happen on every request that has a "lock" on it. Let's see the code:

app.js

app.use(passport.initialize());

function getJwt() {
  // Try accessing a secure route with an INVALID token, and then try with NO TOKEN. You'll get two different errors.
  // Both of those situations will be blocked by this function, and the app won't even make it to the function in JWTStrategy.
  console.log("in getJwt");
  return fakeLocal.Authorization?.substring(7); // remove the "Bearer " from the token.
}

passport.use(
  new JWTstrategy(
    {
      secretOrKey: "TOP_SECRET",
      jwtFromRequest: getJwt,
    },
    async (token, done) => {
      console.log("in jwt strat. token: ", token);

      // 3. Successfully decoded and validated user:
      // (adds the req.user, req.login, etc... properties to req. Then calls the next function in the chain.)
      return done(null, token.user);
    }
  )
);


// replace current /secureroute with below:
app.get(
  "/secureroute",
  passport.authenticate("jwt", { session: false }),
  (req, res) => {
    // 1. Try visiting this route WITHOUT logging in. The authenticate("jwt") line will prevent you from ever getting here.
    //// You should get "unauthorized". In this case use a front end to route appropriately.
    // 2. Try visiting this route with an invalid jwt. So... login, manually alter the jwt, then visit secure route.
    //// you should get "unauthorized" here too. You would use the front end to route in this case.

    // 3. Try visiting this route when logged in with a working user.
    // req.user, req.isAuthenticated, login and logout should all work.

    console.log("------ beginning of /secureroute -----");
    console.log("req.isAuthenticated: ", req.isAuthenticated());
    console.log("req.user: ", req.user); // does this for me.
    console.log("req.login: ", req.login);
    console.log("req.logout: ", req.logout);
    res.send(`welcome to the top secret place ${req.user.email}`);
  }
);

console.log("req.session: ", req.session); // REMOVE THIS from the /failed route if you still have it. We're not using sessions anymore.

At the bottom of that code block we removed some sessions code we aren't using anymore.

In the /secureroute endpoint, we're running passport.authenticate with a JWT strategy. This function prevents the client from getting to the next function if the user doesn't have a valid JWT. The JWT Authenticate function triggers that JWT strategy.

The new JWTstrategy has us pass in the secret TOP_SECRET, along with a function for getting the jwt. Our getJwt function retrieves the token from the fakeLocal file.

If the JWT is valid, we enter into the JWT Strategy function. Right now we're just going to run return done(null, token.user);

Things to test after writing the above code:

  • Signup a user and visit localhost:3000/secureroute
    • This should work, and you should see req.isAuthenticated is true, the req.user exists, and the (apparently useless) req.login and req.logout functions exist too.
  • While logged in and passing the authentication check for /secureroute try manually altering the value of the token in the fakeLocal.json file and refresh the /secureroute page. It should now give you an "unauthorized" message. Try this with NO TOKEN also. Same thing should happen.

Let's pass in false as the user now:

app.js

// in JWTstrategy...
      console.log("in jwt strat. token: ", token);

        if (token?.user?.email == "emptytoken") {
        // 2. Some other reason for user to not exist. pass false as user:
        // displays "unauthorized". Doesn't allow the app to hit the next function in the chain.
        // we are simulating an empty user or no user coming from the JWT
        return done(null, false); // unauthorized
      }

      // 3. Successfully decoded and validated user:
      // (adds the req.user, req.login, etc... properties to req. Then calls the next function in the chain.)
      return done(null, token.user);

Test the above code by updating fakeLocal.json to be an empty object: {}

Then signup a new user with the "email/username" of "emptytoken" and try to visit the /secureroute. We should get unauthorized, even though our token is valid. In this case we're simulating some weird scenario where there is no user coming from the JWT.

Now if we try to hit the /secureroute endpoint we get unauthorized, even if our JWT token is valid.

In the case of an application error, the error should be passed to done() as the first argument like this:

app.js

passport.use(
  new JWTstrategy(
    {
      secretOrKey: "TOP_SECRET",
      jwtFromRequest: getJwt,
    },
    async (token, done) => {
      console.log("in jwt strat. token: ", token);

      // 0. Don't even make it through the getJwt function check. NO token
      // prints unauthorized.

      // 0B. Invalid token: again doesn't make it into this function. Prints unauthorized

      // 1. Makes it into this function but gets App error (displays error message.) no redirecting.
      // We simulate an "application error" occurring in this function with an email of "tokenerror".
      //
      if (token?.user?.email == "tokenerror") {
        let testError = new Error(
          "something bad happened. we've simulated an application error in the JWTstrategy callback for users with an email of 'tokenerror'."
        );
        return done(testError, false);
      }

      if (token?.user?.email == "emptytoken") {
        // 2. Some other reason for user to not exist. pass false as user:
        // displays "unauthorized". Doesn't allow the app to hit the next function in the chain.
        // We are simulating an empty user / no user coming from the JWT.
        return done(null, false); // unauthorized
      }

      // 3. Successfully decoded and validated user:
      // (adds the req.user, req.login, etc... properties to req. Then calls the next function in the chain.)
      return done(null, token.user);
    }
  )
);

To test the above code, reset your fakeLocal.json file to {} and signup a new user with the email/username of tokenerror and visit /secureroute

You should get an Error message with a stack trace. Pass testError.message to done() in order to get the message without the stack trace.

If the JWTstrategy receives an invalid token or no token at all it will also produce an error and print "unauthorized".

Most applications will have front ends that you can use to manage routing, so we don't need powerful redirecting capabilities on the backend.

Logging out a User:

Logging out a user is as simple as removing the token and or invalidating it. Passport.js does not know how to do this, so we must do it. There's actually two ways (or more) to log out, and I'm not sure which one is more "correct" so I'll have them both in the code below:

app.js

// version one
app.get("/logout", async (req, res) => {
  await fs.writeFile(
    "fakeLocal.json",
    JSON.stringify({ Authorization: `` }),
    (err) => {
      if (err) throw err;
    }
  );

  res.redirect("/login");
});

// I am pretty sure the req.logout() function doesn't help us anymore when using JWT.
// I originally thought this turned "isAuthenticated" to false and removed the req.user, but I don't think req.logout does much when using JWT.
// version two
// app.get(
//   "/logout",
//   passport.authenticate("jwt", { session: false }),
//   async (req, res) => {
//     req.logout(async () => {
//       console.log("version two");
//       await fs.writeFile(
//         "fakeLocal.json",
//         JSON.stringify({ Authorization: `` }),
//         (err) => {
//           if (err) throw err;
//         }
//       );
//       console.log("done logging out.");
//       return res.redirect("/login");
//     });
//   }
// );

To test the above code, signup or login the user and you should see the fakeLocal.json file has an Authorization token. Now hit the /logout endpoint and the authorization token should now be empty in fakeLocal.json.

Experimental: JWT Passport Custom Callbacks

We have everything we need to build a powerful authentication system. Especially when you add in a front end. Let's do ONE more thing. We're going to do a custom callback with our JWT Strategy. I'm not sure if this is proper, but so far it appears to work.

app.js

const secureRoutes = require("./secureRoutes");

//....

app.use("/user", secureRoutes);

/server/secureRoutes.js

const express = require("express");
const passport = require("passport");
const router = express.Router();

router.all("*", function (req, res, next) {
  passport.authenticate("jwt", { session: false }, function (err, user, info) {
    console.log("router.all err: ", err?.message); // appears to be the first param of the done() in the JWT strategy.
    console.log("router.all user: ", user); // User from the decoded JWT
    console.log("router.all info: ", info?.message); // info is an "Error" object.

    // 0. Don't even make it through the getJwt function check. NO token
    // (Message in info param: "No auth token")
    // 0B. Invalid token

    if (info) {
      console.log(
        "I happened because the token was either invalid or not present."
      );
      // this is run when the token is either not present, or invalid.
      // The async (token, done) function in the JWTstrategy never even gets run.
      return res.send(info.message);
      // It didn't like me adding an extra string inside .send() along with the message.
      // go ahead and next() or whatever you want.
    }

    // 1. App error.
    if (err) {
      console.log(
        "I happened because you logged in with the user 'tokenerror' and tried to visit a route that passes through this jwt authentication. We are simulating an application error."
      );
      // This err value is populated if there's an error sent as the first parameter of the done(). back in the JWTstrategy we ran:
      //   let testError = new Error("hmm something bad happened");
      //   return done(testError, false);
      // the above two lines in the JWTstrategy will trigger this conditional
      return res.send(err.message);
    }

    if (!user) {
      // if the user somehow gets passed as false from the jwt strategy after being decoded from the token, then we can run this.
      return res.send(
        "Hm... Not sure what happened. We're simulating an empty/false user being decoded from the token."
      );
    }

    // 3. successful decoding / validation
    if (user) {
      // Passing the user object as the second parameter will mean success.
      //Unfortunately with this method we lose the req.user, req.isAuthenticated, req.login, and req.logout methods.
      // This is cause for concern and makes me wonder if there's something wrong with this approach
      // but so far it appears to work, and the req.login() and req.logout() functions seem useless with a JWT strategy so, I think it's fine?
      console.log("req.login? ", req.login);
      req.isAuthenticated = true;
      req.user = user;
      return next();
    }
  })(req, res, next);
});

router.get("/profile", (req, res, next) => {
  console.log("----- beginning of /profile ------");
  console.log("isAuthenticated: ", req.isAuthenticated);
  //   console.log("isAuthenticated value: ", req.isAuthenticated()); // I thought I was able to have this function at one point, but I can't figure it out now.
  console.log("req.user gone: ", req.user);
  console.log("req.login: ", req.login);
  console.log("req.logout: ", req.logout);
  res.json({
    user: req.user,
    message: "Hello friend",
  });
});

router.get("/settings", (req, res, next) => {
  res.json({
    user: req.user,
    message: "Settings page",
  });
});

module.exports = router;

Now we can visit /user/profile and /user/settings endpoints to test. I left comments inside the secureRoutes file, but basically if we try each of the scenarios we just went through, here's what happens for each:

  • invalid or no JWT will trigger the if (info) block, which sends the error message back to the client.
  • If you login with the user of "tokenerror", we can simulate an application error happening in the JWTstrategy, which would be passed as the first parameter of done(). This populates the first parameter "err" in our callback, and sends just the error message back to the client.
  • If you login with the user of "emptytoken" we simulate a valid token with an empty user or no user.
  • Any other valid user will be passed as the 2nd parameter of the done() function, which we then manually add the req.user and req.isAuthenticated to the request object and call the next function. At this point the user has passed validation and will do whatever the next function instructs. In the case of the /user/profile route, we send a json response back with the user and a message.

I'm not sure why we have to manually add the req.user now, OR why req.login and req.logout don't appear to do anything when using jwt. If you visit the "/" endpoint, it seems like no matter what, the isAuthenticated() function returns false, even if you put the jwt creation code in the req.login().

BUT when you hit a route like /secureroute the isAuthenticated() function returns true if you're logged in. I'm led to believe that we have to do the login/logout part ourselves, or I've done something horribly wrong and this whole Passport.js tutorial was a waste.

No idea, but authentication seems to work now!

And that's it for Passport.js Authentication!

Ha! Just kidding... We still have to deal with the front end, and Refresh Tokens, but that will be in another tutorial. Did you know Passport.js has hidden docs?

Get our Paid Tutorials for Free!

name:
email: