NB:
- Basic knowledge of node.js is needed to follow along with this article
- You should be on the root path of the project when typing in the commands from the command line snippet
The majority of the codebases I've worked on over the years have always favoured using JSON web-tokens (JWT) or Authentication-as-a-Service platforms (Auth0, Okta etc) for authentication logic.
These are indeed excellent choices! however, on smaller projects I find these to always seem to be overkill. Recently I started working on a chrome extension that performs social sign-in using twitter OAuth API and decided to use passport.js to outsource some of the heavy lifting involved in setting up authentication. Below I walk through the benefits and step-by-step guide in getting started with passport.js for authentication
The Why
Authentication is a very delicate process in the creation of software and having the right strategy to use for your application can be a chore. In order not to get caught up in a dilemma of the perfect authentication flow to use that would handle millions of users, I like to stick to simpler tools that would serve my end users effectively and only scale when the need arises (and not a moment before). For a frontend heavy application like the chrome extension I'm working on, I found setting up authentication with passport.js to be the more effective option for my use case as I will require very little usage of the backend.
What is PassportJS
According to the official docs
Passport is authentication middleware for node.js. Extremely flexible and modular, Passport can be unobtrusively dropped in to any Express-based web application. A comprehensive set of strategies support authentication using a username and password, Facebook, Twitter, and more.
Let's break this down so we understand it at a more fundamental level.
passport is authentication middleware for node.js: A middleware is just a function that runs in between a request/response cycle and node.js is a runtime environment that enables us run javascript outside the browser.
can be unobtrusively dropped in to any express-based application: This means we can use passport in any node.js application in a way that keeps our code lean, clean and easy to maintain.
support authentication using a username and password, Facebook, Twitter: passport.js gives you the flexibility to authenticate users with username/password, google, twitter and more. It does this by using strategies
which are installed as separate packages from the npm directory and there are more than 500 strategies that can work with passport!
To top it all off this package is 81kB
! For context the google-auth-library which is the official library for implementing google OAuth has a size of 496kB
. Pretty easy to see why I am using passport.js on this project
Project Scope and Setup
Without wasting too much time let's jump into the scope of the project.
Today we will be making a simple app that
allows users to login, view the current time of day and logout
First thing's first, let's setup our server.
(PS: You might want to make sure you have nodejs installed on your machine first)
Create folder
mkdir timely # create folder for project
cd timely # navigate into folder
touch index.js # create main entry point into your server
npm init -y # initialize npm
Install dependencies
npm install passport express express-session passport-local ejs # install dependencies
npm install nodemon --save-dev #install peer dependencies
Setup User Interface
We're going to be using EJS as our templating engine so we can dynamically display user content on our website. If you're not quite sure what templating engines are, you can get an idea through on this amazing article.
mkdir views # create views folder
cd views # navigate into views folder
touch authenticate.ejs && touch index.ejs # create out home and authentication page
Now we will be adding the public
folder which houses our static assets for the project
mkdir public # create public folder
cd public # navigate into public folder
touch main.js && touch styles.css
I've included the content for main.js
and styles.css
which you can copy over here
Locate the index.ejs
file and replace it's contents with the following as well
<!-- navigate into timely/views/index.ejs and insert the following -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/styles.css" />
<script defer src="/main.js"></script>
<title>Timely | Who needs a rolex anyway 🤷♂️</title>
</head>
<body>
<div class="header">
<h1>Timely 🕰</h1>
</div>
<div class="container">
<h1>Welcome User</h1>
<div class="content"></div>
</div>
</body>
</html>
Create a Server
// Navigate into timely/index.js
const express = require('express');
const path = require('node:path');
const app = express();
app.use(express.static(path.join(__dirname, 'public'))); // Require static assets from public folder
app.set('views', path.join(__dirname, 'views')); // Set 'views' directory for any views being rendered res.render()
app.engine('html', require('ejs').renderFile); // Set view engine as EJS
app.set('view engine', 'ejs');
app.get('/', (req, res) => res.render('index'));
app.listen(3000, () => console.log('server is running on port 3000'));
Setup scripts in package.json
{
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js",
"start:dev": "nodemon index.js"
}
}
Run script
npm run start:dev
At this point we should get back a large text that reads "Welcome User" along side the current date and time when we visit http://localhost:3000/
Looks great! but we want only logged in users to have access to the time and date. so lets create an authentication page for users that aren't logged in.
Locate the authentication.ejs
file inside your views
folder and replace it's content with the following
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/styles.css" />
<title>Timely | Who needs a rolex anyway 🤷♂️</title>
</head>
<body>
<div class="header">
<h1>Timely 🕰</h1>
</div>
<form action="/log-in" method="POST" class="container">
<h1>please log in</h1>
<div>
<label for="username">Username</label>
<input name="username" placeholder="username" type="text" />
</div>
<div>
<label for="password">Password</label>
<input name="password" type="password" />
</div>
<button>Log In</button>
</form>
</body>
</html>
Before we write out any more code, lets understand the passport packages we installed and how they fit together
passport: This is the main passport module that helps us attach the information of a current logged in user to the req.user
object
passport-local: Passport.js works in collaboration with strategy packages to perform the authentication flows you need for your application. We will be using passport-local
for this as we will be authenticating our users with username and password only
express-session: This will enable us store and manage sessions of currently logged in users on the server
Now that we have a little idea of how passport works, let's create our passport middleware strategy. This will be the logic behind adding the user information into our req.user
object. In your root folder create the following directories
mkdir strategy && cd strategy # create and move into strategy folder
touch local.js # create strategy file
NB: On real world projects it's best to hash user passwords before storing them, for improved security and also use an actual database for storing user information. Here I will be storing user information in a json file on the system to avoid network calls and keep the sole focus of the article on passport.js.
Inside local.js
replace it's content with the following
const path = require('node:path'); // path module to find absolute paths on the system
const fs = require('node:fs'); // file system module to manipulate files
const passport = require('passport'); // the main star of the show
const LocalStrategy = require('passport-local'); // the co-protagonist in this sequel
const dbpath = path.join(__dirname, 'db.json'); // path to our custom in-house json database
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
if (!fs.existsSync(dbpath)) { // if db does not exist, create db
fs.writeFileSync(dbpath, JSON.stringify({ users: [] }));
}
const db = JSON.parse(fs.readFileSync(dbpath, { encoding: 'utf-8' }));
let user = db.users.find((item) => item.id === id);
if (!user) {
done(new Error('Failed to deserialize'));
}
done(null, user);
});
passport.use(
new LocalStrategy(async (username, password, done) => {
if (!fs.existsSync(dbpath)) { // if db.json does not exist yet, we create it
fs.writeFileSync(dbpath, JSON.stringify({ users: [] }));
}
const db = JSON.parse(fs.readFileSync(dbpath, { encoding: 'utf-8' }));
let user = db.users.find((item) => {
return item.username === username && item.password === password;
});
if (!user) {
user = {
id: Math.floor(Math.random() * 1000), // generate random id between numbers 1 - 999
username,
password,
};
db.users.push(user);
fs.writeFileSync(dbpath, JSON.stringify(db));
}
done(null, user);
})
);
this file will be run each time a request is made to our server. Let's breakdown what each method does exactly
passport.use: This is the first point of contact and is responsible for creating our users. It is run once and after successfully authenticating our user, it passes this information unto the next middleware which is the passport.serializeUser
passport.serializeUser: This creates a session in passport with our key being user.id
. It's quite important for our sessionIDs
to be unique as well to avoid conflict
passport.deserializeUser: On each request, This method receives our sessionID
and scans our database to get the user information of the currently logged in user then attaches that information to the req.user
object
Now That we know how passport works lets finally include it into the project which will enable us identify logged in users and re-route their requests accordingly
const express = require('express');
const passport = require('passport');
const session = require('express-session');
const path = require('node:path');
const app = express();
require('./strategy/local'); // passport strategy will listen to every request
app.use(
session({
secret: 'SOME SECRET', // secret key to sign our session
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 1000 * 60 * 60 * 24, // TTL for the session
},
})
);
// initialize passport package
app.use(passport.initialize());
// initialize a session with passport that authenticates the sessions from express-session
app.use(passport.session());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public'))); // Require static assets from public folder
app.set('views', path.join(__dirname, 'views')); // Set 'views' directory for any views being rendered res.render()
app.engine('html', require('ejs').renderFile); // Set view engine as EJS
app.set('view engine', 'ejs');
app.get('/', (req, res) => {
if (!req.user) {
// if passport does not have record of this user, we redirect them to the authentication page
res.render('authenticate');
} else {
res.render('index', { user: req.user });
}
});
app.post(
'/log-in',
passport.authenticate('local', {
// on Initial login, passport will redirect to either of these routes
successRedirect: '/',
failureRedirect: '/',
})
);
app.listen(3000, () => console.log('server is running on port 3000'));
If we visit http://localhost:3000/
on our browser again, we are immediately routed to the authentication page.
Upon sign-up we are assigned a session cookie which you can view in our browser devtools console (right click on the page -> inspect -> Application -> cookies -> http://localhost:3000).
Each time a request is sent to the server, this cookie is sent alongside it and from this cookie, passport can correctly identify if we are logged in or not.
To further certify this point, you can go ahead and delete the cookie from the devtools console then refresh the page - In an instant you are immediately re-routed to the authentication page.
Our homepage looks good but let's add a bit of personality to it by displaying the users username with ejs
and also the ability for users to logout.
In index.ejs
make the following changes
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/styles.css" />
<script defer src="/main.js"></script>
<title>Timely | Who needs a rolex anyway 🤷♂️</title>
</head>
<body>
<div class="header">
<h1>Timely 🕰</h1>
</div>
<form class="container" action="/log-out" method="post">
<h1>Welcome back, <%= user.username %></h1>
<div class="content"></div>
<button>Log out</button>
</form>
</body>
</html>
Now to handle the logout route on the server we can add the following code just above app.listen()
method
app.post('/log-out', (req, res) => {
req.logOut((err) => { // clear out information in passport-session and redirects user
if (err) {
res.send('something went wrong');
}
res.redirect('/');
});
});
app.use('*', (req, res) => res.redirect('/')); // to catch and redirect all other routes
NB: You might have noticed that each time you make changes to server files, you are re-routed to the authentication page. This is because
sessionIDs
are stored in a cache created byexpress-session
and oncenodemon
restarts the server, theexpress-session
cache is wiped clean hence passport can not recognise us with the previoussessionID
.
And that's it! Now we can fully log in and out of our application using passport.js.
I do recommend reading more into their documentations to discover new strategies you can use in your future projects. Till next time, Ciao!
Top comments (0)