DEV Community

Cover image for Using Cookies with JWT in Node.js
Francisco Mendes
Francisco Mendes

Posted on

Using Cookies with JWT in Node.js

Although JWT is a very popular authentication method and is loved by many. Most people end up storing it at localstorage. I am not going to create an argument here about what is the best way to store the jwt in the frontend, that is not my intention.

If you have already read this article I created on how to create a simple authentication and authorization system with JWT, you must have noticed that I send the jwt in response when an http request is made from the login route. That is, the idea is to keep it in localstorage.

However, there are other ways to send the jwt to the frontend and today I will teach you how to store the jwt in a cookie.

Why use cookies?

Sometimes I'm a little lazy and because of that I don't feel like constantly sending the jwt in the headers whenever I make a request to the Api. This is where cookies come in, you can send them whenever you make an http request without worry.

Another reason is if you use localstorage, on the frontend you must ensure that the jwt is removed from localstorage when the user logs out. While using cookies, you just need a route in the api to make an http request to remove the cookie that you have on the frontend.

There are several reasons for preferring the use of cookies, here I gave small superficial examples that can occur in the elaboration of a project.

Now that we have a general idea, let's code!

Let's code

First we will install the following dependencies:

npm install express jsonwebtoken cookie-parser
Enter fullscreen mode Exit fullscreen mode

Now just create a simple Api:

const express = require("express");

const app = express();

app.get("/", (req, res) => {
  return res.json({ message: "Hello World 🇵🇹 🤘" });
});

const start = (port) => {
  try {
    app.listen(port, () => {
      console.log(`Api up and running at: http://localhost:${port}`);
    });
  } catch (error) {
    console.error(error);
    process.exit();
  }
};
start(3333);
Enter fullscreen mode Exit fullscreen mode

As you may have guessed, we will need something to be able to work with cookies in our Api, this is where the cookie-parser comes in.

First we will import it and we will register it in our middlewares.

const express = require("express");
const cookieParser = require("cookie-parser");

const app = express();

app.use(cookieParser());

//Hidden for simplicity
Enter fullscreen mode Exit fullscreen mode

Now we are ready to start creating some routes in our Api.

The first route that we are going to create is the login route. First we will create our jwt and then we will store it in a cookie called "access_token". The cookie will have some options, such as httpOnly (to be used during the development of the application) and secure (to be used during the production environment, with https).

Then we will send a reply saying that we have successfully logged in.

app.get("/login", (req, res) => {
  const token = jwt.sign({ id: 7, role: "captain" }, "YOUR_SECRET_KEY");
  return res
    .cookie("access_token", token, {
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
    })
    .status(200)
    .json({ message: "Logged in successfully 😊 👌" });
});
Enter fullscreen mode Exit fullscreen mode

Now with the login done, let's check if we received the cookie with the jwt in our client, in this case I used Insomnia.

login

Now with the authentication done, let's do the authorization. For that we have to create a middleware to check if we have the cookie.

const authorization = (req, res, next) => {
  // Logic goes here
};
Enter fullscreen mode Exit fullscreen mode

Now we have to check if we have our cookie called "access_token", if we don't, then we will prohibit access to the controller.

const authorization = (req, res, next) => {
  const token = req.cookies.access_token;
  if (!token) {
    return res.sendStatus(403);
  }
  // Even more logic goes here
};
Enter fullscreen mode Exit fullscreen mode

If we have the cookie, we will then verify the token to obtain the data. However, if an error occurs, we will prohibit access to the controller.

const authorization = (req, res, next) => {
  const token = req.cookies.access_token;
  if (!token) {
    return res.sendStatus(403);
  }
  try {
    const data = jwt.verify(token, "YOUR_SECRET_KEY");
    // Almost done
  } catch {
    return res.sendStatus(403);
  }
};
Enter fullscreen mode Exit fullscreen mode

Now it is time to declare new properties in the request object to make it easier for us to access the token's data.

To do this we will create the req.userId and assign the value of the id that is in the token. And we will also create the req.userRole and assign the value of the role present in the token. And then just give access to the controller.

const authorization = (req, res, next) => {
  const token = req.cookies.access_token;
  if (!token) {
    return res.sendStatus(403);
  }
  try {
    const data = jwt.verify(token, "YOUR_SECRET_KEY");
    req.userId = data.id;
    req.userRole = data.role;
    return next();
  } catch {
    return res.sendStatus(403);
  }
};
Enter fullscreen mode Exit fullscreen mode

