Authenticate Users With Node ExpressJS and Passport.js

Many Node.js applications require users to authenticate in order to access private content. The authentication process must be both functional and secure, and creating one from scratch can be lengthy and cumbersome. Because of this, most modern-day developers opt to use trusted libraries or outside services. Passport.js is a popular Express middleware specifically created to facilitate the login process. It is flexible, trusted by many organizations worldwide, and easy to integrate into your ExpressJS code.

In this tutorial we'll:

  • Create a login form for a Node application using Passport
  • Use the session authentication strategy with Passport
  • Connect Passport to a MongoDB database to store user data
  • Authorize only logged-in users to access a page

By the end of this tutorial, you will learn how to create a functional login page complete with authentication and authorization.

This tutorial is part 7 of 7 tutorials that walk through using Express.js for user authentication.

Goal

Create an application that performs basic authentication and authorization tasks.

Prerequisites

Passport.js strategies

Passport is a library of authentication functions that is adaptable to the needs of most applications. What makes Passport so flexible? The main reason is that developers can choose from a cornucopia of implementation strategies.

Passport strategies are separate modules created to suit individual means of authentication. Available Passport strategies include:

  • passport-local: local username and password authentication
  • passport-facebook: authenticate with Facebook via OAuth
  • passport-twitter: authenticate with Twitter via OAuth
  • passport-jwt: use JSON web tokens instead of sessions

There are hundreds of strategies. You can even find modules tailored to specific technologies and databases. For example, in this project, we will use local authentication with MongoDB. To concentrate on the authentication code, we’ll use the MongoDB wrapper Mongoose and the passport-local-mongoose module.

MongoDB

MongoDB is a NoSQL database that uses JSON-formatted documents to store data. Some NodeJS developers prefer MongoDB because of familiarity of JSON, which can be easily manipulated into JavaScript objects. Here’s an example of MongoDB data:

{ name: 'Lori Fields',
  address: {
    street: 123 Palm Trace
    city: Miami, FL
    zip code: 33101
  }
},  
{ name: 'Harry Humbug',
  address: {
    street: 6 SW 1st Street
    city: Deerfield Beach, FL
    zip code: 33442
  }
}

Authentication application with Passport and ExpressJS

Now that you've had a brief overview of Passport and MongoDB, we are ready to begin the project. Here’s the application's file structure:

.
└── login
    ├── package.json
    ├── server.js
    ├── static
    │   ├── index.html
    │   ├── login.html
    │   └── secret-page.html
    └── user.js

As you can see, our primary project folder is named login. Inside login we have:

  • A folder named static that contains HTML files
  • server.js which is the root to our application and contains all our ExpressJS server code, including our routes
  • user.js which uses Mongoose to connect to the database and create our user model
  • package.json, the configuration file

The code in this tutorial builds on code in 2 previous tutorials:

  1. How to Make a Form with ExpressJS
  2. How to Set Up Express Session

If you do not understand ExpressJS or express-session, please view the previous tutorials.

Download MongoDB and create a database

Navigate to Install MongoDB Community Edition and click the link for your operating system (Linux, macOS, or Windows). Follow the directions to install and start MongoDB.

Once MongoDB is installed and started, go to the MongoDB command shell.

mongo

