Morgbn / nuxt-csurf

Nuxt Cross-Site Request Forgery (CSRF) Prevention
https://nuxt-csurf.vercel.app
MIT License
81 stars 16 forks source link

csrfFetch cannot be used in composables with SSR/hydrating: <no response> Failed to parse URL from #47

Open JoniLieh opened 2 months ago

JoniLieh commented 2 months ago

Hey there, im trying to do a request via a proxy with SSR, the normal $fetch from nuxt is working:

const {
  csrf
} = useCsrf()

// not working (error from title)
await $csrfFetch<User>(`/api/auth/user`)
// working
await $fetch<User>(`/api/auth/user`)

// undefined
console.log(csrf);

I've been logging the csrf token, apparently on the server the token is undefined

My setup:

JoniLieh commented 2 months ago

So this happends when just using $csrfFetch inside a pinia store and using it to fetch while hydration/ssr

// pinia store
const GetOrganizations = async () => {
    try {
      return $csrfFetch<Organization[]>('/api/organizations')
    } catch (e: any) {
      throw e
    } 
  }

// somewhere in setup page function
const { GetOrganizations } = useOrganizationStore();

const { data, refresh, status, error } = await useAsyncData("organizations", () =>
  GetOrganizations()
); // Error: [GET] "/api/organizations": <no response> Failed to parse URL from /api/organizations

But doing it without the store works:

const { data, refresh, status, error } = await useCsrfFetch<Organization[]>("/api/organizations");

Same with composable, inside a composable you can't use $csrfFetch while hydration/ssr, but inside a setup function useCsrfFetch works.

JoniLieh commented 2 months ago

Workaround

So I thought it's a initialization issue here, but wrapping it inside a function etc. and logging useCsrf() works, since $csrfFetch only adds the token to the header I'm using a custom fetch function from here https://nuxt.com/docs/guide/recipes/custom-usefetch and adding the csrf token there

const {
  csrf,
  headerName
} = useCsrf();
...
onRequest({ request, options, error }) {
    const headers: HeadersInit = options.headers ||= {}

    // Add CSRF token
    if (csrf) {
      if (Array.isArray(headers)) {
        headers.push([headerName, csrf])
      } else if (headers instanceof Headers) {
        headers.set(headerName, csrf)
      } else {
        headers[headerName] = csrf
      }
    }
  },
  ...
f754699 commented 2 months ago

解决方法

所以我认为这是一个初始化问题,但是将其包装在函数等中并进行日志记录有效,因为$csrfFetch仅将令牌添加到标头中,我正在使用自定义获取函数从这里 https://nuxt.com/docs/guide/recipes/custom-usefetch 并在那里添加 csrf 令牌useCsrf()

const {
  csrf,
  headerName
} = useCsrf();
...
onRequest({ request, options, error }) {
    const headers: HeadersInit = options.headers ||= {}

    // Add CSRF token
    if (csrf) {
      if (Array.isArray(headers)) {
        headers.push([headerName, csrf])
      } else if (headers instanceof Headers) {
        headers.set(headerName, csrf)
      } else {
        headers[headerName] = csrf
      }
    }
  },
  ...

我是用了你的方法,但是依然报403,因为服务端没有cookie,解析headerName失败

I used your method, but still reported 403 because the server did not have a cookie and failed to parse the “headerName”

f754699 commented 2 months ago

我的解决方案:

//  plugins/request.ts
import {useCsrf, defineNuxtPlugin, useCookie,} from "#imports";

export default defineNuxtPlugin((nuxtApp) => {
    const mySsrFetch = $fetch.create({
        onRequest({request, options, error}) {
            const {
                csrf,
                headerName
            } = useCsrf();
            if (csrf) {
                const headers:any = {}
                headers[headerName] = csrf
                options.headers = headers
                headers.Cookie = nuxtApp.ssrContext?.event.req.headers.cookie
            }
        },
    })

    // Expose to useNuxtApp().$api
    return {
        provide: {
            mySsrFetch
        }
    }
})
JoniLieh commented 2 months ago

In which context are you using your plugin? This is my final file:

export default defineNuxtPlugin({
  name: 'apiFetch',
  parallel: true,
  setup(nuxtApp) {
    const {
      accessToken
    } = useUserStore();
    const {
      csrf,
      headerName
    } = useCsrf();

    const apiFetch = $fetch.create({
      baseURL: '/api',
      onRequest({ request, options, error }) {
        const headers: HeadersInit = options.headers ||= {}

        // Add Authorization header
        if (accessToken) {
          if (Array.isArray(headers)) {
            headers.push(['Authorization', `Bearer ${accessToken}`])
          } else if (headers instanceof Headers) {
            headers.set('Authorization', `Bearer ${accessToken}`)
          } else {
            headers.Authorization = `Bearer ${accessToken}`
          }
        }

        // cache control
        if (!options.cache) {
          // Handle different header types
          if (Array.isArray(headers)) {
            // Check if cache-control header is already present in the array
            const hasCacheControl = headers.some(([key]) => key.toLowerCase() === 'cache-control');
            if (!hasCacheControl) {
              headers.push(['cache-control', 'default']);
            }
          } else if (headers instanceof Headers) {
            // Check if cache-control is already set
            if (!headers.has('cache-control')) {
              headers.set('cache-control', 'default');
            }
          } else if (typeof headers === 'object') {
            // Treat headers as a plain object
            if (headers['cache-control'] === undefined) {
              headers['cache-control'] = 'default';
            }
          }
        }

        // Add CSRF token
        if (csrf) {
          if (Array.isArray(headers)) {
            headers.push([headerName, csrf])
          } else if (headers instanceof Headers) {
            headers.set(headerName, csrf)
          } else {
            headers[headerName] = csrf
          }
        }
      },
    })

    // Add the fetch method to the context
    return {
      provide: {
        apiFetch
      }
    }
  },
})
Morgbn commented 2 weeks ago

are you still facing difficulties with the latest version (fetch management has changed on my side)?