Now we are going to create a new route, this time we are going to create the route to log out. Basically we are going to remove the value from our cookie. That is, we will remove the jwt.

However, we want to add the authorization middleware to our new route. This is because we want to log out if the user has the cookie. If the user has the cookie, we will remove its value and send a message saying that the user has successfully logged out.

app.get("/logout", authorization, (req, res) => {
  return res
    .clearCookie("access_token")
    .status(200)
    .json({ message: "Successfully logged out 😏 🍀" });
});
Enter fullscreen mode Exit fullscreen mode

So now let's test whether we can log out. What is intended is to verify that when logging out the first time, we will have a message saying that it was successful. But when we test again without the cookie, we must have an error saying that it is prohibited.

logout

Now we just need to create one last route so that we can get the data from jwt. This route can only be accessed if we have access to the jwt that is inside the cookie. If we don't, we will get an error. And now we will be able to make use of the new properties that we added to the request.

app.get("/protected", authorization, (req, res) => {
  return res.json({ user: { id: req.userId, role: req.userRole } });
});
Enter fullscreen mode Exit fullscreen mode

If we test it on our favorite client. We will test the entire workflow first. Following the following points:

  • Log in to get the cookie;
  • Visit the protected route to view the jwt data;
  • Log out to clear the cookie;
  • Visit the protected route again but this time we expect an error.

I leave here a gif to show how the final result should be expected:

final

The final code must be the following:

const express = require("express");
const cookieParser = require("cookie-parser");
const jwt = require("jsonwebtoken");

const app = express();

app.use(cookieParser());

const authorization = (req, res, next) => {
  const token = req.cookies.access_token;
  if (!token) {
    return res.sendStatus(403);
  }
  try {
    const data = jwt.verify(token, "YOUR_SECRET_KEY");
    req.userId = data.id;
    req.userRole = data.role;
    return next();
  } catch {
    return res.sendStatus(403);
  }
};

app.get("/", (req, res) => {
  return res.json({ message: "Hello World 🇵🇹 🤘" });
});

app.get("/login", (req, res) => {
  const token = jwt.sign({ id: 7, role: "captain" }, "YOUR_SECRET_KEY");
  return res
    .cookie("access_token", token, {
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
    })
    .status(200)
    .json({ message: "Logged in successfully 😊 👌" });
});

app.get("/protected", authorization, (req, res) => {
  return res.json({ user: { id: req.userId, role: req.userRole } });
});

app.get("/logout", authorization, (req, res) => {
  return res
    .clearCookie("access_token")
    .status(200)
    .json({ message: "Successfully logged out 😏 🍀" });
});

const start = (port) => {
  try {
    app.listen(port, () => {
      console.log(`Api up and running at: http://localhost:${port}`);
    });
  } catch (error) {
    console.error(error);
    process.exit();
  }
};
start(3333);
Enter fullscreen mode Exit fullscreen mode

Final notes

Obviously, this example was simple and I would not fail to recommend reading much more on the subject. But I hope I helped to resolve any doubts you had.

What about you?

Have you used or read about this authentication strategy?

Top comments (23)

Collapse
 
pasgba profile image
Paschal A. Ogba

Very helpful blog.
Nice job again . Thank you.

Collapse
 
hasnaindev profile image
Muhammad Hasnain

Why would I use cookie based JWT authentication?

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

Thanks for the feedback! 😊 I usually keep the JWT on localstorage. But I know a lot of people who prefer to keep it in cookies. 😉

Collapse
 
ki9 profile image
Keith Irwin

According to my research, storing auth tokens in localStorage and sessionStorage is insecure because the token can be retrieved from the browser store in an XSS attack. Cookies with the httpOnly flag set are not accessible to clientside JS and therefore aren't subject to XSS attacks.

After learning this, I tried implementing an Authorization: Bearer XXXXXXXXX request header, but keeping the token stored safely in the cookie. Then I realized I won't be able to copy the token from the cookie to the request header if I can't access it with clientside JS (httpOnly, remember?)

I've therefore come to conclude that saving the token in an httpOnly cookie and sending it to the server as a request cookie is the only secure way of using JWT.

Thread Thread
 