Then go to the database "users" (and create it, if it doesn't already exist):

> use users

At any time, you can exit the MongoDB command shell by typing the quit command.

> quit ()

Create a package.json file

Assuming you have already have NodeJS installed, create a package.json file inside the main project folder.

npm init -y

Install ExpressJS and dependencies

Before we start writing code, we must install the necessary dependencies.

Let’s start by installing ExpressJS (server framework), body-parser (parses incoming request bodies), and express-session (cookie-based session middleware).

npm install express body-parser express-session

Next, we install Passport and passport-local. Passport is the authentication library and passport-local is our core authentication strategy.

npm install passport passport-local

Now we install connect-ensure-login, authorization middleware that makes it easy to restrict access to pages. It is built to work hand-in-hand with Passport, and with one function call, we can authorize routes to only logged-in users.

npm install connect-ensure-login

Next, we install Mongoose, an object data mapper (ODM) used to integrate MongoDB with NodeJS. The library simplifies MongoDB data modeling, facilitating the creation of JavaScript objects and database document persistence. The asynchronous nature of NodeJS query functions can make data modeling cumbersome. Using Mongoose in this project will enable us to concentrate on the authentication code instead of document creation and database integration.

npm install mongoose

Finally, we install passport-local-mongoose. This strategy integrates Mongoose with the passport-local strategy.

npm install passport-local-mongoose

Create HTML files

Let’s make sure we have all our HTML files properly written and saved in the static directory.

Example index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
	<title>Welcome Home</title>
  </head>
  <body>
	<a href="/login">Please Login Here!</a>
  </body>
</html>

Example login.html:

<!DOCTYPE html>
<html lang="en">
  <head>
	<title>Example Login Form</title>
  </head>
  <body>
	<form action="//members.osiolabs.com/login" method="post">
  	  <!-- user input-->
  	  Username:<br>
  	  <input type="text" name="username" placeholder="Username" required><br><br>
  	  Password:<br>
  	  <input type="password" name="password" placeholder="Password" required><br><br>
  	  <!-- submit button -->
  	  <input type="submit" value="login">
	</form>
  </body>
</html>

Example secret-page.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Secret Page</title>
  </head>
  <body>
    <p>This is a secret page that is only available to logged-in users.
     Congratulations!!!  You are in on our secret!</p>
     <a href="/logout">Log Out</a>
  </body>
</html>

Create user.js

Let's get the database code out of the way and create user.js. We will:

  • Connect to the database
  • Create our Passport user model
  • Create a MongoDB document called 'userData'

Let’s start by including Mongoose and passport-local-mongoose.

const mongoose = require('mongoose');
const passportLocalMongoose = require('passport-local-mongoose');

Next, we connect to the database "users" on our local server. If the database server is not on your local machine, instead of "localhost", you will write the hostname or IP address.

mongoose.connect('mongodb://localhost/users',{
  useNewUrlParser: true,
  useUnifiedTopology: true
});
  • Connection URL: Our connection URL is 'mongodb://localhost/users'. 'localhost' is the host and 'users' is the database. If the database is on an outside server you may have to include more parameters, e.g.: 'mongodb://username:password@host:port/database?options...'
  • useNewUrlParser: This is set to avoid deprecation warnings from the native MongoDB driver; it’s opt-in to facilitate conversions for older applications. Set this to true for new development. See Mongoose documentation for details.
  • useUnifiedTopology: Also set to avoid deprecation warnings; briefly, the connection management engine was upgraded. Set to true for new development and see Mongoose documentation for details.

Now we create our User model, containing 2 properties: username and password. They are both String datatype.

const Schema = mongoose.Schema;

const User = new Schema({
  username: String,
  password: String
});

Finally, we are ready to export our model.

  1. Using the passport-local-mongoose library, we will export 'User' in a usable form to the passport code we are about to write.
  2. We export our model as a module, so we can use it in server.js and create a document called 'userData'. userData will store our user information (id, username, hash). Passport-local-mongoose automatically salts and hashes passwords.

The completed user.js file should look like the code below:

// dependencies
const mongoose = require('mongoose');
const passportLocalMongoose = require('passport-local-mongoose');
// connect to database
mongoose.connect('mongodb://localhost/users',{
  useNewUrlParser: true,
  useUnifiedTopology: true
});
// Create Model
const Schema = mongoose.Schema;

const User = new Schema({
  username: String,
  password: String
});
// Export Model
User.plugin(passportLocalMongoose);

module.exports = mongoose.model('userData', User, 'userData');

Create some dummy user data

To test our application, we will need test data. Test data is temporary data we insert into the database to test code. In a live application, users would register via a sign-up form.

To test our authentication code, let’s register two users. Create a file named dummy-data.js with the following code:

const UserDetails = require('./user');

UserDetails.register({ username: 'candy', active: false }, 'cane');
UserDetails.register({ username: 'starbuck', active: false }, 'redeye');

This will create two users, candy and starbuck, with passwords cane and redeye.

While in the early stages of development, this is a quick way to add temporary test data to the temporary test database. You would never use this method on a production server. Other methods of inserting data include running a migration from the command line, using a form (such as a sign-up form), and placing the code above in a data migration file.

Run the code:

node dummy-data.js

Create a server.js

Now we are ready to create our application’s root server.js file.

We start by including our dependencies.

const express = require('express'); // server software
const bodyParser = require('body-parser'); // parser middleware
const session = require('express-session');  // session middleware
const passport = require('passport');  // authentication
const connectEnsureLogin = require('connect-ensure-login'); //authorization

And don’t forget to include our User model.

const User = require('./user.js'); // User Model 

Then let’s create our Express app.

const app = express();

We must configure express-session, as the passport-local-mongoose module does not do it for you. To learn about configuring express-session, please review Setup Express Session Authentication.

app.use(session({
  secret: 'r8q,+&1LM3)CD*zAGpx1xm{NeQhc;#',
  resave: false,
  saveUninitialized: true,
  cookie: { maxAge: 60 * 60 * 1000 } // 1 hour
}));

We have to configure a bit more middleware:

  • bodyParser: We are parsing URL-encoded data from the body
  • passport.initialize: Middleware to use Passport with Express
  • passport.session: Needed to use express-session with passport
app.use(bodyParser.urlencoded({ extended: false }));
app.use(passport.initialize());
app.use(passport.session());

Before doing any authenticating, your strategy must be declared and configured. Our strategy module, passport-local-mongoose, does the heavy lifting of creating the strategy for us, so all we need is the code below.

passport.use(User.createStrategy());

If we were just using passport-local and not passport-local-mongoose, we would have to configure the login strategy. See the example local strategy configuration below (not used in today’s project).

passport.use(new LocalStrategy(
  // function of username, password, done(callback)
  function(username, password, done) {
    // look for the user data
    User.findOne({ username: username }, function (err, user) {
      // if there is an error
      if (err) { return done(err); }
      // if user doesn't exist
      if (!user) { return done(null, false, { message: 'User not found.' }); }
      // if the password isn't correct
      if (!user.verifyPassword(password)) { return done(null, false, {   
      message: 'Invalid password.' }); }
      // if the user is properly authenticated
      return done(null, user);
    });
  }
));

To authenticate, Passport first looks at the user's login details, then invokes a verified callback (done). If the user gets properly authenticated, pass the user into the callback. If the user does not get appropriately authenticated, pass false into the callback. You also have the option to pass a specific message into the callback.

When you use sessions with Passport, as soon as a user gets appropriately authenticated, a new session begins. When this transpires, we serialize the user data to the session and the user ID is stored in req.session.passport.user. To access the user data it is deserialized, using the user ID as its key. The user data is queried and attached to req.user.

passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());

