loading...
Cover image for Remaining Stateless - JWT + Cookies in Node JS(REST)

Remaining Stateless - JWT + Cookies in Node JS(REST)

mr_cea profile image Ogbonna Basil Updated on ・4 min read

JWT is stateless. Using cookies as a container to store JWT is easy and scalable.

Why Store in cookies

The need to store JWT in cookies is seen in considering the difference between cross-site scripting(XSS) and cross-site request forgery (CSRF).
A little digging will suffice but in summary:-

  • XSS happens when an attacker inserts malicious code into the client-side browser basically by inputting a script in forms and checking if the browser will run these scripts.XSS exploits the trust a user has for a particular site.
  • CSRF, on the other hand, is an attack that forces an end user to execute unwanted actions on a web application in which they're currently authenticated(i.e they have sessions). CSRF attacks specifically target state-changing requests, not theft of data since the attacker has no way to see the response to the forged request.CSRF exploits the trust that a site has in a user's browser
  • XSS attacks occur in varied ways.
  • CSRF attack can only occur when an authenticated user session is hijacked, the attacker carrying out activities on behalf of the user.
  • Stateless JWT stored in the browser local storage is more susceptible to XSS attacks and less to CSRF attacks.
  • Cookies are less susceptible to XSS attacks provided it's HTTPOnly and the secure flag is set to true. Using only HTTPOnly might not prevent an attack as an attacker might use XST (cross-site tracing) to retrieve the cookie via XSS + HTTP Trace.
  • Cookies are more susceptible to CRSF attacks. However, it is well known how to mitigate CSRF attacks than the more varied XSS attacks.

Storing JWT in cookies in Node JS

Step 1 - Create a JWT on register or Login

  • install JWT and dotenv
 npm install jsonwebtoken
 npm install dotenv
  • Generate Token and save in token in Cookie
const jwt = require('jsonwebtoken');
//import jwt from 'jsonwebtoken'; 

const generateToken = (res, id, firstname) => {
  const expiration = process.env.DB_ENV === 'testing' ? 100 : 604800000;
  const token = jwt.sign({ id, firstname }, process.env.JWT_SECRET, {
    expiresIn: process.env.DB_ENV === 'testing' ? '1d' : '7d',
  });
  return res.cookie('token', token, {
    expires: new Date(Date.now() + expiration),
    secure: false, // set to true if your using https
    httpOnly: true,
  });
};
module.exports = generateToken

// generateToken.js file

Step 2 - Use Cookie-Parser

  • install cookie-parser and cors
npm install cookie-parser
npm install cors

  • Ensure server uses cookie-parser


const express = require('express');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const loginRouter = require('./path/to/loginRouter');
require('dotenv').config();
const server = express();
server.use(express.json());
server.use(cors());
server.use(cookieParser());
// server should use login router
server.use('/', loginRouter)

const PORT = process.env.PORT || 5000;

server.listen(PORT, () => console.log(`Server started at port ${PORT}`));

// server.js file

Step 3- On Login/Register call the generate token

const generateToken = require('./path/to/generateToken');

const login = async (req, res) => {
  const { email, password } = req.body;

  try {
// get user details based on the login parameters

    const result = await emailExists(email);

    const { id, firstname} = result;

    await generateToken(res, id, firstname);
// carry out other actions after generating token like sending a response);
  } catch (err) {
    return res.status(500).json(err.toString());
  }
};
// loginController.js file

Verifying JWT stored in cookies

To create protected routes, the user needs to be authenticated. Authenticating a user requires decrypting the token gotten in the cookies. To do that you need an authentication middleware that gets the token from the cookies, verify the token is still valid and passes the action to the controller.

const dotenv = require('dotenv');
const jwt = require('jsonwebtoken');

dotenv.config();
const verifyToken = async (req, res, next) => {
  const token = req.cookies.token || '';
  try {
    if (!token) {
      return res.status(401).json('You need to Login')
    }
    const decrypt = await jwt.verify(token, process.env.JWT_SECRET);
    req.user = {
      id: decrypt.id,
      firstname: decrypt.firstname,
    };
    next();
  } catch (err) {
    return res.status(500).json(err.toString());
  }
};

module.exports = verifyToken;

// verifyToken.js file

This is then used as a middleware to verify the user. If the user is verified, the controller is called. Otherwise, an authorization error is sent to the user

const express = require('express');
const user = require('./userController');
const verifyToken = require('../path/to/verifyToken');

const router = express.Router();

router.post(
  '/auth/verify',
  verifyToken,
  user.verifyMail,
);
)
module.exports = router;

// LoginRouter.js file

Using backend generated Cookies for front-end authentication

When cookies are created at the backend with options of HTTPOnly set to true, the cookies are not visible to the frontend. When a request is made to the server, the cookies comes embedded in the headers alongside the request.

  • Set up cors on the backend

