nuxt-community / axios-module

Secure and easy axios integration for Nuxt 2
https://axios.nuxtjs.org
MIT License
1.19k stars 245 forks source link

proxy cookies #333

Open usb248 opened 4 years ago

usb248 commented 4 years ago

What problem does this feature solve?

Handle cookie jar in order to automatically resend received cookies on every subsequent request (server-side). Actually, axios doesn't resend cookie received (with set-cookie) by nuxt serverResponse (in a serverMiddleware for example). It cause many problem in my nuxt app.

I have to do something like this (temporary fix) :

$axios.onResponse((resp) => {
        if(resp.headers['set-cookie'] && process.server){
            try{
                // add header to nuxt's server response
                res.setHeader('Set-Cookie', [ ...resp.headers['set-cookie'] ])
            }catch(e) {}
        }
        return resp
    })

but in some case, set-cookie header is duplicated...

This feature request is available on Nuxt community (#c329)
rchl commented 4 years ago

It might make sense to have an option to "Proxy response cookies" but I have a comment for your "temporary fix". You will lose cookies already set on res if you do it this way. You want to also append existing cookie headers taken from res.getHeader('Set-Cookie').

usb248 commented 4 years ago

@rchl yes i know, but strangely res.getHeader('Set-Cookie') is always undefined even if i see double set-cookie in nuxt response in my browser to render the view and i don't understand why...so I make sure that the 'last one' Set-Cookie is always the right one... it's ugly, but it works for the moment

usb248 commented 4 years ago

a solution : https://github.com/nuxt-community/express-template/blob/master/protected-ssr-api.md#create-middlewaressr-cookiejs ?

SebastiaanYN commented 4 years ago

I'm currently dealing with the same problem. My backend adds a Set-Cookie header to the response with a new access_token whenever it has expired. This causes issues with SSR because they aren't forwarded to the client.

I've been working around this issue so far by making requests that require authentication from the client, but this takes away the smoothness of SSR.

An issue with the solution provided above is that on subsequent requests Axios might also need to access the new cookies. The ideal solution would be to mimic the behavior of browsers when dealing with cookies.

A possible solution I've been considering is to have a server-side plugin that updates the Axios cookies, and the Set-Cookie header on the response, whenever a Set-Cookie header is received.

export default function ({ $axios, res }) {
  $axios.onResponse((response) => {
    // get the set-cookie header from the response
    const setCookies = response.headers['set-cookie'];

    if (setCookies) {
      // parse the cookies axios uses
      const cookies = parse($axios.defaults.headers.common.cookie);

      // add cookies from setCookies to cookies
      // set $axios.defaults.headers.common.cookie equal to the new cookie header

      // merge the existing Set-Cookie header with setCookies
      // set the cookies on the response, so the client gets them back
      res.setHeader('Set-Cookie', setCookies);
    }
  });
}

One possible issue with this solution is that it would proxy all cookies. So a potential filter could be added to not proxy cookies such as x-powered-by, x-ratelimit-limit, x-ratelimit-remaining, and others.

It would be nice to have this as an option, so SSR can be used to the fullest, without worrying about cookies not syncing.

usb248 commented 4 years ago

@SebastiaanYN : const cookies = parse($axios.defaults.headers.common.cookie); you do anything with cookies variable. What's the point ?

to not proxy cookies such as x-powered-by, x-ratelimit-limit, x-ratelimit-remaining

It's not cookies ....

SebastiaanYN commented 4 years ago

It's not cookies ....

You're right, ignore that.

I think I managed to get a working plugin that updates cookies for Axios, and merges them on the response object. This allows sequential requests to have the correct cookies.

// plugins/ssr-cookie-proxy.js
import { parse as parseCookie } from 'cookie';

function parseSetCookies(cookies) {
  return cookies
    .map(cookie => cookie.split(';')[0])
    .reduce((obj, cookie) => ({
      ...obj,
      ...parseCookie(cookie),
    }), {});
}

function serializeCookies(cookies) {
  return Object
    .entries(cookies)
    .map(([name, value]) => `${name}=${encodeURIComponent(value)}`)
    .join('; ');
}

function mergeSetCookies(oldCookies, newCookies) {
  const cookies = new Map();

  function add(setCookie) {
    const cookie = setCookie.split(';')[0];
    const name = Object.keys(parseCookie(cookie))[0];

    cookies.set(name, cookie);
  }

  oldCookies.forEach(add);
  newCookies.forEach(add);

  return [...cookies.values()];
}

export default function ({ $axios, res }) {
  $axios.onResponse((response) => {
    const setCookies = response.headers['set-cookie'];

    if (setCookies) {
      // Combine the cookies set on axios with the new cookies and serialize them
      const cookie = serializeCookies({
        ...parseCookie($axios.defaults.headers.common.cookie),
        ...parseSetCookies(setCookies),
      });

      $axios.defaults.headers.common.cookie = cookie; // eslint-disable-line no-param-reassign

      // If the res already has a Set-Cookie header it should be merged
      if (res.getHeader('Set-Cookie')) {
        const newCookies = mergeSetCookies(
          res.getHeader('Set-Cookie'),
          setCookies,
        );

        res.setHeader('Set-Cookie', newCookies);
      } else {
        res.setHeader('Set-Cookie', setCookies);
      }
    }
  });
}

And then register it in nuxt.config.js

plugins: [
  { src: '@/plugins/ssr-cookie-proxy.js', mode: 'server' },
],
usb248 commented 4 years ago

Your code manage signed cookies too ? @SebastiaanYN

SebastiaanYN commented 4 years ago

It copies the cookies over, by name, without modifying the values. So I would expect it to work, but you'd have to test it to be 100% certain.

pi0 commented 4 years ago

@SebastiaanYN Nice work! If you would like to make a PR adding it built-in supported it is more than welcome. Also, this may you inspire for a one-liner update solution. (originally taken from express)

usb248 commented 4 years ago

Something seems to be wrong... i get duplicate set-cookie headers : image

:/

SebastiaanYN commented 4 years ago

@pi0 I'll give it a go. The reason I don't directly concat the old cookies with the new is to prevent unnecessary headers. If you make multiple requests that all overwrite a certain cookie, that could increase the header size quite a bit. And it would then also come down to the order browsers read the headers, whether they set the right cookies.

@usb248 I think this is caused by the getHeader functions returning either a string or a string[]. In this case, I only worried about string[] so it only works for multiple Set-Cookie headers. This can probably be fixed by having an arrayify function that is called on the headers.

usb248 commented 4 years ago

@SebastiaanYN res.getHeader('Set-Cookie') return undefined in my code (even when duplicate headers) ... not a string neither string[]

SebastiaanYN commented 4 years ago

That's strange. I don't know what could cause that. As far as I know res.getHeader it just the Response#getHeader function from Node itself. It should only return undefined if there's no cookie with that name.

usb248 commented 4 years ago

yes... i don't know why too. maybe @pi0 ?

duplicate headers occurs on first call (when there is no session cookie) on a route which has an axios SSR request. nuxt response + axios response. I have for example a get axios request in my nuxtServerInit (on an internal API in express in servermiddleware) which start an anonyme user session. Problem still here with arrayify function

pi0 commented 4 years ago

res.getHeader('Set-Cookie')

Maybe can try 'set-cookie? (node makes all headers lower-case)

usb248 commented 4 years ago

The same ... :/, even if in async nuxtServerInit ({ commit, dispatch }, { req, res }) { unable to get any header...

usb248 commented 4 years ago

The problem seems to come from nuxt ?? : ouput => [Object: null prototype] {}

export default function ({ $axios, res }) {
  console.log(res.getHeaders()) // Returns a shallow copy of the current outgoing headers
   ....
}

'~/plugins/proxy-cookies-ssr.server.js',

an explanation @pi0 ?

pi0 commented 4 years ago

@usb248 Browser cookies are already loaded into headers.common.cookie via req.headers. For Axios response cookies they are in response.headers['set-cookie'] You can follow #358 implementation :) BTW res.getHeader() is probably called to early in your example

usb248 commented 4 years ago

BTW res.getHeader() is probably called to early in your example

So how to get header at the right moment :satisfied: @pi0 ?

Sebastian’s code still doesn’t work for me.

res.getHeader('Set-Cookie') is always undefined in ssr plugin

sbthemes commented 4 years ago

res.getHeader('Set-Cookie') is always undefined in ssr plugin

I have same issue. In ssr plugin, res.getHeader('Set-Cookie') always returns undefined.

@usb248 You found any solution for this?

arkhamvm commented 4 years ago

@usb248 @sbthemes Take a look, maybe it helps: https://stackoverflow.com/a/34011746/1360402

pavel-lens commented 4 years ago

+1 This indeed is a significant problem.

I'm using express-session and I'm doing subsequent calls in asyncData() to load data and server-side render the page. The session should be initialized in 1st API call and then same session should be reused to access information (eg. user).

However, since $axios is not sending cookies in those calls in asyncData() a new session was recreated over a over again with every call which caused my issue of calling Auth0 /userinfo endpoint over and over again and hitting "limit exceeeded" problem.

The solution by @SebastiaanYN helped, thanks a lot!

piotrjoniec commented 4 years ago

@SebastiaanYN Your solution works. Thank you so much.

This should be a standard feature of Nuxt.

garryshield commented 3 years ago
yarn workspace <ws> add set-cookie-parser
yarn workspace <ws> add cookie-universal-nuxt
nuxt.config.js
...
modules: [
...
'cookie-universal-nuxt',
...
]
...
/store/index.js
...
import SetCookieParser from 'set-cookie-parser'

export const actions = {
    async nuxtServerInit({ commit }, { $api, $cookies }) {
        const resOrgCookies = $cookies.getAll({ fromRes: true })
        $api.onResponse(resp => {
            const setCookies = SetCookieParser.parse(resp)
            setCookies.forEach((cookie) => {
              const { name, value, ...options } = cookie
              $cookies.set(name, value, options)
            })
            return resp
        })

        const reqCookies = $cookies.getAll({ fromRes: false })
        const resCookies = $cookies.getAll({ fromRes: true })

        console.log(process.server, reqCookies, resCookies, resOrgCookies)
    }
}
...
app.controller.ts
@Get('bootstrap')
async bootstrap(@Req() req: Request, @Res() res: Response) {
    res.cookie('test', {
        access_token: 'XXX',
        refresh_token: 'YYY'
    }, {
        path: '/',
        expires: new Date(Date.now() +60*1000), // one minute
    })
    res.send({
        code: 0,
        message: 'ok',
        data: {

        }
    })
}

and then you can use cookie-parser parse request on server side do jwt verify

pavel-lens commented 3 years ago

It seems to me that all my problems were resolved by upgrading @nuxtjs/axios@5.12.1 to @nuxtjs/axios@5.13.1.

After the upgrade, I don't need plugins/ssr-cookie-proxy.js at all!

rchl commented 3 years ago

@svitekpavel AFAICS this module has no functionality to forward the response headers (cookie headers) of requests made on the server-side. So your problem was likely something else and couldn't be related to this issue.

Such functionality would be wrong in a general case as you don't want response headers of any random SSR request to be forwarded to the client. There might be specific cases where you'd want that if you ensure that you only do it for specific URLs and specific headers but that's why you have an option using response interceptor.

So for example, if your server is making an axios request to, let's say github, and that response sets some cookies, you wouldn't want those cookies to be forwarded to the client that made a request to your server. That would just not make sense in the general case.

I think this issue can basically be closed because there is solution available and IMO it wouldn't make sense to have such functionality built-in.

arkhamvm commented 3 years ago

@rchl How about domains whitelist, which will pass cookies to client? Application backend apis for example.

rchl commented 3 years ago

That could exist but maybe someone would want to have a white-list for specific cookies only? The interceptor approach is flexible and allows all that and more so I think there is no need to add anything built-in. But it's not up to me to decide anyway.

hamid159 commented 1 year ago
cookies.set(name, cookie);

@SebastiaanYN You're coping only cookie value here. but it should set other attributes of cookies too e.g, expiry, path, httponly etc. It should be

cookies.set(name, setCookie);