Building HttpOnly Cookie JWT Authentication With Passport.js
Passport is an authentication middleware that can be easily dropped into your Express app. It’s a great solution for those wanting an easy-to-maintain and understand flow of authenticating users with their APIs. Chances are you knew that already, and you’re here because of countless search engine dead-ends. Let’s get right into it!
Project Setup
If you’re here with an existing project, feel free to skip to the next section. If not, let’s go setup the sample project.
Create a directory, like say node-cookie-jwt
and maybe a src
folder inside it. After that, create a script called server.js
in the src
directory. Then using any terminal, use the command npm init -y
to initialize the Node Package Manager. Finally, create a file to store your environment variables.
Summary
node-cookie-jwt
|-- dev.env
|-- src
| |-- server.js
|-- package.json
Installing Dependencies
Run this command to install the complete dependencies for the project.
$ npm install express jsonwebtoken passport passport-jwt cookie-parser
This has Express for our easily creating our APIs, Jsonwebtoken for signing and verifying JWTs, the Passport core with the JWT Strategy, and finally Cookie Parser for Express. Then feel free to use either dotenv
or env-cmd
for the environment variable file (or ditch it altogether and just set env vars on your command line).
Environment Variables
There won’t be much inside our dev.env
file, just stuff we normally wouldn’t want include in our version control, or publicly visible source code. We will also simulate user data with these.
USER=yourUser
PASSWORD=verySecurePassw0rd123!
JWT_SECRET=wow123
JWT_EXPIRATION_TIME=600000
[OPTIONAL] Babel and ES6
It’s important to note that I’ll be writing code in mostly ES6, therefore I have Babel as a development dependency. You may or may not install these. It’s all up to your comfort zone.
Application Bootstrap
This creates our application’s starting point. A key takeaway from this is that we need to import Passport and the strategies we’re about to create. While it seems obvious, this is rather easy to miss, and a common mistake to make.
You may comment out those non-existent imports to test out if the server works or not. Otherwise, we’ll move on to setting up the Passport Strategies.
Creating Passport Strategies
When researching on how to create a Passport Strategy, it’s surprising how little learning material is out there. The documentation itself for various Strategies are somewhat lacking. Instead of trying to guess how they work, let’s take out the magic and write something that is functional and understandable.
Create a new directory in src
called auth
and inside it, create a new file called passport.js
to store our strategies.
Inside passport.js
The JWT Strategy is what allows us to secure routes with cookie-based tokens, let’s go ahead and create that.
To start, we’ve created a cookie extractor function, it takes the request body and grabs the cookie based on the key that you’ve given it. The ['jwt']
is the key name which can be easily assigned — but we’ll discuss this shortly in the router section.
Back again on the creation of a Passport Strategy, we know that this one is called ‘jwt’. The first argument of the constructor takes an object literal, which contains the function of where we want to get our JWT, in this case it’s in the Cookie so we used the cookieExtractor
function. Next is the secretOrKey
attribute, that’s where you’ll pass in the JWT_SECRET
var. The passport-jwt
Strategy has a lot more tools available, if you’re interested you may want to check out its documentation.
Alright so we have a function and it has 2 parameters, the first one is the payload of the verified JWT. If the verification fails there’s a good chance you’ll get a 401 response. Side note: the jsonwebtoken library already has a way to check the expiration using the exp
attribute if it was passed during the token’s creation. Again it helps to check the documentation for our tools. Anyway, we destructure the expiration value from the payload object and that’s what we’ll check if the JWT is still good or invalid. Using simple logic, if it’s expired return an error, else just return the verified payload. And that’s it!
It’s simplistic, yes, but it’s to help us understand how this Strategy works. You can add all manner of validation checks and further security layers, the middleware is flexible after all! In fact, it’s recommended that you create and customize a combination of Strategies according to your security need or business process.
Building The Login Middleware
I initially intended to use passport-local
to accompany the JWT Strategy, but I found that to be far too inconsistent for a simpler implementation. Not to say it’s a bad Strategy, it’s just more important to understand the basic method of logging in using a username and password field with existing tools already present in Express.
[OPTIONAL] Important Considerations
Before we write the Login middleware, let’s keep in mind that if this were the real implementation, you’d need some form of input validation. Something that actively checks and sanitizes the request body input to avoid SQL and NoSQL Injection attacks. I highly recommend checking out express-validator
as it’s the easiest to just ‘drop-in’ to your routes and also to understand.
Using a well-prepared and tested Stored Procedure is a must when building your login endpoint. While it won’t be completely invulnerable to all forms of SQL Injection, it will mitigate the risk of a successful attack.
Next, if you’re also responsible for building the Client, be sure to sanitize the input client-side as well. There’s plenty of material out there to help you get started with this.
It’s important to grasp the fact that Security is composed of layers, multiple lines of defense are extremely important to help deter potential attackers.
Login Middleware
Inside the auth
directory, create a new file and call it login.js
. This is where we’ll create the new middleware for our router later. All this will do is take the username and password fields from the request body and match it with the set env vars.
First we destructure username
and password
from req.body
then create a user
object for use later. The try-catch setup is just there to check if your server has errors.
If the username matches with that of the set env var, then we proceed to comparing the password — also that of the set env var. Once everything succeeds we store the user
object inside of res.locals
which is the recommended and documented way of passing values between Express middlewares. Finally we call next()
middleware function which simply continues the process of authentication within the login route. Typically the password here would be hashed and first need to be compared with something like Argon2 or Bcrypt. If you feel like incorporating that in your app, feel free to do so.
Of course if the checks fail, we respond with a 400 status and tell the Client that either the username or the password are incorrect. It’s highly important to never be specific about which one is wrong. This serves to deter brute force attempts by way of random password or username guessing.
Let’s go ahead and create that all important router!
Building The User Router
This section contains our login, logout, and protected routes. In the src
directory, create a new folder and call it routers
. Then inside of routers, create a new script user.router.js
.
Let’s begin with the most important route, /login
. The job of the login route is to take the user object from the middleware and take its properties to embed it to the JWT, which will then be sent as a Cookie to the Client. Sound simple? Alright, let’s do it.
We begin by importing the essentials, and of course our Login Middleware.
The first thing we do is create an empty variable called user
which is where we’ll store the user object from res.locals.user
, which was passed earlier from the Login Middleware.
Then we create a payload object which contains our username
property and the expiration
in milliseconds. You’ll want to assign something higher than a minute, maybe 10 minutes in milliseconds will do. We combine that with the value of Date.now()
, which simply grabs the numerical value of the current date. There’s another important topic surrounding this one, which I will get to at the end of the article.
After that we sign the token with the secret. Once that’s done, we send the response object with the Cookie containing our token
and various properties. If you’ve noticed this is where we name our Cookie, or rather, we assigned the Cookie its key. The string ‘jwt’ can really be whatever you want to call your Cookie, it’s all up to your team or your preferred standards. I recommend using a name that’s less obvious to deter inexperienced attackers.
httpOnly
on simple terms prevents the Client from accessing the Cookie. The secure
flag if set to true
will only set the cookie on secure or encrypted networks, mitigating man-in-the-middle attacks commonly occurring on public wireless access points.
And finally we send a 200 status and a message to acknowledge that the login process was successful.
Logout & Protected
All the logout route does is clear your existing JWT Cookie, using Cookie Parser’s res.clearCookie()
method.
Then we have the protected route which has the JWT Strategy in place, handy for checking if the Client succeeds or fails in authentication.
That’s it! Now we’re ready to test out those routes for real.
Testing Routes
In this section we will begin testing routes using Postman.
If you have certain preferences in your run script, you should configure them now. There will be errors and minor corrections to be made so be prepared to have something like Nodemon ready.
Login
Empty Fields:
Attempting to submit an empty request body results with an expected Bad Request response from the server.
Incorrect Credentials:
Submitting incorrect user credentials gives us the same 400 response, just as expected.
Successful Login:
Once we submit a valid username and password, we have a 200 response and a Cookie.
The Cookie contains our JSON Web Token which will be used for the protected route.
Protected Route
Valid Token:
Performing a GET request with a valid token to the protected route results with an OK response from the server.
Invalid or No Token:
Attempting to perform a GET request to the protected route without a token, or an invalid and/or expired one will result with an Unauthorized response from the server.
Logout
Invalid or No Token:
An invalid or empty token results in a 401 response.
Successful Logout:
As expected, the logout clears our Cookie by key name and attempting to logout again will result in a 401.
That Other Topic I Was Talking About
Let’s get the giant elephant in the room out of the way. True Stateless Authentication requires you to pair JWT Cookies with something called a Refresh Token. This is another topic entirely which will require, at worst, a code overhaul, and a live database. JWTs normally have a short lifespan, something like 5 to 10 minutes, but Refresh Tokens normally live longer, sometimes a day or a full week. Please consider studying about this after finishing this article.
Conclusion
There you have it! The very basic and bare minimum fundamentals to create your own HttpOnly Cookie JWT authentication system with Passport. Of course there’s much that I merely glanced over with the intent to deliver the idea of what would become your pattern of creating a solid foundation for future projects.
As always feel free to correct any mistakes you find, or tell me any suggestions you have to improve the code. And speaking of code, the source for the project is available right here. Do whatever you want with it, the most important thing for me is that I hope I helped you learn something significant.
Support Me
Hey there, I see you’re still reading. Thank you so much for taking the time to read through this article. I sincerely hope it was a practical experience, by that I mean the coding portions weren’t boring or lacking in explanation. With that being said, if you enjoyed this tutorial and want to support me, why not buy me a cup of coffee?
Or maybe you want to hire or collaborate with a full stack web developer? You can do that right on my website.