So far, server.js should look like the code below:

const express = require('express'); // server software
const bodyParser = require('body-parser'); // parser middleware
const session = require('express-session');  // session middleware
const passport = require('passport');  // authentication
const connectEnsureLogin = require('connect-ensure-login');// authorization

const User = require('./user.js'); // User Model

const app = express();

// Configure Sessions Middleware
app.use(session({
  secret: 'r8q,+&1LM3)CD*zAGpx1xm{NeQhc;#',
  resave: false,
  saveUninitialized: true,
  cookie: { maxAge: 60 * 60 * 1000 } // 1 hour
}));

// Configure More Middleware
app.use(bodyParser.urlencoded({ extended: false }));
app.use(passport.initialize());
app.use(passport.session());

// Passport Local Strategy
passport.use(User.createStrategy());

// To use with sessions
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());

Define GET routes

We are ready to append the server.js code with the GET routes.

We start with the route to our homepage. It is accessible to everybody.

app.get('/', (req, res) => {
  res.sendFile(__dirname + '/static/index.html');
})

Next, we create the route to our application's login page. It is also accessible to everybody.

app.get('/login', (req, res) => {
  res.sendFile(__dirname + '/static/login.html');
});

Now we create the dashboard route. It is only accessible to logged-in users.

app.get('/dashboard', connectEnsureLogin.ensureLoggedIn(), (req, res) => {
  res.send(`Hello ${req.user.username}. Your session ID is ${req.sessionID} 
   and your session expires in ${req.session.cookie.maxAge} 
   milliseconds.<br><br>
   <a href="/logout">Log Out</a><br><br>
   <a href="/secret">Members Only</a>`);
});

As you can see above, we call the connectEnsureLogin.ensureLoggedIn() function in our route handler to ensure only logged-in users can access the page.

Once navigated to that route, we send a message to our logged-in user. The message contains:

  • Their username: req.user.username (further explained above when creating the user.js schema file)
  • Their session ID: req.sessionID
  • The maximum age of the session cookie: req.session.cookie.maxAge
  • A link to log out of the application: /logout
  • A link to a members-only page: /secret

Our secret page route is also only accessible to logged-in users.

app.get('/secret', connectEnsureLogin.ensureLoggedIn(), (req, res) => {
  res.sendFile(__dirname + '/static/secret-page.html');
});

Finally, we create our route to log out users.

app.get('/logout', function(req, res) {
  req.logout();
  res.redirect('/login');
});
  • req.logout: logs out user
  • req.redirect: Redirects the user to another page. In this case, the user gets redirected to /login.

Add a login POST route

Our application contains one POST route, /login. We are ready to make the authentication process work by creating this route.

app.post('/login', passport.authenticate('local', { failureRedirect: '/' }),  function(req, res) {
	console.log(req.user)
	res.redirect('/dashboard');
});

