Local User Authentication With Passport And Express 4

Node.js

23/03/2021


Setting up user authentication can be a tricky business. But fret not, I've got you covered! In this tutorial, I'll show you how to set up your own user authentication from scratch with Passport.js and Express 4, specifically implementing the local strategy with Mongoose and MongoDB.

How it works

Before we dive right into the code, let's take a minute to explain what we're exactly doing. I mean, the goal of this article is to make you smarter, not more confused.

Confused look

First of all, what do we mean by "local strategy"? Well, Passport.js offers many login strategies, such as social media logins. However, the most common and simple strategy of them all, and the one we are considering in this tutorial, is using good ol' login with a username and passport.

The great thing about Passport.js is that most of the user authentication process is already taken care off. Nevertheless, we will still remain responsible for:

  • Storing and authenticating any user information with Mongoose in our database. Fortunately, there's a library that greatly simplifies this process.
  • The sessions and cookies, so users don't have to login every time they visit our page.

Setting up our Express app

To begin this tutorial, you'll first need Express 4 up and running. You can generate an Express app with the following command line in your project folder.

BASH
$ npx express-generator <express-app-name>

Subsequently, we need install all our dependencies with $ npm install and make sure that everything works fine with $ npm run start. You can find the app running by default on http://localhost:3000/.

Installing the MongoDB driver

If you don't have MongoDB installed yet on your computer, you can read on their site how to do so.

Then, install the Node.js driver with

BASH
$ npm install mongodb --save

and launch your db with

BASH
$ mongod
  • If mongod failed for you, double-check your installation guide as it may differ depending on your installation method and OS.
  • If you find yourself running into the error Failed to set up listener: SocketException: Address already in use, run $ killall mongod once before.

Installing the right dependencies

Alright, it's about time we get to the interesting bits! 🤩

The Mongoose ODM is the first library we'll need to install after setting up our basic project template.

BASH
$ npm install mongoose --save

For Passport.js, on the other hand, we will need to install several dependencies.

BASH
$ npm install passport passport-local passport-local-mongoose --save

And lastly, to handle sessions and cookies we require express-session.

BASH
$ npm install express-session --save

From the terminal, we next move on to our app.js file and include the following changes

JAVASCRIPT
var mongoose = require('mongoose');
var passport = require('passport');
var session = require('express-session');
var LocalStrategy = require('passport-local').Strategy;

Easy so far, ain't it? 👍

Oh! And before I forget it, we also need to connect our app to our database.

JAVASCRIPT
mongoose.connect('mongodb://localhost/<database-name>', { useNewUrlParser: true });

Including middleware

Our next step is to include any necessary middleware, which we certainly got a few of. Starting with express-session:

JAVASCRIPT
app.use(session({
name: 'session-id',
secret: '123-456-789',
saveUninitialized: false,
resave: false
}));

Immediately afterwards, we initialise our passport module and connect it to our session module.

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

Last but not least, we configure our Passport/Passport-Local modules using passport-local-mongoose.

JAVASCRIPT
passport.use(new LocalStrategy(User.authenticate()));
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());

Creating our User model

If you've paid any particular attention to the Passport/Passport-Local configuration code, you'll notice that it relies on a certain User module. This module is essentially our Mongoose user schema with which we sign up and login all our users.

Let's continue and create a file called user.js in a folder called models. Our file will contain the following.

JAVASCRIPT
var mongoose = require('mongoose');
var passportLocalMongoose = require('passport-local-mongoose');
var UserSchema = new mongoose.Schema({
username: {
type: String,
unique: true,
required: true
}
});
UserSchema.plugin(passportLocalMongoose);
var User = mongoose.model('User', UserSchema);
module.exports = User;

In our user model, we don't need to explicitly define a password or even a username as it is automatically taken care of by passport-local-mongoose. Pretty neat, right? However, in this example, I took the liberty of adding some requirements to the username. And in case you were worried about security, our library also takes care of this by hashing and salting our passwords. Double neat, right? 😲

After doing this, we have to require our model into our app.js file in order to ensure our Passport/Passport-Local configurations work properly.

JAVASCRIPT
var User = require('./models/user');

Configuring our routes

Still awake? Don't worry, we're almost done! 🤞

Falling asleep

Moving on, let's open up the existing users.js file created by Express in the routes folder, and include the following changes.

JAVASCRIPT
const User = require('../models/user');
const passport = require('passport');
router.post('/signup', (req, res, next) => {
User.register(new User({
username: req.body.username
}),
req.body.password, (err, user) => {
if (err) {
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.json({
err: err
});
} else {
passport.authenticate('local')(req, res, () => {
User.findOne({
username: req.body.username
}, (err, person) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.json({
success: true,
status: 'Registration Successful!',
});
});
})
}
})
});

In this chunk of code, we're signing up a user whenever they access /users/signup with a POST request. As I've made it a requirement that each username is unique, our database will throw an error whenever we try to register a duplicate, thus never reaching passport.authenticate('local'), which then automatically logs in the newly created user.

JAVASCRIPT
router.post('/login', passport.authenticate('local'), (req, res) => {
User.findOne({
username: req.body.username
}, (err, person) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.json({
success: true,
status: 'You are successfully logged in!'
});
})
});

Similarly to before, whenever a user accesses /users/login with a POST request, Passport.js takes care of all the bothersome authentication details for us when we reach passport.authenticate('local'). Furthermore, Passport.js automatically creates a cookie and a session for the user logging in.

JAVASCRIPT
router.post('/logout', (req, res, next) => {
if (req.session) {
req.logout();
req.session.destroy((err) => {
if (err) {
console.log(err);
} else {
res.clearCookie('session-id');
res.json({
message: 'You are successfully logged out!'
});
}
});
} else {
var err = new Error('You are not logged in!');
err.status = 403;
next(err);
}
});

Last but not least, a user can log out by accessing /users/logout with a POST request. We're using POST instead of GET as the browser might otherwise try to cache the response.

In this scenario, next to logging out the user with req.logout(), we have to manually destroy a user's session and clear any cookies. We also provide some error handling should someone try to logout who's not logged in.

Oof, we've finally made it! Why not give your code a try with Postman? 👏


WRITTEN BY

Code and stuff