In this post, I am going to walk through why the passport-local
authentication strategy is a simple, secure solution for small teams and startups implementing a Node/Express web app.
This post is closely tied to my post on the passport-jwt
strategy, which you can find here.
To help you work through this post, I have created a Github repo with all the code herein: Session Based Auth Repo
Your authentication choices
Above is a high-level overview of the main authentication choices available to developers today. Here is a quick overview of each:
localStorage
). This JWT has assertions about a user and can only be decoded using a secret that is stored on the server.One note I’ll make–Oauth can become confusing really quickly, and therefore is not fully explored in this post. Not only is it unnecessary for a small team/startup getting an application off the ground, but it is also slightly different depending on which service you are using (i.e. Google, Facebook, Github, etc.).
Finally, you might notice that OAuth is listed as “As a service” and “In house”. This is a specific note made to highlight the fact that there is actually a company called “OAuth” that implements the OAuth protocol… As a service. You can implement the OAuth protocol without using OAuth the company’s service!
If we were to create a lineage for these authentication methods, session based authentication would be the oldest of them all, but certainly not obsolete. Session based authentication is at the root of the passport-local
strategy. This method of authentication is “server-side”, which means our Express application and database work together to keep the current authentication status of each user that visits our application.
To understand the basic tenets of session-based-authentication, you need to understand a few concepts:
There are many ways to make an HTTP request in a browser. An HTTP client could be a web application, IoT device, command line (curl), or a multitude of others. Each of these clients connect to the internet and make HTTP requests which either fetch data (GET), or modify data (POST, PUT, DELETE, etc.).
For explanation purposes, let’s assume that:
Server = www.google.com
Client = random guy in a coffee shop working on a laptop
When that random person from the coffee shop types www.google.com
into their Google Chrome browser, this request will be sent with “HTTP Headers”. These HTTP Headers are key:value pairs that provide additional data to the browser to help complete the request. This request will have two types of headers:
To make this interactive, open Google Chrome, open your developer tools (right click, “Inspect”), and click on the “Network” tab. Now, type www.google.com
into your address bar, and watch as the Network tab loads several resources from the server. You should see several columns like Name, Status, Type, Initiator, Size, Time, and Waterfall. Find the request that has “document” as the “Type” value and click on it. You should see all the headers for this request and response interaction.
The request that you (as the client) made will have General and Request headers resembling (but not exact) the following:
General Headers
Request URL: https://www.google.com/
Request Method: GET
Status Code: 200
Request Headers
Accept: text/html
Accept-Language: en-US
Connection: keep-alive
When you typed www.google.com
into your address bar and pressed enter, your HTTP request was sent with these headers (and probably a few others). Although these headers are relatively self-explanatory, I want to walk through a few to get a better idea of what HTTP Headers are used for. Feel free to look up any you don’t know on MDN.
The General
headers can be a mix of both request and response data. Clearly, the Request URL
and Request Method
are part of the request object and they tell the Google Chrome browser where to route your request. The Status Code
is clearly part of the response because it indicates that your GET request was successful and the webpage at www.google.com
loaded okay.
The Request Headers
only contain headers included with the request object itself. You can think of request headers as “instructions for the server”. In this case, my request tells the Google server the following:
There are many more request headers that you can set, but these are just a few common ones that you will probably see on all HTTP requests.
So when you searched for www.google.com
, you sent your request and the headers to the Google Server (for simplicity, we will just assume it is one big server). The Google Server accepted your request, read through the “instructions” (headers), and created a response. The response was comprised of:
As you might have guessed, the “Response Headers” were those set by the Google Server. Here are a few that you might see:
Response Headers
Content-Length: 41485
Content-Type: text/html; charset=UTF-8
Set-Cookie: made_up_cookie_name=some value; expires=Thu, 28-Dec-2020 20:44:50 GMT;
These response headers are fairly straightforward with the exception of the Set-Cookie
header.
I included the Set-Cookie
header because it is exactly what we need to understand in order to learn what Session-based Authentication is all about (and will help us understand other auth methods later in this post).
Without Cookies in the browser, we have a problem.
If we have a protected webpage that we want our users to login to access, without cookies, those users would have to login every time they refresh the page! That is because the HTTP protocol is by default “stateless”.
Cookies introduce the concept of “persistent state” and allow the browser to “remember” something that the server told it previously.
The Google Server can tell my Google Chrome Browser to give me access to a protected page, but the second I refresh the page, my browser will “forget” this and make me authenticate again.
This is where Cookies come in, and explains what the Set-Cookie
header is aiming to do. In the above request where we typed in www.google.com
into our browser and pressed enter, our client sent a request with some headers, and the Google Server responded with a response and some headers. One of these response headers was Set-Cookie: made_up_cookie_name=some value; expires=Thu, 28-Dec-2020 20:44:50 GMT;
. Here’s how this interaction works:
Server: “Hey client! I want you to set a cookie called made_up_cookie_name
and set it equal to some value
.
Client: “Hey server, I will set this on the Cookie
header of all my requests to this domain until Dec 28, 2020!”
We can verify that this actually happened in Google Chrome Developer Tools. Go to “Application”->“Storage” and click “Cookies”. Now click on the site that you are currently visiting and you will see all the cookies that have been set for this site. In our made-up example, you might see something like:
Name Value Expires / Max-Age
made_up_cookie_name some value 2020-12-28T20:44:50.674Z
pl-1.md
example cookie
This cookie will now be set to the Cookie
Request Header on all requests made to www.google.com
until the expiry date set on the cookie.
As you might conclude, this could be extremely useful for authentication if we set some sort of “auth” cookie. An overly simplified process of how this might work would be:
www.example-site.com/login/
into the browser[www.example-site.com](http://www.example-site.com.)
.www.example-site.com
receives the login info, checks the database for that login info, validates the login info, and if successful, creates a response that has the header Set-Cookie: user_is_authenticated=true; expires=Thu, 1-Jan-2020 20:00:00 GMT
.Name Value Expires / Max-Age
user_is_authenticated true 2020-12-28T20:44:50.674Z
6. The random person now visits [www.example-site.com/protected-route/](http://www.example-site.com/protected-route/)
7. The random person’s browser creates an HTTP request with the header Cookie: user_is_authenticated=true; expires=Thu, 1-Jan-2020 20:00:00 GMT
attached to the request.
8. The server receives this request, sees that there is a cookie on the request, “remembers” that it had authenticated this user just a few seconds ago, and allows the user to visit the page.
Obviously, what I have just described would be a highly insecure way to authenticate a user. In reality, the server would create some sort of hash from the password the user provided, and validate that hash with some crypto library on the server.
That said, the high-level concept is valid, and it allows us to understand the value of cookies when talking about authentication.
Keep this example in mind as we move through the remainder of this post.
Sessions and cookies are actually quite similar and can get confused because they can actually be used together quite seamlessly. The main difference between the two is the location of their storage.
In other words, a Cookie is set by the server, but stored in the Browser. If the server wants to use this Cookie to store data about a user’s “state”, it would have to come up with an elaborate scheme to constantly keep track of what the cookie in the browser looks like. It might go something like this:
Set-Cookie: user_auth=true; expires=Thu, 1-Jan-2020 20:00:00 GMT
) next time you request something from meCookie
request headerwww.domain.com/protected
? Here is the cookie you sent me on the last request.Set-Cookie
header (Set-Cookie: marketing_page_visit_count=1; user_ip=192.1.234.21
) because the company that owns me likes to track how many people have visited this specific page and from which computer for marketing purposes.Cookie
request headerwww.domain.com/protected/special-offer
? Here are all the cookies that you have set on me so far. (Cookie: user_auth=true; expires=Thu, 1-Jan-2020 20:00:00 GMT; marketing_page_visit_count=1; user_ip=192.1.234.21
)As you can see, the more pages the browser visits, the more cookies the Server sets, and the more cookies the Browser must attach in each request Header.
The Server might have some function that parses through all the cookies attached to a request and perform certain actions based on the presence or absence of a specific cookie. To me, this naturally begs the question… Why doesn’t the server just keep a record of this information in a database and use a single “session ID” to identify events that a user is taking?
This is exactly what a session is for. As I mentioned, the main difference between a cookie and a session is where they are stored. A session is stored in some Data Store (a fancy term for a database) while a Cookie is stored in the Browser. Since the session is stored on the server, it can store sensitive information. Storing sensitive information in a cookie would be highly insecure.
Now where this all gets a bit confusing is when we talk about using cookies and session together.
Since Cookies are the method in which the client and server communicate metadata (among other HTTP Headers), a session must still utilize cookies. The easiest way to see this interaction is by actually building out a simple authentication application in Node + Express + MongoDB. I will assume that you have a basic understanding of building apps in Express, but I will try to explain each piece as we go.
Setup a basic app:
mkdir session-auth-app
cd session-auth-app
npm init -y
npm install --save express mongoose dotenv connect-mongo express-session passport passport-local
Here is app.js
. Read through the comments to learn more about what is going on before continuing.
const express = require('express');
const mongoose = require('mongoose');
const session = require('express-session');
// Package documentation - https://www.npmjs.com/package/connect-mongo
const MongoStore = require('connect-mongo')(session);
/**
* -------------- GENERAL SETUP ----------------
*/
// Gives us access to variables set in the .env file via `process.env.VARIABLE_NAME` syntax
require('dotenv').config();
// Create the Express application
var app = express();
// Middleware that allows Express to parse through both JSON and x-www-form-urlencoded request bodies
// These are the same as `bodyParser` - you probably would see bodyParser put here in most apps
app.use(express.json());
app.use(express.urlencoded({extended: true}));
/**
* -------------- DATABASE ----------------
*/
/**
* Connect to MongoDB Server using the connection string in the `.env` file. To implement this, place the following
* string into the `.env` file
*
* DB_STRING=mongodb://<user>:<password>@localhost:27017/database_name
*/
const connection = mongoose.createConnection(process.env.DB_STRING);
// Creates simple schema for a User. The hash and salt are derived from the user's given password when they register
const UserSchema = new mongoose.Schema({
username: String,
hash: String,
salt: String
});
// Defines the model that we will use in the app
mongoose.model('User', UserSchema);
/**
* -------------- SESSION SETUP ----------------
*/
/**
* The MongoStore is used to store session data. We will learn more about this in the post.
*
* Note that the `connection` used for the MongoStore is the same connection that we are using above
*/
const sessionStore = new MongoStore({ mongooseConnection: connection, collection: 'sessions' })
/**
* See the documentation for all possible options - https://www.npmjs.com/package/express-session
*
* As a brief overview (we will add more later):
*
* secret: This is a random string that will be used to "authenticate" the session. In a production environment,
* you would want to set this to a long, randomly generated string
*
* resave: when set to true, this will force the session to save even if nothing changed. If you don't set this,
* the app will still run but you will get a warning in the terminal
*
* saveUninitialized: Similar to resave, when set true, this forces the session to be saved even if it is unitialized
*/
app.use(session({
secret: process.env.SECRET,
resave: false,
saveUninitialized: true,
store: sessionStore
}));
/**
* -------------- ROUTES ----------------
*/
// When you visit http://localhost:3000/login, you will see "Login Page"
app.get('/login', (req, res, next) => {
res.send('<h1>Login Page</h1>');
});
app.post('/login', (req, res, next) => {
});
// When you visit http://localhost:3000/register, you will see "Register Page"
app.get('/register', (req, res, next) => {
res.send('<h1>Register Page</h1>');
});
app.post('/register', (req, res, next) => {
});
/**
* -------------- SERVER ----------------
*/
// Server listens on http://localhost:3000
app.listen(3000);
pl-3.js
The first thing we need to do is understand how the express-session
module is working within this application. This is a “middleware”, which is a fancy way of saying that it is a function that modifies something in our application.
Let’s say we had the following code:
const express = require('express');
var app = express();
// Custom middleware
function myMiddleware1(req, res, next) {
req.newProperty = 'my custom property';
next();
}
// Another custom middleware
function myMiddleware2(req, res, next) {
req.newProperty = 'updated value';
next();
}
app.get('/', (req, res, next) => {
res.send(`<h1>Custom Property Value: ${req.newProperty}`);
});
// Server listens on http://localhost:3000
app.listen(3000);
middleware.js
As you can see, this is an extremely simple Express application that defines two middlewares and has a single route that you can visit in your browser at http://localhost:3000
. If you started this application and visited that route, it would say “Custom Property Value: undefined” because defining middleware functions alone is not enough.
We need to tell the Express application to actually use these middlewares. We can do this in a few ways. First, we can do it within a route.
app.get('/', myMiddleware1, (req, res, next) => {
res.send(`<h1>Custom Property Value: ${req.newProperty}`);
});
If you add the first middleware function as an argument to the route, you will now see “Custom Property Value: my custom property” show up in the browser. What really happened here:
http://localhost:3000/
in the browser, which triggered the app.get()
function.app.get()
function and noticed that there was a middleware function installed before the callback. The application ran the middleware and passed the middleware the req
object, res
object, and the next()
callback.myMiddleware1
middleware first set req.newProperty
, and then called next()
, which tells the Express application “Go to the next middleware”. If the middleware did not call next()
, the browser would get “stuck” and not return anything.This is just one way to use middleware, and it is exactly how the passport.authenticate()
function (more on this later, so keep in mind) works.
Another way we can use middleware is by setting it “globally”. Take a look at our app after this change:
const express = require('express');
var app = express();
// Custom middleware
function myMiddleware1(req, res, next) {
req.newProperty = 'my custom property';
next();
}
// Another custom middleware
function myMiddleware2(req, res, next) {
req.newProperty = 'updated value';
next();
}
app.use(myMiddleware2);
app.get('/', myMiddleware1, (req, res, next) => {
// Sends "Custom Property Value: my custom property
res.send(`<h1>Custom Property Value: ${req.newProperty}`);
});
// Server listens on http://localhost:3000
app.listen(3000);
pl-5.js
middleware.js
With this app structure, you will notice that visiting http://localhost:3000/
in the browser still returns the same value as before. This is because the app.use(myMiddleware2)
middleware is happening before the app.get('/', myMiddleware1)
. If we removed the middleware from the route, you will see the updated value in the browser.
app.use(myMiddleware2);
app.get('/', (req, res, next) => {
// Sends "Custom Property Value: updated value
res.send(`<h1>Custom Property Value: ${req.newProperty}`);
});
We could also get this result by placing the second middleware after the first within the route.
app.get('/', myMiddleware1, myMiddleware2, (req, res, next) => {
// Sends "Custom Property Value: updated value
res.send(`<h1>Custom Property Value: ${req.newProperty}`);
});
Although this is a quick and high-level overview of middleware in Express, it will help us understand what is going on with the express-session
middleware.
As I mentioned before, the express-session
module gives us middleware that we can use in our application. The middleware is defined in this line:
// Again, here is the documentation for this - https://www.npmjs.com/package/express-session
app.use(session({
secret: process.env.SECRET,
resave: false,
saveUninitialized: true,
store: sessionStore
}));
Here is a brief overview of what the Express Session Middleware is doing:
connect-mongo
custom Session Store).connect.sid
Cookie to the HTTP request.connect.sid
. It then attaches the Set-Cookie
HTTP header to the res
object with the hashed value (Set-Cookie: connect.sid=hashed value
).You might be wondering why this is useful at all, and how all this actually works.
If you remember from the quick refresher on Express Middlewares, I said that a middleware has the ability to alter the req
and res
objects that are passed from one middleware to the next until it reaches the end of the HTTP request. Just like we set a custom property on the req
object, we could also set something much more complex like a session
object that has properties, methods, etc.
That is exactly what the express-session
middleware does. When a new session is created, the following properties are added to the req
object:
req.sessionID
- A randomly generated UUID. You can define a custom function to generate this ID by setting the genid
option. If you do not set this option, the default is to use the uid-safe
module.app.use(session({
genid: function (req) {
// Put your UUID implementation here
}
}));
req.session
- The Session object. This contains information about the session and is available for setting custom properties to use. For example, maybe you want to track how many times a particular page is loaded in a single session:app.get('/tracking-route', (req, res, next) => {
if (req.session.viewCount) {
req.session.viewCount = req.session.viewCount + 1;
} else {
req.session.viewCount = 1;
}
res.send("<p>View count is: " + req.session.viewCount + "</p>");
});
req.session.cookie
- The Cookie object. This defines the behavior of the cookie that stores the hashed session ID in the browser. Remember, once the cookie has been set, the browser will attach it to every HTTP request automatically until it expires.There is one last thing that we need to learn in order to fully understand Session-Based Authentication–Passport JS.
Passport JS has over 500 authentication “Strategies” that can be used within a Node/Express app. Many of these strategies are highly specific (i.e. passport-amazon
allows you to authenticate into your app via Amazon credentials), but they all work similar within your Express app.
In my opinion, the Passport module could use some work in the department of documentation. Not only does Passport consist of two modules (Passport base + Specific Strategy), but it is also a middleware, which as we saw is a bit confusing in its own right. To add to the confusion, the strategy that we are going to walk through (passport-local
) is a middleware that modifies an object created by another middleware (express-session
). Since the Passport documentation has little to say around how this all works, I will attempt to explain it to the best of my ability in this post.
Let’s first walk through the setup of the module.
If you have been following along with this tutorial, you already have the modules needed. If not, you will need to install Passport and a Strategy to your project.
npm install --save passport passport-local
Once you have done that, you will need to implement Passport within your application. Below, I have added all the pieces you need for the passport-local
strategy. I have removed comments to simplify. Take a quick read through the code and then we will walk through all of the // NEW
code.
const express = require('express');
const mongoose = require('mongoose');
const session = require('express-session');
// NEW
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
var crypto = require('crypto');
// ---
const MongoStore = require('connect-mongo')(session);
require('dotenv').config();
var app = express();
const connection = mongoose.createConnection(process.env.DB_STRING);
const UserSchema = new mongoose.Schema({
username: String,
hash: String,
salt: String
});
mongoose.model('User', UserSchema);
const sessionStore = new MongoStore({ mongooseConnection: connection, collection: 'sessions' })
app.use(session({
secret: process.env.SECRET,
resave: false,
saveUninitialized: true,
store: sessionStore
}));
// NEW
// START PASSPORT
function validPassword(password, hash, salt) {
var hashVerify = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex');
return hash === hashVerify;
}
function genPassword(password) {
var salt = crypto.randomBytes(32).toString('hex');
var genHash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex');
return {
salt: salt,
hash: genHash
};
}
passport.use(new LocalStrategy(
function(username, password, cb) {
User.findOne({ username: username })
.then((user) => {
if (!user) { return cb(null, false) }
// Function defined at bottom of app.js
const isValid = validPassword(password, user.hash, user.salt);
if (isValid) {
return cb(null, user);
} else {
return cb(null, false);
}
})
.catch((err) => {
cb(err);
});
}));
passport.serializeUser(function(user, cb) {
cb(null, user.id);
});
passport.deserializeUser(function(id, cb) {
User.findById(id, function (err, user) {
if (err) { return cb(err); }
cb(null, user);
});
});
app.use(passport.initialize());
app.use(passport.session());
// ---
// END PASSPORT
app.get('/login', (req, res, next) => {
res.send('<h1>Login Page</h1>');
});
app.post('/login', (req, res, next) => {});
app.get('/register', (req, res, next) => {
res.send('<h1>Register Page</h1>');
});
app.post('/register', (req, res, next) => {});
app.listen(3000);
pl-6.js
app.js
Yes, I know there is a lot to take in here. Let’s start with the easy parts–the helper functions. In the code above, I have two helper functions that will assist in creating and validating a password.
/**
*
* @param {*} password - The plain text password
* @param {*} hash - The hash stored in the database
* @param {*} salt - The salt stored in the database
*
* This function uses the crypto library to decrypt the hash using the salt and then compares
* the decrypted hash/salt with the password that the user provided at login
*/
function validPassword(password, hash, salt) {
var hashVerify = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex');
return hash === hashVerify;
}
/**
*
* @param {*} password - The password string that the user inputs to the password field in the register form
*
* This function takes a plain text password and creates a salt and hash out of it. Instead of storing the plaintext
* password in the database, the salt and hash are stored for security
*
* ALTERNATIVE: It would also be acceptable to just use a hashing algorithm to make a hash of the plain text password.
* You would then store the hashed password in the database and then re-hash it to verify later (similar to what we do here)
*/
function genPassword(password) {
var salt = crypto.randomBytes(32).toString('hex');
var genHash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex');
return {
salt: salt,
hash: genHash
};
}
pl-7.js
passwordUtils.js
In addition to the comments, I’ll note that these functions require the NodeJS built-in crypto
library. Some would argue a better crypto library, but unless your application requires a high degree of security, this library is plenty sufficient!
Next up, let’s take a look at the passport.use()
method.
/**
* This function is called when the `passport.authenticate()` method is called.
*
* If a user is found an validated, a callback is called (`cb(null, user)`) with the user
* object. The user object is then serialized with `passport.serializeUser()` and added to the
* `req.session.passport` object.
*/
passport.use(new LocalStrategy(
function(username, password, cb) {
User.findOne({ username: username })
.then((user) => {
if (!user) { return cb(null, false) }
// Function defined at bottom of app.js
const isValid = validPassword(password, user.hash, user.salt);
if (isValid) {
return cb(null, user);
} else {
return cb(null, false);
}
})
.catch((err) => {
cb(err);
});
}));
pl-8.js
Passport JS Configuration
I know the above function is quite a lot to look at, so let’s explore some of its key components. First, I’ll mention that with all Passport JS authentication strategies (not just the local strategy we are using), you will need to supply it with a callback that will be executed when you call the passport.authenticate()
method. For example, you might have a login route in your app:
app.post('/login', passport.authenticate('local', { failureRedirect: '/login' }), (err, req, res, next) => {
if (err) next(err);
console.log('You are logged in!');
});
Your user will type in their username and password via a login form, which will create an HTTP POST request to the /login
route. Let’s say your post request contained the following data:
{
"email": "[email protected]",
"pw": "sample password"
}
This WILL NOT WORK. The reason? Because the passport.use()
method expects your POST request to have the following fields:
{
"username": "[email protected]",
"password": "sample password"
}
It looks for username
and password
field. If you wanted the first json request body to work, you would need to supply the passport.use()
function with field definitions:
passport.use(
{
usernameField: 'email',
passwordField: 'pw'
},
function (email, password, callback) {
// Implement your callback function here
}
);
By defining the usernameField
and passwordField
, you can specify a custom POST request body object.
That aside, let’s return to the POST request at the /login
route:
app.post('/login', passport.authenticate('local', { failureRedirect: '/login' }), (err, req, res, next) => {
if (err) next(err);
console.log('You are logged in!');
});
When the user submits his/her login credentials, the passport.authenticate()
method (used as middleware here) will execute the callback that you have defined and supply it with the username
and password
from the POST request body. The passport.authenticate()
method takes two parameters–the name of the strategy, and options. The default strategy name here is local
, but you could change this like so:
// Supply a name string as the first argument to the passport.use() function
passport.use('custom-name', new Strategy());
// Use the same name as above
app.post('/login', passport.authenticate('custom-name', { failureRedirect: '/login' }), (err, req, res, next) => {
if (err) next(err);
console.log('You are logged in!');
});
The way I have used the passport.authenticate()
strategy will first execute the callback function that we defined within new LocalStrategy()
, and if the authentication is successful, it will call the next()
function, and we will enter the route. If authentication was not successful (invalid username or password), the app will redirect to the /login
route again.
Now that we understand how it is used, let’s return to the callback function that we defined earlier and that passport.authenticate()
is using.
// Tells Passport to use this strategy for the passport.authenticate() method
passport.use(new LocalStrategy(
// Here is the function that is supplied with the username and password field from the login POST request
function(username, password, cb) {
// Search the MongoDB database for the user with the supplied username
User.findOne({ username: username })
.then((user) => {
/**
* The callback function expects two values:
*
* 1. Err
* 2. User
*
* If we don't find a user in the database, that doesn't mean there is an application error,
* so we use `null` for the error value, and `false` for the user value
*/
if (!user) { return cb(null, false) }
/**
* Since the function hasn't returned, we know that we have a valid `user` object. We then
* validate the `user` object `hash` and `salt` fields with the supplied password using our
* utility function. If they match, the `isValid` variable equals True.
*/
const isValid = validPassword(password, user.hash, user.salt);
if (isValid) {
// Since we have a valid user, we want to return no err and the user object
return cb(null, user);
} else {
// Since we have an invalid user, we want to return no err and no user
return cb(null, false);
}
})
.catch((err) => {
// This is an application error, so we need to populate the callback `err` field with it
cb(err);
});
}));
pl-9.js
Passport Configuration
I have commented the above in great detail, so be sure to read through before moving on.
As you may notice, the callback function is database agnostic and validation agnostic. In other words, we don’t need to use MongoDB nor do we need to validate our passwords in the same way. PassportJS leaves this up to us! This can be confusing, but is also extremely powerful and is why PassportJS has such widespread adoption.
Next, you’ll see two related functions:
passport.serializeUser(function(user, cb) {
cb(null, user.id);
});
passport.deserializeUser(function(id, cb) {
User.findById(id, function (err, user) {
if (err) { return cb(err); }
cb(null, user);
});
});
Personally, I found these two functions to be the most confusing because there is not a lot of documentation around them. We will further explore what these functions are doing when we talk about how PassportJS and Express Session middleware interact, but in short, these two functions are responsible for “serializing” and “deserializing” users to and from the current session object.
Instead of storing the entire user
object in the session, we only need to store the database ID for the user. When we need to get more information about the user in the current session, we can use the deserialize function to look the user up in the database using the ID that was stored in the session. Again, we will make more sense of this soon.
Finally, with the Passport implementation, you will see two more lines of code:
app.use(passport.initialize());
app.use(passport.session());
If you remember from earlier in the post on how middleware works, by calling app.use()
, we are telling Express to execute the functions within the parentheses in order on every request.
In other words, for every HTTP request our Express app makes, it will execute passport.initialize()
and passport.session()
.
Something seem weird here??
If app.use()
executes the function contained within, then the above syntax is like saying:
passport.initialize()();
passport.session()();
The reason this works is because these two functions actually return another function! Kind of like this:
Passport.prototype.initialize = function () {
// Does something
return function () {
// This is what is called by `app.use()`
}
}
This is not necessary to know to use Passport, but definitely clears up some confusion if you were wondering about that syntax.
Anyways…
These two middleware functions are necessary for integrating PassportJS with express-session
middleware. That is why these two functions must come AFTER the app.use(session({}))
middleware! Just like passport.serializeUser()
and passport.deserializeUser()
, these middlewares will make much more sense shortly.
Now that we understand HTTP Headers, Cookies, Middleware, Express Session middleware, and Passport JS middleware, it is finally time to learn how to use these to authenticate users into our application. I want to first use this section to review and explain the conceptual flow, and then dive into the implementation in the next section.
Here is a basic flow of our app:
http://www.expressapp.com
(just assume this is true for the sake of the example).http://www.expressapp.com/login
in the browserexpress-session
middleware realizes that there is a user connecting to the Express server. It checks the Cookie
HTTP header on the req
object. Since this user is visiting for the first time, there is no value in the Cookie
header. Because there is no Cookie
value, the Express server returns the /login
HTML and calls the Set-Cookie
HTTP header. The Set-Cookie
value is the cookie string generated by express-session
middleware according to the options set by the developer (assume in this case the maxAge value is 10 days).http://www.expressapp.com/login
again.express-session
middleware runs on the GET request, checks the Cookie
HTTP header, but this time, finds a value! This is because the user had previously created a session earlier that day. Since the maxAge
option was set to 10 days on the express-session
middleware, closing the browser does not destroy the cookie.express-session
middleware now takes the connect.sid
value from the Cookie
HTTP header, looks it up in the MongoStore
(fancy way to say that it looks up the id in the database in the sessions
collection), and finds it. Since the session exists, the express-session
middleware does not do anything, and both the Cookie
HTTP header value and the MongoStore
database entry in the sessions
collection stays the same./login
route, which uses the passport.authenticate()
middleware.passport.initialize()
and passport.session()
middlewares have been running. On each request, these middlewares are checking the req.session
object (created by the express-session
middleware) for a property called passport.user
(i.e. req.session.passport.user
). Since the passport.authenticate()
method had not been called yet, the req.session
object did not have a passport
property. Now that the passport.authenticate()
method has been called via the POST request to /login
, Passport will execute our user-defined authentication callback using the username and password our user typed in and submitted.passport.authenticate()
method now returns the user
object that was validated. In addition, it attaches the req.session.passport
property to the req.session
object, serializes the user via passport.serializeUser()
, and attaches the serialized user (i.e. the ID of the user) to the req.session.passport.user
property. Finally, it attaches the full user object to req.user
.express-session
middleware checks the Cookie
HTTP header on req
, finds the session from yesterday (still valid since our maxAge
was set to 10 days), looks it up in MongoStore
, finds it, and does nothing to the Cookie
since the session is still valid. The middleware re-initializes the req.session
object and sets to the value returned from MongoStore
.passport.initialize()
middleware checks the req.session.passport
property and sees that there is still a user
value there. The passport.session()
middleware uses the user
property found on req.session.passport.user
to re-initialize the req.user
object to equal the user attached to the session via the passport.deserializeUser()
function.req.session.passport.user
exists. Since the Passport middleware just re-initialized it, it does, and the protected route allows the user access.express-session
middleware runs, realizes that the value of the Cookie
HTTP header has an expired cookie value, and replaces the Cookie
value with a new Session via the Set-Cookie
HTTP header attached to the res
object.passport.initialize()
and passport.session()
middlewares run, but this time, since express-session
middleware had to create a new session, there is no longer a req.session.passport
object!req.session.passport.user
exists. Since it doesn’t, access is denied!passport.authenticate()
middleware, the req.session.passport
object will be re-established, and the user will again be able to visit protected routes.Phewwww…
Got all that?
The hard part is over.
Putting everything together, below is your full functional Session Based authentication Express app. Below is the app contained within a single file, but I have also refactored this application closer to what you would use in the real world in this repository.
const express = require('express');
const mongoose = require('mongoose');
const session = require('express-session');
var passport = require('passport');
var crypto = require('crypto');
var LocalStrategy = require('passport-local').Strategy;
// Package documentation - https://www.npmjs.com/package/connect-mongo
const MongoStore = require('connect-mongo')(session);
/**
* -------------- GENERAL SETUP ----------------
*/
// Gives us access to variables set in the .env file via `process.env.VARIABLE_NAME` syntax
require('dotenv').config();
// Create the Express application
var app = express();
app.use(express.json());
app.use(express.urlencoded({extended: true}));
/**
* -------------- DATABASE ----------------
*/
/**
* Connect to MongoDB Server using the connection string in the `.env` file. To implement this, place the following
* string into the `.env` file
*
* DB_STRING=mongodb://<user>:<password>@localhost:27017/database_name
*/
const conn = 'mongodb://devuser:[email protected]:27017/general_dev';
//process.env.DB_STRING
const connection = mongoose.createConnection(conn, {
useNewUrlParser: true,
useUnifiedTopology: true
});
// Creates simple schema for a User. The hash and salt are derived from the user's given password when they register
const UserSchema = new mongoose.Schema({
username: String,
hash: String,
salt: String
});
const User = connection.model('User', UserSchema);
/**
* This function is called when the `passport.authenticate()` method is called.
*
* If a user is found an validated, a callback is called (`cb(null, user)`) with the user
* object. The user object is then serialized with `passport.serializeUser()` and added to the
* `req.session.passport` object.
*/
passport.use(new LocalStrategy(
function(username, password, cb) {
User.findOne({ username: username })
.then((user) => {
if (!user) { return cb(null, false) }
// Function defined at bottom of app.js
const isValid = validPassword(password, user.hash, user.salt);
if (isValid) {
return cb(null, user);
} else {
return cb(null, false);
}
})
.catch((err) => {
cb(err);
});
}));
/**
* This function is used in conjunction with the `passport.authenticate()` method. See comments in
* `passport.use()` above ^^ for explanation
*/
passport.serializeUser(function(user, cb) {
cb(null, user.id);
});
/**
* This function is used in conjunction with the `app.use(passport.session())` middleware defined below.
* Scroll down and read the comments in the PASSPORT AUTHENTICATION section to learn how this works.
*
* In summary, this method is "set" on the passport object and is passed the user ID stored in the `req.session.passport`
* object later on.
*/
passport.deserializeUser(function(id, cb) {
User.findById(id, function (err, user) {
if (err) { return cb(err); }
cb(null, user);
});
});
/**
* -------------- SESSION SETUP ----------------
*/
/**
* The MongoStore is used to store session data. We will learn more about this in the post.
*
* Note that the `connection` used for the MongoStore is the same connection that we are using above
*/
const sessionStore = new MongoStore({ mongooseConnection: connection, collection: 'sessions' })
/**
* See the documentation for all possible options - https://www.npmjs.com/package/express-session
*
* As a brief overview (we will add more later):
*
* secret: This is a random string that will be used to "authenticate" the session. In a production environment,
* you would want to set this to a long, randomly generated string
*
* resave: when set to true, this will force the session to save even if nothing changed. If you don't set this,
* the app will still run but you will get a warning in the terminal
*
* saveUninitialized: Similar to resave, when set true, this forces the session to be saved even if it is unitialized
*
* store: Sets the MemoryStore to the MongoStore setup earlier in the code. This makes it so every new session will be
* saved in a MongoDB database in a "sessions" table and used to lookup sessions
*
* cookie: The cookie object has several options, but the most important is the `maxAge` property. If this is not set,
* the cookie will expire when you close the browser. Note that different browsers behave slightly differently with this
* behaviour (for example, closing Chrome doesn't always wipe out the cookie since Chrome can be configured to run in the
* background and "remember" your last browsing session)
*/
app.use(session({
//secret: process.env.SECRET,
secret: 'some secret',
resave: false,
saveUninitialized: true,
store: sessionStore,
cookie: {
maxAge: 1000 * 30
}
}));
/**
* -------------- PASSPORT AUTHENTICATION ----------------
*/
/**
* Notice that these middlewares are initialized after the `express-session` middleware. This is because
* Passport relies on the `express-session` middleware and must have access to the `req.session` object.
*
* passport.initialize() - This creates middleware that runs before every HTTP request. It works in two steps:
* 1. Checks to see if the current session has a `req.session.passport` object on it. This object will be
*
* { user: '<Mongo DB user ID>' }
*
* 2. If it finds a session with a `req.session.passport` property, it grabs the User ID and saves it to an
* internal Passport method for later.
*
* passport.session() - This calls the Passport Authenticator using the "Session Strategy". Here are the basic
* steps that this method takes:
* 1. Takes the MongoDB user ID obtained from the `passport.initialize()` method (run directly before) and passes
* it to the `passport.deserializeUser()` function (defined above in this module). The `passport.deserializeUser()`
* function will look up the User by the given ID in the database and return it.
* 2. If the `passport.deserializeUser()` returns a user object, this user object is assigned to the `req.user` property
* and can be accessed within the route. If no user is returned, nothing happens and `next()` is called.
*/
app.use(passport.initialize());
app.use(passport.session());
/**
* -------------- ROUTES ----------------
*/
app.get('/', (req, res, next) => {
res.send('<h1>Home</h1>');
});
// When you visit http://localhost:3000/login, you will see "Login Page"
app.get('/login', (req, res, next) => {
const form = '<h1>Login Page</h1><form method="POST" action="/login">\
Enter Username:<br><input type="text" name="username">\
<br>Enter Password:<br><input type="password" name="password">\
<br><br><input type="submit" value="Submit"></form>';
res.send(form);
});
// Since we are using the passport.authenticate() method, we should be redirected no matter what
app.post('/login', passport.authenticate('local', { failureRedirect: '/login-failure', successRedirect: 'login-success' }), (err, req, res, next) => {
if (err) next(err);
});
// When you visit http://localhost:3000/register, you will see "Register Page"
app.get('/register', (req, res, next) => {
const form = '<h1>Register Page</h1><form method="post" action="register">\
Enter Username:<br><input type="text" name="username">\
<br>Enter Password:<br><input type="password" name="password">\
<br><br><input type="submit" value="Submit"></form>';
res.send(form);
});
app.post('/register', (req, res, next) => {
const saltHash = genPassword(req.body.password);
const salt = saltHash.salt;
const hash = saltHash.hash;
const newUser = new User({
username: req.body.username,
hash: hash,
salt: salt
});
newUser.save()
.then((user) => {
console.log(user);
});
res.redirect('/login');
});
/**
* Lookup how to authenticate users on routes with Local Strategy
* Google Search: "How to use Express Passport Local Strategy"
*
* Also, look up what behaviour express session has without a maxage set
*/
app.get('/protected-route', (req, res, next) => {
console.log(req.session);
if (req.isAuthenticated()) {
res.send('<h1>You are authenticated</h1>');
} else {
res.send('<h1>You are not authenticated</h1>');
}
});
// Visiting this route logs the user out
app.get('/logout', (req, res, next) => {
req.logout();
res.redirect('/login');
});
app.get('/login-success', (req, res, next) => {
console.log(req.session);
res.send('You successfully logged in.');
});
app.get('/login-failure', (req, res, next) => {
res.send('You entered the wrong password.');
});
/**
* -------------- SERVER ----------------
*/
// Server listens on http://localhost:3000
app.listen(3000);
/**
* -------------- HELPER FUNCTIONS ----------------
*/
/**
*
* @param {*} password - The plain text password
* @param {*} hash - The hash stored in the database
* @param {*} salt - The salt stored in the database
*
* This function uses the crypto library to decrypt the hash using the salt and then compares
* the decrypted hash/salt with the password that the user provided at login
*/
function validPassword(password, hash, salt) {
var hashVerify = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex');
return hash === hashVerify;
}
/**
*
* @param {*} password - The password string that the user inputs to the password field in the register form
*
* This function takes a plain text password and creates a salt and hash out of it. Instead of storing the plaintext
* password in the database, the salt and hash are stored for security
*
* ALTERNATIVE: It would also be acceptable to just use a hashing algorithm to make a hash of the plain text password.
* You would then store the hashed password in the database and then re-hash it to verify later (similar to what we do here)
*/
function genPassword(password) {
var salt = crypto.randomBytes(32).toString('hex');
var genHash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex');
return {
salt: salt,
hash: genHash
};
}
pl-10.js
app.js
That is the end of our passport-local
authentication tutorial, but I have a closely related tutorial on the passport-jwt
strategy that will help you gain a widespread understanding of different user authentication flows.
As we transition from talking about session-based authentication to JWT based authentication, it is important to keep our authentication flows clear. To do a quick review, the basic auth flow of a session-based authentication app is like so:
/login
route on the Express application serverpassport-local
middleware will attach the user to the current session.What I want you to notice about this flow is the fact that the user only had to type in his username and password one time, and for the remainder of the session, he can visit protected routes. The session cookie is automatically attached to all of his requests because this is the default behavior of a web browser and how cookies work! In addition, each time a request is made, the Passport middleware and Express Session middleware will be making a query to our database to retrieve session information. In other words, to authenticate a user, a database is required.
Now skipping forward, you’ll begin to notice that with JWTs, there is absolutely no database required on each request to authenticate users. Yes, we will need to make one database request to initially authenticate a user and generate a JWT, but after that, the JWT will be attached in the Authorization
HTTP header (as opposed to Cookie
header), and no database is required.
☞ JavaScript Programming Tutorial Full Course for Beginners
☞ Getting Started with Node.js - Full Tutorial
☞ Learn JavaScript - Become a Zero to Hero
☞ Angular and Nodejs Integration Tutorial
☞ How To Create A Password Protected File Sharing Site With Node.js, MongoDB, and Express