when using cookies on the backend, the origin of the request needs to be specifically stated. To do this ensure that the server has cors with the credentials set to true and origin containing the needed origins. One way to easily change up origin is by saving the frontend as an environment variable and setting it up using process.env.

// server.js (backend)
server.use(
  cors({
    origin: [
      `${process.env.FRONT_URL}`,
      'http://localhost:3000',
      'https://mypage.com',
    ],
    credentials: true
  })
);

  • Ensure headers are sent with request to the backend

On the frontend, when making request from the backend with axios or fetch ensure that headers are sent alongside the request. To do that with axios calls, set the with credentials option to true for both get and post calls. If you are using fetch calls set the credentials option to include likewise.

// axios post
axios
    .post(`${url}`, {contentToBeSent}, {
      withCredentials: true
    })

// fetch get
fetch('https://example.com', {
  credentials: 'include'
});

I'll leave it to you to try JWT + Cookies for better security.

Feel free to reach out to me on basil_cea or on the threads below.

Posted on by:

mr_cea profile

Ogbonna Basil

@mr_cea

Full Stack / AI Engineer with a history of working in the computer software industry following global best practices in building scalable technology solutions. A graduate of Maths/Statistics.

Discussion

markdown guide
 

Great tutorial. Devs always say you should use cookies but never tell you how to do it. By the way, I think I'm doing something wrong because the cookie resets when I reload the page what could be causing this?

 

The cookie used by the backend is automatically embedded in the header when you make request. Maybe what is happening is that you have authenticated a user in the frontend and then when you reload the page it losses the user information so the user needs to login again. If that is the issue, you need means to identify the authenticated user as the real cookie used in the backend is always embedded in the header and you have no access to it. To solve that problem what i do is to create a fake cookie that identifies the user with non-sensitive information about the user that comes from the response on login or signup and save the fake cookie in the frontend cookie storage. This is because even if an attacker somehow gets the cookie from the cookie storage he cant access information from the backend as that is not the real cookie used by the backend. At the same time the fake cookie helps to keep the user logged in when you reload the page. I hope this is the issue you have. If it is try this approach it worked for me.

 

Yes you are right!

it's working now :) thank you

 

The best way to store JWT is the memory, while having an HTTP ONLY cookie containing the refresh token.

 

Belhassen, when you say store the JWT in memory, are you talking about localstorage, or are you talking about something like Redux or React Context state?

 

In the application state (eg: react state).

And when user close the browser? Of course, some applications (like banking) prefer to terminate user login, but on most applications, we need to keep user login after closing the browser and open it next time.

the http only cookie for the refresh token stays so you can always get the acces token accessing the refresh endpoint that will give you a new access and a new refresh token so no need to login again.

 

i heard that many time. but how's the implementation?

 

Write an example just like Ogbonna Basil ;)

 

Thanks Belhassen. I will take a look at this method too.

 

can't get the cookie when i try to get it req.cookies.token it returns undefined

 

always get udefined in req.cookies.toke

require("dotenv").config();
const express = require("express");
const app = express();
const cors = require("cors")
const cookie = require("cookie-parser");
const jwt = require("jsonwebtoken");
app.use(cors());
app.use(cookie());

const createToken = function(res,user){
try{
const token = jwt.sign({user},process.env.ACESS_TOKEN);
console.log(token);
return res.cookie('token', token, {
secure: false, // set to true if your using https
httpOnly: true,
});
}catch(e){
return console
.log(e)
}
}

const verifyToken = async (req, res, next) => {
const token = req.cookies.token || '';
console.log(token);
try {
if (!token) {
return res.status(401).json('You need to Login')
}
const decrypt = await jwt.verify(token, process.env.JWT_SECRET,(err,data)=>{
if(err)res.send("Token is undefined")
});
req.user = {
id: decrypt.id,
firstname: decrypt.firstname,
};
next();
} catch (err) {
return res.status(500).json(err.toString());
}
};

app.get("/",verifyToken,async(req,res)=>{
res.send("Token")
});

app.post("/post",(req,res)=>{
user = {
Name : "vibhor",
Age : "27"
}
createToken(res,user);

});

app.listen(6000,()=>{
console.log("Server has started")
})

 

You are probably getting undefined because you signed the jwt with the secret process.env.Acess_Token but you are trying to verify it with process.env.JWT_Secret. Please correct that and check if it works.

 

gald someone finally put pen to paper show how to use cookies instead of localstorage!

 

Glad you found the article useful

 

You really did so great, thanks a lot!!

 

Really good post

Deserves more attention.

 

Thanks DonnieTD, glad you found this article helpful

 

This is really how I am working, the JWT with a very short expiration time stored in memory, and the refresh token as you explain only to renew the JWT. Good Post, greetings.

 

Thanks Leonardo. I have looked at storing jwt in memory in the frontend and saving refresh token in http only cookie and i think it is the most optimal approach currently