Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Token not being set in cookies

Tags:

So we have an random issue where every now and then the JWT token which we store in cookies for authentication is not set in the browser. Now 99% of the time a user goes to the login web page enter their details it sends a request to the server which sends back their user details and a JWT token set into the cookies. Now every now and then the cookie does not seem to be set. Its random has happened on nearly all browsers now but with no reason as to why. It happens in both our local, staging and production environments. (I have removed some code for privacy reasons)

The backend auth service is built using Node and ExpressJS it set the token with the following code:

module.exports.signIn = async function(req, res, next) {
  try {
    const { email, password } = req.body;
    if (!email || !password)
      throwBadRequest("Please enter a valid email and password");

    const data = await Users.get(`?email=${email.toLowerCase().trim()}`);

    const { name, val, options } = await Token.generateCookieParams(data);
    res.cookie(name, val, options);

    return res.json(toDTO(data));
  } catch (err) {
    next(err)
  }
};

We are using the middleware cookie parser if that helps. Here is the code that set the token:

async function generateFor(user, expireTime, special = null) {
    const payload = { id: user._id, type: user.type, account: user.account };
    if (user.entity) {
      payload.entity = user.entity;
    }
    if (special) {
      payload.special = special;
    }
    const token = await jwt.sign(payload, config.secret, {
      expiresIn: expireTime
    });
    return token;
  }

async function generateCookieParams(user) {
    const expireTime = 60 * 60 * 12; // 12 hour
    const token = await Token.generateFor(user, expireTime);
    return { name: config.tokenKey, val: token, options: { httpOnly: true } };
  }

We are using the middleware cors for managing cors in the express app and have the option credentials set to true.

Then in the front end, we are using superagent to make all the request from the react app, we also used Axios as well but have the same issues. The base code for the networking looks like this in the front end:

import superagent from "superagent";

const superagentManager = {};

/**
 * POST
 * @param {string} path => the path for the post request
 * @param {object} data => the object you are posting in json format
 */
superagentManager.post = async (path, data) => {
  return await superagent
    .post(path)
    .withCredentials()
    .type("application/json")
    .send(data);
};

/**
 * GET
 * @param {string} path => the path for the get request
 */
superagentManager.get = async path => {
  return await superagent
    .get(path)
    .withCredentials()
    .type("application/json");
};

/**
 * PATCH
 * @param {string} path => the path for the patch request
 * @param {object} data => the object you are posting in json format
 */
superagentManager.patch = async (path, data) => {
  return await superagent
    .patch(path)
    .withCredentials()
    .type("application/json")
    .send(data);
};

/**
 * DELETE
 * @param {string} path => the path for the delete request
 */
superagentManager.delete = async path => {
  return await superagent
    .delete(path)
    .withCredentials()
    .type("application/json");
};

export default superagentManager;

If anyone could help me it would be much appreciated. The system works but every now and then let's say 1 out of every 50 logins it doesn't set the token in the browser. So the user object is returned from the login request but further request that happens straight afterwards throw an error as there is no token in the cookie. With the user base growing the bug is becoming more and more noticeable.

like image 512
AlexanderKaran Avatar asked Mar 29 '20 09:03

AlexanderKaran


2 Answers

This looks like a cookie issue!

So there are two ways of keeping states in the browser with cookies. Session data is stored on the server and typically use some key to retrieve values related to user states. Cookies are stored client side and are sent in requests to determine user states as well. ExpressJS supports the ability to use both of these. For JWT you want to use the cookie approach of course!

Let's start by taking a look at your cookie options:

// return { name: config.tokenKey, val: token, options: { httpOnly: true } };

const cookieOptions = {
    httpOnly: true
}

So far this looks good. You're following best practices about storing tokens as http only, but to get the cookie stored correctly you may need to add some more to the cookie options.

Here is a link to cookie options in the express docs. Check out the description for "expires":

Expiry date of the cookie in GMT. If not specified or set to 0, creates a session cookie.

Essentially what is happening is that you are not specifying an expiry so your cookie is set as a session cookie. What that means is that the cookie is being destroyed whenever a user closes the browser.

Bonus:

  • If your site uses HTTPS make sure to set the cookie as secure: true

  • You may want to check out the sameSite attribute as well if it applies to your team.

like image 138
EJ Mason Avatar answered Nov 15 '22 06:11

EJ Mason


I don't see anything wrong with your code, but some things to look out for that have bit me in the past;

  • ensure your client doesn't start the next call when the login call is still happening. Also ensure correct errorhandling to show the client the login failed.
  • if you have a proxy (like nginx, haproxy) ensure the vary header is configured correctly
  • ensure there is no caching happening on the server AND in the browser by configuring the no-cache and max-age headers
like image 20
japrescott Avatar answered Nov 15 '22 04:11

japrescott