Use of JWT in httpOnly cookie #4632

Agree, with one mention: it depends how csrf is implemented! You have to keep in mind that Strapi token is valid for a month! When you use httpOnly cookie without secure flag the cookie can be captured since it will remain saved in some logs. See more about httpOnly cookie - secure flag.

Regarding implementation, I think the best way is to do this on downstream, not in Strapi. This is how I did it in my Strapi Access Proxy project where I used csurf and jsonwebtoken libraries, you may want to study it to see how to implement a solution in any way you want.

Would you mind sharing the code where you implement CSRF tokens in Strapi / KOA? Im struggling with implementing this!

I didnā€™t author the original posts, only migrated the discussion from Github.

Session vs token-based authentication - interesting discussion! They each have their trade-offs as mentioned already. If using JWT for authentication, I think they should be limited to a short life span, which depends on the risk tolerance of the particular business application (on average 20 mins). It would be great if we have control over the expiry in Strapi. (Can we already do this? I havenā€™t looked into it.)

Also, my opinion is that Authentication tokens (JWTā€™s or otherwise) shouldnā€™t be stored in local or session-storage especially if they have long lifespans, due to XSS vulnerabilities.

Re: Ways of storing a JWT in a cookie and mitigating CSRF attacks: Iā€™ve used this OWASP cheatsheet as a guide in previous projects and use the double-submit method. I believe Django uses this too.
https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html

2 Likes

Totally agree, but the access can be managed in downstream, I think this is the best solution not just for Strapi, but for all apps managing sensitive information. Check my project strapi access proxy to see the concept

1 Like

Hi! Hello & thanks for your feedback.

While I do agree that modifying the original Strapi code is not ideal (and will require really keeping an eye on updates for such a key feature) my concern is the cookieSetter/cookieGetter strategy performance (maybe early optimization here?) since those middlewares will run for every single request, which seems unnecessary for a piece of code that needs to be run only at one specific almost-single-use functionality :confused:

Do you have any feedback on performance?

Best! :raised_hands:

Hi everyone.

Iā€™ve been thinking of a new way to apply to solve this in a way that doesnā€™t require overwriting the core auth controller (auth/local) nor running the cookieSetter middleware on every request/response.

I have come up with the idea of creating a custom POST endpoint (route/controller) that takes the user credentials and use them to perform a new request to Strapiā€™s core login endpoint: auth/local, capturing the response and if successful (jwt is included) adding the jwt value in a secure cookie.

Example:

Custom controller directories:
/api/custom/
/api/custom/config/
/api/custom/controllers/

Route: /api/custom/config/routes.js
:star2: Now use this route to POST your user credentials (instead of Strapiā€™s official /auth/local route).

{
  "routes": [
    {
      "method": "POST",
      "path": "/auth/cookielogin",
      "handler": "Custom.index"
    }
  ]
}

Controller: /api/custom/controllers/Custom.js

const axios = require("axios");

module.exports = {

  // GET /auth/cookielogin
  async index(ctx) {

    // Capture the request body (identifier and password)
    const { body } = ctx.request;

    // Build Strapi's Absolute Server URL.
    // Copied from https://github.com/strapi/strapi/blob/86e0cf0f55d58e714a67cf4daee2e59e39974dd9/packages/strapi-utils/lib/config.js#L62
    const hostname =
      strapi.config.environment === "development" &&
      ["127.0.0.1", "0.0.0.0"].includes(strapi.config.server.host)
        ? "localhost"
        : strapi.config.server.host;
    const absoluteURL = `http://${hostname}:${strapi.config.server.port}`;

    try {
      // Now submit the credentials to Strapi's default login endpoint
      const { data } = await axios.post(`${absoluteURL}/auth/local`, body);

      // Set the secure cookie
      if (data && data.jwt) {
        ctx.cookies.set("jwt", data.jwt, {
          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,
        });
      }

      // Respond with the jwt + user data, but now this response also sets the JWT as a secure cookie
      return ctx.send(data);

    } catch (error) {

      return ctx.badRequest(null, error);

    }
  },
};

Now you can use the cookieGetter middleware (detailed above by other users) to make sure your JWT is taken from the cookie and passed into the requests authorization headers, where Strapi is expecting them to be.

Thanks to everyone for the discussion and collaboration. I hope this is useful for some of you.

