nuxt-community / auth-module

Zero-boilerplate authentication support for Nuxt 2
https://auth.nuxtjs.org
MIT License
1.93k stars 925 forks source link

Setting X-CSRF-TOKEN header for refresh scheme #1282

Open Destaq opened 3 years ago

Destaq commented 3 years ago

In Universal Nuxt mode when using the refresh scheme, if I refresh the page after the access token has expired, I'm unable to set the required X-CSRF-TOKEN. When refreshing manually using the .refreshTokens() method, this header is present, as it is defined in plugins/axios.js. I've spent the past few days trying to figure this out, but haven't been able to get custom schemes or plugin settings to work.

// plugins/axios.js
export default function({ $axios, app }) {
  // This is a nuxt specific instance config, this will work in
  // everyplace where nuxt inject axios, like Vue components, and store
  $axios.defaults.xsrfHeaderName = "x-csrf-token";
  $axios.defaults.xsrfCookieName = "csrf_refresh_token";

  // on request set header
  // this is being set on .refreshTokens() manually
  $axios.onRequest(config => {
    // not being set for some reason
    config.headers.common["X-CSRF-TOKEN"] = app.$auth.$storage.getCookie(
      "csrf_refresh_cookie"
    );
  });
}

However, the clear benefit of the refresh scheme is that tokens are auto-refreshed. However, while I see that the call is made, it comes back with a 401 error since the auth module is using a custom axios instance (this.$auth.ctx.$axios). Is there any way to set the CSRF header on these such calls too, from the storage cookie?

Note: if I set headers { common { 'some-header': 'something' } } in nuxt.config.js, the header is sent every time, even from ctx requests. However, I can't access the cookie there.

devzom commented 3 years ago

"headers { common { 'some-header': 'something' } } in nuxt.config.js, the header is sent every time" becouse the header is set during SSR. I have two seperated axios instances and I set the token to the 2nd one as You, but use this

axiosAuth.onRequest(
        async request => {
                    try {
                if (app.$auth.strategy.token.status().expired()) {
                    await app.$auth.refreshTokens()
                }
                // set the Authorization header using the access token fetched from login config
                request.headers['Authorization'] =
                    app.$auth.strategy.token.get()
            } catch (e) {
                console.error(e)
            }

            return request
        },
        error => {
            return Promise.reject(error)
        }
    )

and it does work

Destaq commented 3 years ago

Glad to see there's a solution @devzom , but could you please explain how you set the axios instance for when the refresh scheme is set automatically? I see that you have an axiosAuth instance, but how is it combined the the ctx instance used by node_modules/@nuxtjs/auth-next/dist/runtime.js?

I see what you mean by doing the above - setting the 'Authorization' header is not a problem. However, when I add:

request.headers['X-CSRF-TOKEN'] = app.$cookies.get("auth.csrf_refresh_cookie")

it isn't set. I can log request.headers and get the right response, but it doesn't seem to be sending to the backend.

full nuxt.config.js ```js export default function({ $axios, app }) { // This is a nuxt specific instance config, this will work in // everyplace where nuxt inject axios, like Vue components, and store $axios.defaults.xsrfHeaderName = "x-csrf-token"; $axios.defaults.xsrfCookieName = "csrf_refresh_cookie"; // on request set header $axios.onRequest( async request => { try { if (app.$auth.strategy.token.status().expired()) { await app.$auth.refreshTokens(); } // set the Authorization header using the access token fetched from login config request.headers["Authorization"] = app.$auth.strategy.token.get(); // set the x-csrf-token header taken from cookies request.headers["x-csrf-token"] = app.$cookies.get("auth.csrf_refresh_cookie"); console.log(request.headers) } catch (e) { console.error(e); } return request; }, error => { return Promise.reject(error); } ); } ```
What backend receives on /refresh ``` Connection: keep-alive Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Sec-Gpc: 1 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Referer: http://localhost:3000/ Accept-Encoding: gzip, deflate Accept-Language: en-GB,en-US;q=0.9,en;q=0.8 Cookie: auth.strategy=local; auth._token.local=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTYzMDQxNzgwNSwianRpIjoiNWQ4MjQ3ZTMtM2ViNy00YTY2LThhMDEtMjI2MzdhMGIwNDk3IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6NywibmJmIjoxNjMwNDE3ODA1LCJjc3JmIjoiMzhiYjhkMDgtYzZiOS00YzQzLTg2ZTUtMzEyNjIyMTM2YmJhIiwiZXhwIjoxNjMwNDE3ODE1fQ.XxR55VpObMpe7eeC7qrGWslYXtVg7Vo8LqGEEoRXHcE; auth.csrf_access_cookie=38bb8d08-c6b9-4c43-86e5-312622136bba; auth._refresh_token.local=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTYzMDQxNzgwNSwianRpIjoiMGYxYmI0ODctODYzOS00YjhlLTg3ZTUtZDk1NTlhMGUwZDYyIiwidHlwZSI6InJlZnJlc2giLCJzdWIiOjcsIm5iZiI6MTYzMDQxNzgwNSwiY3NyZiI6IjEwMmExMmExLWI3MDAtNDc3Ny05YTI1LTI1NzE1YzVlMDVjZSIsImV4cCI6MTYzMDQyNTAwNX0.KB7VEYifMPLXFfd6bgxCblVxfxcv6YP7VLPhkGkE7mk; auth.csrf_refresh_cookie=102a12a1-b700-4777-9a25-25715c5e05ce; auth._token_expiration.local=1630417815000; auth._refresh_token_expiration.local=1630425005000 If-None-Match: "12cc9-fgTi0+5S3MEVXmbymIL3oy15J5c" Accept: application/json, text/plain, */* Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTYzMDQxNzgwNSwianRpIjoiNWQ4MjQ3ZTMtM2ViNy00YTY2LThhMDEtMjI2MzdhMGIwNDk3IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6NywibmJmIjoxNjMwNDE3ODA1LCJjc3JmIjoiMzhiYjhkMDgtYzZiOS00YzQzLTg2ZTUtMzEyNjIyMTM2YmJhIiwiZXhwIjoxNjMwNDE3ODE1fQ.XxR55VpObMpe7eeC7qrGWslYXtVg7Vo8LqGEEoRXHcE Content-Type: application/json;charset=utf-8 Content-Length: 344 Host: localhost:5000 ```
alimuradov commented 3 years ago

I have the same problem. If you have a solution, please tell me which file you need to make changes to in order for everything to work. I am new to NuxtJs

Destaq commented 3 years ago

It seems that we can workaround this by setting any custom header + value, as this is something that cannot be done by a CSRF attack. So, hardcoding some value as in headers/common would be sufficient protection.

However, this method is not futureproof (see this comment). I'm sure there's a way to set the header with the CSRF value using Nuxt Auth, so I'm leaving this open, but this is a workaround for the time being while we wait.

devzom commented 3 years ago

It seems that we can workaround this by setting any custom header + value, as this is something that cannot be done by a CSRF attack. So, hardcoding some value as in headers/common would be sufficient protection.

However, this method is not futureproof (see this comment). I'm sure there's a way to set the header with the CSRF value using Nuxt Auth, so I'm leaving this open, but this is a workaround for the time being while we wait.

Wait, but the new x-csrf-token shouldn't be set by axios response from backend with attribute 'Set-Cookie': ['x-csrf-token', ......] then it's set on SSR by default every new $auth.refreshTokens() is done?

let's start from the begining, which type of scheme You use in nuxt.$auth and what kind of backend You use?

Destaq commented 3 years ago

Wait, but the new x-csrf-token shouldn't be set by axios response from backend with attribute 'Set-Cookie': ['x-csrf-token', ......] then it's set on SSR by default every new $auth.refreshTokens() is done?

The "workaround" here is just that: a) CSRF attacks cannot set headers b) If you set a static custom header (e.g. in nuxt.config.js), this will be sent to the server, but not in a CSRF attack c) You can check for the existence of this header to know that it isn't an attack d) This is not future proof - it works in the present, but in the future, it may be possible to send Headers in an attack.

let's start from the begining, which type of scheme You use in nuxt.$auth and what kind of backend You use?

I've attached the info in this message at the bottom. I'm using Nuxt SSR (Universal), Refresh Scheme, and a Flask backend. The issue is that when tokens are refreshed automatically (on page reload after access token has expired), the X-CSRF-TOKEN header is not set, since plugins/axios.js only works if you manually trigger the refreshTokens() function. This is because the nuxt-auth source code uses a custom axios instance.

@devzom