In our route handler we call passport.authenticate, to which we pass 2 parameters:

  • 'local': The primary strategy we are using
  • failureRedirect: If the user does not authenticate, they get redirected to the homepage.

If the user fails to authenticate, they are sent back to the homepage. If they do authenticate, the code inside the callback will execute:

  • console.log(req.user): When a user authenticates, user properties are attached to req.user. When you test this application, we will see those properties in your terminal: ID, username, salt, and hash. Depending on how you design your User, these properties can differ.
  • res.redirect(‘/dashboard’): Once the user authentications, they are redirected to /dashboard.

Assign a port for the application

We are ready to finish server.js by assigning a port.

// assign port
const port = 3000;
app.listen(port, () => console.log(`This app is listening on port ${port}`));

Our completed server.js file should look like the code below:

const express = require('express'); // server software
const bodyParser = require('body-parser'); // parser middleware
const session = require('express-session');  // session middleware
const passport = require('passport');  // authentication
const connectEnsureLogin = require('connect-ensure-login');// authorization

const User = require('./user.js'); // User Model

const app = express();

// Configure Sessions Middleware
app.use(session({
  secret: 'r8q,+&1LM3)CD*zAGpx1xm{NeQhc;#',
  resave: false,
  saveUninitialized: true,
  cookie: { maxAge: 60 * 60 * 1000 } // 1 hour
}));

// Configure Middleware
app.use(bodyParser.urlencoded({ extended: false }));
app.use(passport.initialize());
app.use(passport.session());

// Passport Local Strategy
passport.use(User.createStrategy());

// To use with sessions
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());

// Route to Homepage
app.get('/', (req, res) => {
  res.sendFile(__dirname + '/static/index.html');
});

// Route to Login Page
app.get('/login', (req, res) => {
  res.sendFile(__dirname + '/static/login.html');
});

// Route to Dashboard
app.get('/dashboard', connectEnsureLogin.ensureLoggedIn(), (req, res) => {
  res.send(`Hello ${req.user.username}. Your session ID is ${req.sessionID} 
  and your session expires in ${req.session.cookie.maxAge} 
  milliseconds.<br><br>
  <a href="/logout">Log Out</a><br><br><a href="/secret">Members Only</a>`);
});

// Route to Secret Page
app.get('/secret', connectEnsureLogin.ensureLoggedIn(), (req, res) => {
  res.sendFile(__dirname + '/static/secret-page.html');
});

// Route to Log out
app.get('/logout', function(req, res) {
  req.logout();
  res.redirect('/login');
});

// Post Route: /login
app.post('/login', passport.authenticate('local', { failureRedirect: '/' }),  function(req, res) {
	console.log(req.user)
	res.redirect('/dashboard');
});

// assign port
const port = 3000;
app.listen(port, () => console.log(`This app is listening on port ${port}`));

Test the code

Our project is complete, and we are ready to test the code. Go to your terminal, navigate to the main project folder (login), and start the server.

node server.js

In your web browser, go to http://localhost:3000/. You should see the home page.

Click on the Please Login Here! link to navigate to the login page.

Now enter valid login credentials. Then click the login button to navigate to /dashboard.

Page showing your username, session info, and a link to view members only content.

Next, click the Members Only link to navigate to /secret.

Page showing the text &quot;This is a secret page that is only available to logged in users&quot; and a link to log out.

Finally, click the Log Out link to make sure you don’t have access to /dashboard or /secret when you are not logged in. You can also make sure you are redirected to the homepage when you enter incorrect login details.

Recap

Passport.js is a widely used authentication middleware that facilitates the creation of applications. Developers have a wide assortment of authentication strategies to choose from, suiting most applications’ needs. In this project, we used passport-local to verify usernames and passwords locally.

For our database, we used MongoDB, a popular NoSQL database that stores data in JSON documents. To simplify data modeling, we used Mongoose to handle NodeJS and MongoDB interaction. There are hundreds of specific Passport strategy modules available, and we chose to use passport-local-mongoose.

To use Passport with ExpressJS, you must:

  • Connect to the Database
  • Create a User model
  • Configure the appropriate middleware
  • Configure the Passport strategy
  • Call the function passport.authenticate in your login POST route

Once authenticated, req.user is created, allowing you to access user properties.

Further your understanding

  • Besides username and password, what other user properties might you want to use?
  • What are the advantages and disadvantages of using an ODM or ORM?
  • Why isn’t it advisable to store user data in a server file?

Additional resources

Sign in with your Osio Labs account
to gain instant access to our entire library.

Data Brokering with Node.js