*Based on what Iā€™ve seen so far, Iā€™m quite excited about what Strapi is doing :clap:

Hi author, What is file in order to add logic cookieGetter, cookieSetter? and cokies could working with provider google or facebook?

1 Like

Okay so everyone wants to move the JWT into a cookie. Fine, but then everyone now must add some CSRF protection to ensure naughty guys from out there wonā€™t ride your authenticated session to do some nasty POSTs on your behalf.
Most common way is to use CSRF tokens. Backend (strapi) gives a frontend a CSRF token and the frontend sends it back on requests.
For those who only use AJAX to talk to strapi backend, the protection is simplified to just setting a custom header on every request to strapi. And on the strapi side, checking that the header is there (rejecting the requests that are lacking the header).

This is all easily implemented by having a custom middleware on Strapi side and by updating a frontend.
But as soon as you add CSRF checks to strapi backend, it will break GraphQL playground and Admin UI (these guys do not use CSRF tokens and they donā€™t need to).

So, how does one add CSRF protection to strapi so their frontend is secured but does not break the Admin UI?
Good Ideas anyone? Perhaps I missed something?

Hey,

I did it according to How to put JWT's in server side cookies using the Strapi user-permissions plugin | Christopher Talke | Coffs Harbour based ICT Professional | talke.dev but my front-end application cannot see the cookie. I use js-cookie (Cookies.get) but after these changes I have ā€œundefinedā€ in console.log(user). Of course, I deleted Cookies.set() and I added { withCredentials: true, } (I use axios and Next.js).

How can this be rectified? Because I donā€™t know :frowning:
Please help me.

Not sure what you mean by ā€œmy front-end application cannot see the cookieā€. The whole point of using httpOnly cookie to store JWT is so that javascript running on your front-end cannot access it. This is work by design and desired effect.
Essentially, you want your front-end not to be able to access cookie with the JWT. This is your protection from XSS attack. The cookie will be sent to the server with every request your front-end makes to the backend. And your backend will be able to access it.
The issue with the cookies though, as soon as you start using them, you opening yourself to CSRF attack, which, if you ask me, is easier to arrange compared to XSS but also easier to protect against.

Interesting problem. I did this by implementing a ā€œblacklistā€ of routes that are to be CSRF protected. For every request, I check if the root pathname matches an item in the list e.g. ā€˜restaurantsā€™. If not, it bypasses CSRF protection. I also only look for state-changing HTTP methods e.g. POST, PUT, PATCH, DELETE.

You can use the strapi.models object to generate this blacklist when your middleware is first loaded. You will need to consider models that are collections, single-types as well as default strapi routes like /users. I specifically used the controller name and pluralized it for collection-types.

Admin UI routes should then bypass anti-CSRF. I havenā€™t tried this with GraphQL playground though.

If you want a more sophisticated way of generating a list of routes based on your api models, have a look at the source code for strapi-middleware-cache for some inspiration.

Iā€™m interested to hear any other ideas people have for this as well.

Yeah, applying CSRF selectively to some routes is what I was thinking too as I donā€™t really see any other way around it. Although, to be honest, when I was thinking about it, it seems to me that storing JWT in localStorage as opposed to httpOnly cookie is not that bad.
Think about it, no cookies, no CSRF. As to XSS, well, it is harder attack to setup, besides, if someone can successfully execute XSS on your website, then game is probably over, regardless of where you store your JWT.

hi all, I created a package that can handle this JWT on cookies thing, with some more features incoming.

@bwyx/strapi-jwt-cookies

it only applies middleware to the auth routes so that it wonā€™t affect any other routes. And the installation is easy-peasy. check it out.

3 Likes

I know itā€™s a rather old topic, however on Strapi version 3.6.10 the Permissions.js modification (as per Talke post) that reads the JWT from HTTPOnly cookie apparently is not being executed when requesting the GET /users/me endpoint.
When I try to access any other endpoint of my collection types, the JWT is retreived from cookie and passed to the Authorization header, hence - Iā€™m able to retreive the data, but whenever I try to query the users/me it always returns ā€œNo authorization header was foundā€.
Could it be that thereā€™s different policy applied to /users/me endpoint?

@bayuā€™s solution seems to do the trick for REST APIs. how could we extend this to GraphQL as well. or do we bypass this by using REST for auth and then relying on the cookie?