How to put JWT's in server side cookies using the Strapi user-permissions plugin

Christopher Talke Buscaino

This post won't explain what Strapi is, how it works, or even when you should use it. If you are after that type of information, just see any of the below links...

This post will instead explain how to modify the included plugin strapi-plugin-users-permissions in order to force it to use server side cookies and give you the ability to set http only or secure options. This will ensure that your frontend application does not have to store sensitive data such as JWT's in client side local, session or cookie storage for future authenticated requests.

If you want to understand why avoiding use of client side storage is important and a mitigates potential security risk, check out these resources for reference:

🔧 Setup

Before you start you'll need to import some files into your project from the Strapi master branch that relate to the strapi-plugin-users-permissions plugin, see this link for the specific package: Strapi Plugin User Permissions source.

Once you've completed this your folder directory should look a little something like this:

Project directory after importing files

If you want to understand why we are doing this, please reference Strapi's documentation: Customization. Essentially we are setting ourselves up to override the methods used by the strapi-plugin-users-permissions plugin.

Before we proceed my advice is to review the two files we imported, and understand how the base code works. You don't have to do this, but it will allow you to revisit this in the future for debugging / re-implementation purposes.

🥇 Modify 'Auth' Handler

First we need to ensure that when you hit the normal auth/local endpoint, that the JSON Web Token (JWT) is issued via a cookie rather then be included in the body of the http response.

To do this you will need to modify the Auth.js file we imported earlier, first you'll need to find this code within the callback() method, and change it from this:

ctx.send({
  jwt: strapi.plugins['users-permissions'].services.jwt.issue({
    id: user.id,
  }),
  user: sanitizeEntity(user.toJSON ? user.toJSON() : user, {
    model: strapi.query('user', 'users-permissions').model,
  }),
});

To this:

const token = strapi.plugins["users-permissions"].services.jwt.issue({
  id: user.id,
});

ctx.cookies.set("token", token, {
  httpOnly: true,
  secure: process.env.NODE_ENV === "production" ? true : false,
  maxAge: 1000 * 60 * 60 * 24 * 14, // 14 Day Age
  domain: process.env.NODE_ENV === "development" ? "localhost" : process.env.PRODUCTION_URL,
});

ctx.send({
  status: 'Authenticated',
  user: sanitizeEntity(user.toJSON ? user.toJSON() : user, {
    model: strapi.query('user', 'users-permissions').model,
  }),
});

The above change will do the following:

  • Generate the JSON Web Token and assign it to the variable token
  • Use a Koa.js method to create a cookie, using the token variable as the value, and allow us to pass our custom cookie options
  • Allows you to use environment variables such as NODE_ENV and PRODUCTION_URL to dynamically set cookie values depending on the environment.
  • Modify the http response to replace the jwt key with a status key to communicate a successful authentication

Great - with that change you can test your auth/local route with a tool like Postman, if everything went right you should now see a token cookie with your JSON Web Token as the value!

Now you just need to be able to make authenticated requests to Strapi, if you try to make a request to a protected route right now, even with a cookie, it will not work. In order to fix this we need to modify the other file we imported earlier to include the cookie in the authentication business logic.

🥈 Modify 'Permissions' Config Policy

Open the permissions.js file we imported earlier, and add the following code the top of the function in this file.

Your code will go from looking like this:

let role;
if (ctx.request && ctx.request.header && ctx.request.header.authorization) {
// Rest of the code
}

To this:

let role;
if (ctx.request && ctx.request.header && !ctx.request.header.authorization) {
  const token = ctx.cookies.get("token");
  if (token) {
    ctx.request.header.authorization = "Bearer " + token;
  }
}
if (ctx.request && ctx.request.header && ctx.request.header.authorization) {
  // Rest of the code
}

The above change will do the following:

  • Checks if the ctx.request and ctx.request.header are not undefined values, and then checks that ctx.request.header.authorization is undefined
  • If all of the above comes back as true, we then pull the token from the cookie using the ctx.cookies.get() method and assign it to the token variable.
  • If the token is not undefined, we modify ctx.request.header.authorization to act as a Bearer Token header to let the rest of the business logic know that we are an authenticated user.

Now you'll be able to authenticate and continue to make requests to protected api routes using your new secure httpOnly cookie! Now the only problem you have is that you can't logout, or kill the cookie... yet.

🥉 Custom 'Logout' Router and Handler

Before we start on this section, I just want to say that there may be better ways to do this, however, this is how I solved this problem.

First go to your api folder in your project, and create a folder called custom and inside of this directory create the following folders config and controllers.

From there we will need to create the routes so that Strapi accepts traffic to our custom route, you can do this by creating a routes.json file in the config folder and insert the following code:

{
  "routes": [
    {
      "method": "POST",
      "path": "/logout",
      "handler": "Custom.logout"
    }
  ]
}

Okay perfect - the above .json file will let Strapi know the following:

  • /logout is a valid path for a POST http request
  • If a request comes through, look for a logout() function in the controllers/Custom.js file.

To make sure that something actually happens when we hit that route, lets create that file controllers/Custom.js, and insert the following code:

"use strict";

module.exports = {
  async logout(ctx) {
    ctx.cookies.set("token", null);
    ctx.send({
      authorized: true,
      message: "Successfully destroyed session",
    });
  },
};

With that you'll now have a completely functional authentication process in Strapi, and you won't need to manually store your JWT in client side storage when you are making requests from your frontend!

💜 Credit

While doing research on how to achieve this, I came across this Github Issue. Within this issue, you will find a reply from a user called sanojsilva where I pulled the idea / logic from, however, there was no clear documentation on how this is achieved. Thanks for the pointer sanojsilva and I hope this helps someone who is trying to figure this out.

🤔 Frequently Asked Questions

Since I had originally published this blog post it has picked up some traction, either due to the link I left in a Github Issue, or due to someone I look up (with a big following, aka Wes Boss) taking notice of it and sharing it.

Either way, a few people have asked me repeat questions, so I thought I'd add them here for others that are trying to resolve this issue.

=== Question 1 ===
When I authenticate in the browser the cookies arn't being stores in the browser, however, I can see them in tools such as Postman, and I can even see the cookie in the response! Chris what the hell is happening?!

Okay relax, relax, the same thing happened to me after I posted this 😑*. After some debugging, and frantic searching, I realised that your frontend request NEEDS to include the 'withCredentials' header.*

In my use case I had a React JS frontend, with axios as my http library. Please see the below code for reference:

const login = async () => {
    await axios
      .post(
        `${constants.API}/auth/local`,
        {
          identifier: username,
          password,
        },
        {
          withCredentials: true, // <-- This is a critical part of the HTTP Request, if you are not using axios, please refer to the docs of your http client for a 'withCredentials' header
        }
      )
      .then(() => {
        ...
      })
      .catch((e) => {
        ...
      });
  };

>**=== Question 2 ===<br/>Does this work with the GraphQL Plugin?** 

>A fella by the name of [eikaramba](https://github.com/eikaramba) raised Issue - [Use server side cookies with graphql #3](https://github.com/christopher-talke/talke-dev/issues/3) which highlights how this can be done.

>Thank you eikaramba 👍🏽

>Anyway, if any more questions come through in the future, I'll keep adding them to this post!