Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tokens in the header and cookie are rarely different #81

Open
topas opened this issue Feb 9, 2025 · 3 comments
Open

Tokens in the header and cookie are rarely different #81

topas opened this issue Feb 9, 2025 · 3 comments

Comments

@topas
Copy link

topas commented Feb 9, 2025

Hi,

First of all... thanks for this library! It's great and easy to use. In my project, I'm trying to solve a mystery when CSRF validation failed because the csrf token in the header and in the cookie didn't match (I'm logging this exact case). It's a rare issue, happening independently on OS/Platform. But I want the system to be reliable. And it seems like I have run out of ideas what could be the reason.

Could it be something like long running sessions, when react SPA is loaded a couple days ago (I store CSRF token to DOM) and then an API call refreshed the cookie?

Thanks so much!

I have a react, SSR, Vite app in this setup:

RequestContext is something passing data from the backend to the react app

const saveRequestContext = (req: any, res: any, next: NextFunction) => {
  const user = getUserFromRequest(req)
  const serverContext: RequestContext = {
    user: user,
    csrfToken: generateToken(req, res, false, false),
    locale: getLocaleFromRequestWithFallback(req)
  }
  next()
}     

csrf-csrf setup:

const { doubleCsrfProtection, generateToken } = doubleCsrf({
  getSecret: () => process.env.CSRF_SECRET ?? "123 missing secret 123",
  cookieName: "_csrf",
  cookieOptions: isDeployed() ? { sameSite: true, secure: true } : { secure: false },
  getTokenFromRequest: req => req.headers["x-csrf-token"] 
})

And wired like this in express:

...
  .use(saveRequestContext)
  .use(doubleCsrfProtection)
...

In a form, I'm using Axios with this configuration for passing CSRF token back:

...
{
    baseURL: getServerContext().baseUrl,
    data: data && JSON.stringify(data),
    withCredentials: true,
    withXSRFToken: true,
    headers: {
      "content-type": "application/json",
      "Cache-Control": "no-cache",
      "x-lng": this.contextGetter().locale,
      ...(isClientSide() && method === "POST" ? { "x-csrf-token": getRequestContext().csrfToken } : {})
    },
    method: method,
    params: params,
    validateStatus: status => status >= 200 && status < 500,
    url: url
}
@topas topas changed the title Token in the header and cookie are rarely different Tokens in the header and cookie are rarely different Feb 9, 2025
@psibean
Copy link
Contributor

psibean commented Feb 9, 2025

The first thing that comes to mind is, is the CSRF cookie expired?

What could be happening is, you're including an expired CSRF token in the header, when the request first comes in, there's no CSRF cookie with the request, it generates a new one for the session and sets the Set-Cookie header on the response. The request continues through the middleware and it essentially compares the token that was sent with the token that was just generated due to the cookie not being present (expired).

You can provide cookieOptions in the initial config to set expiry and such, and you can also provide cookieOptions to the generateToken call to control cookie settings on a per-call basis.

If you're using sessions with server side state, I'd generally recommend csrf-sync over csrf-csrf.

@topas
Copy link
Author

topas commented Feb 9, 2025

Thanks! 🤩

The cookie has "session" expiration, it should be ok, but I don't know exactly that is the cookie's lifespan in case of closing browser/closing laptop, etc. I guess there may be some corner cases.

It looks like the most reliable solution is to have dedicated /csrf-token endpoint for serving a fresh token together with up-to-date cookie. Something as is in csrf-csrf's examples. Maybe an extra backend call is worth.

What do you think?

@higbee4
Copy link

higbee4 commented Feb 21, 2025

We ran into a similar issue. Your issue seems slightly different from ours. But I thought I'd mention our solution in case you find it helpful. Turns out setting sameSite="strict" can cause problems with this. This is because if the request originates from an external source the request is considered "cross-site" and the cookie is not included in the initial request to your server. So if people are following an external link to come to your site you can run into issues where the cookie is missing from their request. Anyway, changing sameSite to "lax" fixed our issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants