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.
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.
$ 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
$ npm install mongodb --save
and launch your db with
$ 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.
$ npm install mongoose --save
For Passport.js, on the other hand, we will need to install several dependencies.
$ npm install passport passport-local passport-local-mongoose --save
And lastly, to handle sessions and cookies we require express-session.
$ npm install express-session --save
From the terminal, we next move on to our app.js file and include the following changes
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.
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:
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.
app.use(passport.initialize());app.use(passport.session());
Last but not least, we configure our Passport/Passport-Local modules using passport-local-mongoose.
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.
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.
var User = require('./models/user');
Configuring our routes
Still awake? Don't worry, we're almost done! 🤞
Moving on, let's open up the existing users.js file created by Express in the routes folder, and include the following changes.
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.
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.
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? 👏