Passport.js
What is Passport?
Passport is a middleware that is used in Express.js apps that handles user authentication. It is more so a framework than a library, meaning that it's not just onenpm installaway from being ready. There are multiple libraries that work together to create a seamless authentication experience.What problem does Passport solve?
After using Passport one quickly realizes that it is not that simple. Actually it might be, but the documentation on their website makes it hard to figure out what you are supposed to do. So, why would anyone want to use it?Well, we shouldn't always use it. Even though it has support for what it calls a local-strategy, meaning custom authentication with username and password, this is not what it excels at. Simpler yet effective authentication systems can be built relatively easy by using JWT's or even a completely custom AUTH method. There are multiple ways to do that afterall.
What Passport does really well for us is that it provides an easy way to authenticate ourselves when connecting to other applications, such as Google or Github. Even though that documentation is also lacking, it is still easier to connect to an external service by using a pre-built library that handles everything for us than building our own. Getting the grasp of how 'local' authentication works is going to make it much easier to work with other strategies as well, and thankfully there are guides out there that help with that.
What is a Strategy?
A Strategy is a specific set of libraries that utilizes Passport's core functionalities for a certain service.Like I mentioned earlier, Passport's strength lies in authenticating with third party apps. It is also a framework, meaning we need multiple libraries for it to run. So, depending on where we are trying to authenticate ourselves, Passport provides us with different Strategies - libaries that work out-of-the box. For example, there is a specific Strategy for when trying to connect to Facebook: it has the right functions and data that Facebook requires to authenticate ourselves.
Example
Below is a small application that handles local authentication to demonstrate its usage.app.js
1const express = require("express");
2const passport = require("passport");
3const session = require("express-session");
4const LocalStrategy = require("passport-local").Strategy;
5
6const app = express();
7
8require("dotenv").config();
9
10app.use(express.urlencoded({ extended: true }));
11app.use(express.static("public"));
12app.use(express.json());
13
14const users = [{
15 id: 1,
16 username: '1',
17 password: '1'
18}];
19
20/**
21 * this is the strategy that passport will
22 * use to authenticate the user
23 * i have broken it into parts for readability
24 * first the verifyCallback function. that
25 * is the function called when we call passport.authenticate();
26 * second, the localStrategy object. it only
27 * takes one argument, the verifyCallback function
28 * third, the passport.use() method. it
29 * takes the localStrategy object as an argument
30 * NOTE: 'LocalStrategy' is used because we
31 * are using custom username and password
32 * there are respective strategies for other authentication
33 * methods like OAuth, Google, Facebook, etc.
34 */
35const verifyCallback = (username, password, done) => {
36 try {
37 // mock logic to check to see if user exists in database
38 const foundUser = users.find(user => user.username === username);
39
40 // 'done' is a callback function that takes three arguments: error, user, info
41 if (!foundUser) return done(null, false, { message: 'User does not exist in database' });
42
43 const isPasswordValid = password === foundUser.password;
44
45 if (!isPasswordValid) return done(null, false, { message: 'Password is incorrect' });
46
47 return done(null, foundUser);
48 } catch (err) {
49 done(err);
50 }
51}
52
53const localStrategy = new LocalStrategy(verifyCallback)
54
55passport.use(localStrategy);
56
57/**
58 * serializeUser is used to 'inject' the user into the session (req.session.passport.user)
59 * */
60passport.serializeUser(function (user, cb) {
61 cb(null, user.id);
62});
63
64// deserializeUser is used to 'extract' the user from the session
65// it finds the user in the database
66// using 'req.passport.user.id' and creates the req.user object
67passport.deserializeUser(function (id, cb) {
68 const user = users.find(user => user.id === id);
69 cb(null, user);
70});
71
72// this keeps the session alive and stores it in the browser's cookies
73// and then retrieves it on every request
74// passport-local has built-in support for this library
75// in fact, passport will throw an error if it is missing
76// if you comment out the code below and try to load a page, you will get
77// "Error: Login sessions require session support.
78// Did you forget to use `express-session` middleware?"
79app.use(session({
80 secret: 'secret',
81 resave: false,
82 saveUninitialized: true,
83 cookie: { secure: false, maxAge: 1000 * 30 }, // 30 seconds
84}));
85
86// this is probably the most important part of the setup
87// it initializes the passport middleware and
88// on every request, it checks if 'req.session.passport'
89// exists (session exists)
90// if it finds it, it saves it internally in order to be
91// used later
92app.use(passport.initialize());
93// in case we find a session, the user's id is retrieved and sent
94// to the deserializeUser function created above to match the user to the id
95app.use(passport.session());
96
97app.get("/", (req, res) => {
98 res.sendFile(__dirname + "/index.html");
99});
100
101app.get("/login", (_, res) => {
102 res.sendFile(__dirname + "/login.html");
103});
104
105app.get("/failed-login", (_, res) => {
106 res.sendFile(__dirname + "/failed-login.html");
107});
108
109app.post('/login', passport.authenticate('local',
110{
111 successRedirect: '/protected-route',
112 failureRedirect: '/failed-login'
113}));
114
115// if you try refreshing this page while logged in, you
116// will see that you will not be redirected.
117// if you open the browser's developer tools and look
118// at the application tab, you will see the cookie stored
119// in the browser's cookies, connect.sid
120// if you delete the cookie and refresh the page, you
121// will be redirected to the login page
122app.get('/protected-route', (req, res) => {
123 if (req.user && req.isAuthenticated()) {
124 res.sendFile(__dirname + "/protected-route.html");
125 } else {
126 res.redirect('/login');
127 }
128});
129
130// this is the logout route
131// you could make it into a POST request if you want to be more secure
132// but for simplicity, we will use a GET request
133// NOTE: you might notice that after logging out, we still have
134// a connect.sid cookie in the browser's cookies
135// this is NOT the same cookie
136// every time the page gets refreshed, a new cookie is created,
137// unless we store user session data in a database, which we are
138// not doing in this example
139// if we were using connect-mongo for example, the cookie would be
140// the same in every request until the session expires, then a new
141// one would be assigned
142// the cookie itself is just a string that is used to identify the
143// session in the database, not the user themselves, since the user
144// object is empty in the requests and passport objects.
145app.get('/logout', (req, res,) => {
146 req.logout((err) => {
147 if (err) {
148 return next(err);
149 }
150 });
151 res.redirect('/');
152});
153
154app.get("/", (error, req, res, next) => {
155 console.error(error);
156 res.status(500).send("Internal Server Error");
157});
158
159app.listen(3000, () => {
160 console.log("Server is running on port 3000")
161});Conclusion
Passport is a very handy tool that can make authentication a lot easier once we grasp how it works and really understand what is going on behind the scenes. I am not certain that I understand it at its fullest yet. I would like to thank and shout out Zach Gollwitzer for creating an awesome guide. I had tried to learn about Passport before and I had a really hard time understanding its intricacies. Even though now I have a few years of experience as a developer, Zach's guide helped me get a better grasp of the framework and its capabilities.Zach's Course
Zach's Youtube Channel