Node.js passport simplifies user authentication and authorization when building web applications. Despite the usefulness of passport.js, you may fail to maximize its potential without understanding HTTP headers, middleware, sessions, the concept of strategies, and user authentication.
This tutorial explains the passport.js must-knows in plain English. After understanding the key concepts, you will practice them by building a simple login application. What is more? Find out below.
Understanding Node.js Passport module in layman's terms
Technically, Nodejs passport is a module for user authentication.
A module is code residing in a file away from the one you are currently modifying. There are two main types of modules: global and custom modules.
A global module comes with Node.js, and you don't install it with the node package manager (npm
). A custom module is user-created and lives in the npm
. Passport.js is an example of a custom module.
npm install passport passport-local
It is worth noting the difference between authentication and authorization. Authentication is knowing the users of your application. On the other hand, authorization entails controlling the information accessible to each user.
Nodejs passport stands between a browser (also known as the client or user-agent) and the server, identifying logged-in users.
It monitors requests to authenticate a user, appending different properties to them. Nodejs passport is thus an example of middleware. A middleware is a block of code that controls how a part of code interacts with another.
Nodejs passport exists in strategies. A passport strategy is a middleware designed to interact differently depending on the platform and its access mode.
Different developers build and post the Nodejs passport strategies, including their documentation. As a result, some strategies could have better documentation than others.
However, the concept of user-authentication with Node.js passport is the same. This tutorial points out what the docs may not tell you.
Now that you know the significance of Nodejs passport in your application and the challenges incurred while adopting it, you should understand client-server interaction before applying the module.
How Express.js powers Node.js passport
Before learning the role of express sessions in user authentication with Nodejs passport, it would be best to find out how Express.js powers the authentication technology in the background.
Think of express as a pool of middleware. Unlike typical JavaScript functions, the middleware accepts arguments whose representations are predetermined by their parsing order: error handler, request object, response object, and the next middleware handler.
The type of middleware determines the number of arguments to parse. A route middleware is complete with request and response objects. Think of routes as middleware incorporating HTTP headers: GET, POST, PUT and DELETE.
Other middleware apart from error-handlers accepts the request, response, and next middleware handlers. Lastly, an error-handling middleware accepts the four parameters in the above order.
The order of the middleware calling determines their effectiveness. Express.js wants you to put a middleware in the global use()
middleware if you wish the middleware to be effective throughout the application. Otherwise, run or reference the middleware within another sub-middleware.
You can also attach custom properties to a middleware, visible in subsequent middleware. That is how Nodejs passport links particular data to request objects, the state being visible across your application.
The role of sessions in Node.js passport user authentication
Nodejs passport authenticates a user through express sessions.
The client sends HTTP requests to one of the routes. The express session middleware initializes a session on the server.
Unlike a (client-side) cookie that identifies the client by attaching itself to HTTP requests, a (server-side) session authenticates a user using a secret key.
Next, the middleware grabs the session id and sets it to the request cookie. The middleware then stores the cookie in the set-cookie HTTP response header returning to the client. Once the cookie reaches the browser, it holds it and resends the cached data with subsequent requests.
During the subsequent request, express session middleware grabs the cookie value and matches it to that stored in the database using the store attribute.
Let's see the authentication practically using Nodejs passport with the local strategy.
Lab setup to practice Nodejs passport local authentication
We will build a simple login using Express.js, authenticating the user with Nodejs passport's local strategy. It would be best if you understood how Nodejs modules and filesystem work, using Express.js ejs
and routing to follow the following sections of this tutorial.
I will explain the specific project files that directly interact with Nodejs passport. You can get the code on GitHub before proceeding. Clone the repository and open the files using your preferred code editor.
Install the packages
npm i
and nodemon
globally or as a devDependency
npm i -g nodemon
Here is what each installed module does.
bcryptjs
hashes and verifies user passwords. connect-mongo
stores session data in MongoDB. ejs
is the templating engine, and we are using its express layouts. express-session
caches client and logged-in user data. method-override
handles DELETE requests when logging out the user from the session.
mongoose
connects us to MongoDB. passport
is the authentication middleware, and we are using its local strategy in this application. dotenv
stores sensitive files, preventing us from pushing them to a remote repository.
nodemon
refreshes our server, relieving us of the burden of restarting the server whenever we make server-side changes to the code.
Configure environment variables
Create .env
file and configure PORT
, SECRET
and MONGODBURI
.
PORT = 3000
SECRET = "secret"
MONGODBURI = "mongodb://127.0.0.1:27017/nodejs_passport"
I am using a local MongoDB database instance called nodejs_passport
. I do the connection it in the db.js
file and log the result if the connection succeeds.
import mongoose from 'mongoose'
const dbConnection = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODBURI)
console.log(`Db runs in ${conn.connection.host}`)
} catch {
process.exit(1)
}
}
export default dbConnection
Let's start the server to see if everything works as we expect.
// start the server
npm start
// output
Listening on port 3000
Db runs in 127.0.0.1
Model
I tell MongoDB the blueprint of data to store: username, email, password, and registrationDate in the models/User.js
import mongoose from 'mongoose'
const userSchema = mongoose.Schema({
username: { type: 'string', required: true },
email: { type: 'string', required: true, unique: true },
password: { type: 'string', required: true },
registrationDate: { type: Date, default: Date.now }
})
const User = mongoose.model('User', userSchema)
export default User
I receive the data using the form in views/register.ejs
.
<div class="register">
<form autocomplete="off" action="/users/register" method="post">
<label for="username">Username: </label><br>
<input type="text" name="username" id="username" required ><br><br>
<label for="email">Email: </label><br>
<input type="email" name="email" id="email" required ><br><br>
<label for="pwd">Password: </label><br>
<input type="password" name="pwd" id="pwd" required ><br><br>
<label for="pwdConf">Confirm Password: </label><br>
<input type="password" name="pwdConf" id="pwdConf" required ><br><br>
<button>Register</button>
</form>
</div>
In the form, I tell the client to make a POST request to /users/register
, that is found in routes/users.js
.
router.post('/register', async (req, res) => {
const { username, email, pwd, pwdConf } = req.body
// VALIDATION
const errors = []
if(pwd !== pwdConf) errors.push(`Passwords don't match`)
const emailTaken = await User.findOne({ email })
if(emailTaken) errors.push(`Email taken!`)
if(errors.length > 0) res.redirect('/register', { errorMessage: errors})
// REGISTRATION
const hashpwd = await bcrypt.hash(pwd, 12)
let user = new User({ username, email, password: hashpwd })
try {
await user.save()
res.redirect('/users/login')
} catch {
res.redirect('/', { message: "There was a problem register the user" })
}
})
I register the user and redirect them to the login page.
<div class="register">
<form autocomplete="off" action="/users/login" method="post">
<label for="email">Email: </label><br>
<input type="email" name="email" id="email" required><br><br>
<label for="password">Password: </label><br>
<input type="password" name="password" id="password" required><br><br>
<button>Login</button>
</form>
</div>
The form, views/login.ejs
, collects the user details named email and password and sends them to the /users/login
route, accessible through routes => users.js
.
router.post('/login', passport.authenticate('local', {
successRedirect: '/',
failureRedirect: '/users/register'
}))
And that is where Nodejs passport starts user authentication. I am telling passport middleware, "Hey, passport. Authenticate this user whose details are coming through the form. Use your local strategy. If you identify the user, let her see the landing page. Otherwise, redirect her to the register page."
Nodejs passport does all the magic in the passport.js
file, as explained below.
Authentication with Nodejs passport module (step-by-step)
Import modules
I import the modules.
import local_strategy from "passport-local"
import bcrypt from "bcryptjs"
import User from './models/User.js'
I store the Strategy instance in LocaStrategy
.
const LocalStrategy = local_strategy.Strategy
Authenticate the user
I create the main function, authenticateUser()
for user authentication, serialization, and deserialization. I will feed the function with the passport
in the entry file: app.js
. Before that let's focus on how the function authenticates the user.
const authenticateUser = (passport) => {}
I tell the function to globally use the passport middleware, which goes ahead to instantiate a LocalStrategy
. The object, in turn, takes any custom fields and a verification function.
passport.use(new LocalStrategy(customFields, verifyCallback))
Let's donate the attention to the customFields
.
Passport intercepts the login form data and stores it in Fields, for example, username + Field = usernameField. In this case, passport.js attaches the incoming form name
s as username, email, or password.
So, if your form sends the password as any other name other than its default password, for instance, pwd, passport.js won't authenticate the user.
I use the options
const customFields = {
usernameField: 'email',
passwordField: 'password',
}
to inform passport to accept a customized representation other than what its defaults. For instance, I am logging in the users with emails and passwords, NOT the default username and password.
The callback function accepts three parameters: email and password and another callback function, conventionally named done
.
const verifyCallback = (email, password, done) => {
User.findOne({ email })
.then (user => {
if(!user){ return done(null, false) }
else {
const isValid = bcrypt.compareSync(password, user.password)
if(!isValid){
return done(null, false)
}else{
return done(null, user)
}
}
})
}
The done
function tracks authentication errors and the user. I check the user in MongoDB using their email.
If the verifyCallback
function fails to find a user, it returns false with no errors. If a user with the email from the form exists in the database, they get logged in. Before that, Nodejs passport checks if the user has typed the correct password using bcryptjs
module that we imported earlier. If the password is valid, the user gets logged in to the session.
Note: Maintain the order when verifying the password: (password, user.password) NOT (user.password, password).
Cache the user details in sessions
For the client to cache the information, passport.js serializes the user. The most straightforward implication of user serialization is, "Hey passport, grab the authenticated user's id and store in the session in the database."
When the session expires, passport.js
deserializes the user. It implies, "Run a check in the database. If you locate a user with the given id, remove their details from the session. If the process doesn't proceed as expected, catch the errors."
Now that Nodejs passport has got all it needs to authenticate the user, let's run the authenticateUser()
function in the app.js
and make Nodejs passport effective in the entire application.
Globalize the transactions
Finally, let us connect everything we have configured to app.js
.
We are importing the installed modules.
import dotenv from 'dotenv'
import passport from 'passport'
import express from 'express'
import ejsLayout from 'express-ejs-layouts'
import methodOverride from 'method-override'
import session from 'express-session'
import MongoStore from 'connect-mongo'
and local ones
import authenticateUser from './passport.js'
import userRoutes from './routes/users.js'
import indexRoute from './routes/index.js'
import db from './db.js'
Instantiate an express server, start the database connection, and the authenticateUser(passport)
function with passport as an argument.
Scrolling down the page, you get to configure the sessions.
app.use(session({
secret: process.env.SECRET,
resave: false,
saveUninitialized: false,
store: MongoStore.create({ mongoUrl: process.env.MONGODBURI }),
cookie: {
maxAge: 1000* 60 * 60 * 24
}
}))
// passport session
app.use(passport.initialize())
app.use(passport.session())
The secret
validates a session. resave
and saveUninitialized
ask if we want to save the session if nothing changes in the session. The store
property saves the session as a collection in MongoDB. We have set up a cookie that expires after a day.
passport.initialize()
activates the passport middleware so it doesn't go stale while switching between various routes. passport.session()
implements the express session middleware in Nodejs passport.
Conclusion
Despite the usefulness of the Nodejs passport, you may never maximize its potential if you use it without understanding what each line does. Now that you have a deep knowledge of the authentication module, go ahead and implement the strategy of your choice.
Very helpful. I was able to modify your code examples to use azure data tables in lieu of mongoose (they are very similar)
Awesome! More to come on GoLinuxCloud.