ekbal41 profile image
Asif Ekbal

Wow, I didn't know that!

Collapse
 
hasnaindev profile image
Muhammad Hasnain

Sure, I'm just curious to know what is the benefit of using JWT inside cookies? Thanks.

Thread Thread
 
franciscomendes10866 profile image
Francisco Mendes

The biggest difference when saving the JWT in a cookie would be the fact that when making an http request, the cookie would be sent with the request. But if you store the JWT in localstorage, you would have to send it explicitly with each http request. 🧐

Thread Thread
 
hasnaindev profile image
Muhammad Hasnain

Ahan, I understand. I wasn't sure if this was for a server-side website. Meaning, we don't have to use packages like Passport.js with this approach.

Thread Thread
 
franciscomendes10866 profile image
Francisco Mendes

Exactly. If you do it this way you end up with less boilerplate in your Api. The use of Passport.js is not incorrect, I just like to show that we can make simple and functional implementations. 🥸

Thread Thread
 
aziz477 profile image
aziz477

please can you tell me what is the exact role of passport strategy next to the normal jwt?

Thread Thread
 
franciscomendes10866 profile image
Francisco Mendes

Passport is a middleware with a good level of abstraction, for example, with jwt you wouldn't have to write that much code. In addition to being faster to implement, it is also the simplest. However, business rules can change from project to project so I advise people to know how to do a simple setup.

Collapse
 
inderharrysingh profile image
Inderjot Singh

then you don't need to read the localstorage each time and manually send the token alongside every request from the client .

Collapse
 
danielkjcoles profile image
Daniel KJ Coles

Awesome, thanks for this! Saved me a massive headache

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

I'm glad you liked it! 💪

Collapse
 
rook2pawn profile image
david wee • Edited

I am very confident this is open to CSRF attack. The attacker simply needs to host another site B and then a user with the cookie will end up sending the encrypted token to the main site A where they can /login and then /protected. Recommend using a combination of SameSite (stops CSRF for browsers that respect sameSite) and Synchronizer Token Pattern (stops Cross site same origin attacks). Both should be used since SameSite is still vulnerable to cross site same origin. Also there is a major flaw in using GET since SameSite wont be applied. Also you mention because its less work to use Cookies vs Localstorage - and that you are using HttpOnly flag to prevent XSS - good - but it should be made clear Localstorage is never an option for JWT, not because its just more work but XSS attackable.

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

Thanks for your feedback! 😊 Yes, with the setup I share in this article it is possible to suffer a CSRF attack 🧐. However, the purpose of this article was not to provide a completely secure solution, but rather to create a simple authentication and authorization strategy that can be implemented in small personal projects (done for fun).

Authentication and authorization is not an area that I like to discuss, because there are a thousand strategies that can be implemented. But regarding the discussion of Cookies vs Local Storage, in my opinion, let the devil come and choose, each one has its disadvantages and advantages and it only depends on the programmer to choose which risks to take. These days, to be honest, I'm indifferent.

Hope you have a great day! 🙏

Collapse
 
franciscomendes10866 profile image
Francisco Mendes • Edited

Yes it works. So you mean it's working with other http methods? Do you have the authorization middleware on the route?

 
franciscomendes10866 profile image
Francisco Mendes • Edited

Do you have cors installed in your Api project?

const cors = require("cors");

app.use(cors({ credentials: true }));
Enter fullscreen mode Exit fullscreen mode
Collapse
 
jamesdev21 profile image
Semaj21 • Edited

Hi. How to setup cookie parser options in one place? like this one?

const cookieOptions = {
httpOnly: true,
secure: true,
sameSite: true
}

app.use(cookieParser(cookieOptions))

so you only need to write:

.cookie('access_token', token)

it's annoying you have to write the options every time you use it. is this right? thanks

 
franciscomendes10866 profile image
Francisco Mendes

Weird, are you sure you're sending the cookie?

Collapse
 
hamidshaikh1499 profile image
HamidShaikh1499 • Edited

All My Doubt And Question Cleared Through This Blog And Thank You @FranciscoMendes .

Collapse
 
badreddineab profile image
Badreddine-Ab

I had a problem following up, when I used the authorization middelware in the Logout route, I got the forbidden message in Insomnia.
Do you have know the reason behind it ?

Collapse
 
kvn137272 profile image
kvn137272

that was great