JiProchazka / nuxt-cookies-auth

Nuxt 3 module for using httpOnly cookies tokens and their refreshing
3 stars 0 forks source link

Authentication does not work after page refresh #2

Closed jskitz closed 9 months ago

jskitz commented 9 months ago

Thanks for this tool. It has definitely got me thinking as to the best way to get cookie based authentication working in my project. I'm so very surprised how hard this has been and that Nuxt itself does not provide guidance on doing something so common in web development, which is authenticating to your backend using cookie based http-only cookies. Anyway, I integrated your code into my project, and things seemed to work properly. But as soon as I reload the site, which I'm guessing means that the server needs to be accessing the API, I am no longer logged in and it seems like my session data has been cleared out. I can then login again, and everything works again, but I need the server to also have access to the cookies as well.

Any guidance on getting this to work correctly?

JiProchazka commented 9 months ago

Hi @jskitz, thanks for the post. It should be definitely working with a page reload as well. I'm using it that way. I guess it is probably in configuration. Can you provide some basic app or at least the code pieces related to the configuration?

jskitz commented 9 months ago

Hi @JiProchazka, thank you for the quick response. I've tracked the problem down, but I'm not entirely sure how to fix it when developing locally.

The problem seems that the Nuxt server process is having a hard time connecting to my backend, which is running locally at 127.0.0.1:8000 (it's a Django backend, using cookie based JWT tokens). This is the reason why authentication is not working for me on a refresh.

The exact error coming back is:

TypeError: fetch failed
    at Object.fetch (node:internal/deps/undici/undici:11576:11)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async $fetchRaw2 (file:///Users/jason/Sites/tjskillup/node_modules/ofetch/dist/shared/ofetch.00501375.mjs:219:26)
    at async $fetch2 (file:///Users/jason/Sites/tjskillup/node_modules/ofetch/dist/shared/ofetch.00501375.mjs:261:15)
    at async Proxy.fetchUser (/Users/jason/Sites/tjskillup/stores/authStore.js:20:33)
    at async setup (/Users/jason/Sites/tjskillup/app.vue:31:12) {
  cause: Error: connect ECONNREFUSED ::1:8000
      at __node_internal_captureLargerStackTrace (node:internal/errors:496:5)
      at __node_internal_exceptionWithHostPort (node:internal/errors:671:12)
      at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1495:16) {
    errno: -61,
    code: 'ECONNREFUSED',
    syscall: 'connect',
    address: '::1',
    port: 8000
  }
}

I'm still tracking this down, but does not seem related to nuxt-cookies-auth.

jskitz commented 9 months ago

@JiProchazka I've made some progress. It's still not working, but at least I'm getting server API calls and client API calls made.

Here is what I'm running up against now:

Failure Mode 1

If I set apiBaseUrl: "http://localhost:8000/api", then the front end works correctly. I can login, and then make subsequent requests to the backend using cookie authentication. However, with the apiBaseUrl set to using localhost, no server requests will be made and I receive the ECONNREFUSED issue listed above.

Failure Mode 2

If I set apiBaseUrl: "http://127.0.0.1:8000/api", then the Nuxt server process will make connections, but it still does not refresh the token because now the front end does not work. Trying to login and then make subsequent requests afterwards does not work correctly on the front end. So this is why the backend likely also does not work.

I think if I can figure out why cookies are not properly being set at 127.0.0.1, maybe I can get the Nuxt server process to work as well.

JiProchazka commented 9 months ago

Isn't it related to the domain? How do you set cookies on the backend? In most cases to have cookies correctly working you must run frontend and backend on the same domain of the second level.

http://localhost:8000 and http://localhost:3000 is not enough. For example I have: http://app.myapp.local:3000 for frontend and http://app.myapp.local:4000 for backend.

and in my /etc/hosts I have: 127.0.0.1 localhost app.myap.local

jskitz commented 9 months ago

I tried doing what you said, and mapped a domain myapp.local in my /etc/hosts file, and I'm getting failures on both sides now. The front end can connect and login, but subsequent requests are not authenticated properly. The Nuxt server process will also not make server requests to that URL. It seems like only if set the URL to 127.0.0.1 will the Nuxt server process send a request to the server.

I'm not sure where to go from here. But I can't consistently get the Nuxt server process and the browser environment to handle things in the same way. I'm also not sure why changing from localhost to anything else does not allow me to stay authenticated on the front end.

Lots of mysteries here.

JiProchazka commented 9 months ago

Did you logout after changing the hosts file?

Do you see cookies being set in chrome dev console? It is also showing a warning triangle with the explanation why it wasn't set...

jskitz commented 9 months ago

Okay, I'm back to square one again. I have the frontend working using a domain at myapp.local. My problem was that I had the cookie samesite parameter set to 'lax' and tried accessing it from localhost. So I have that squared away, but the problem now is still that the Nuxt server process is still giving me this error:

cause: Error: connect ECONNREFUSED ::1:8000
      at __node_internal_captureLargerStackTrace (node:internal/errors:496:5)
      at __node_internal_exceptionWithHostPort (node:internal/errors:671:12)
      at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1495:16) {
    errno: -61,
    code: 'ECONNREFUSED',
    syscall: 'connect',
    address: '::1',
    port: 8000
  }

I can only seem to fix this by setting my backend server to 127.0.0.1. It seems like it cannot make that request if it does a dns name lookup. However, it does know that myapp.local is localhost. I've been talking to Nuxt folks over in discord as well, and someone had said they changed the order of ipv6 in their /etc/hosts, and this fixed it. But I've had no such luck.

JiProchazka commented 9 months ago

So let me be clear:

you have a FE running on http://myapp.local:XXXX and BE running on http://myapp.local:8000? And when you put http://myapp.local:8000 to the browser you get the api?

jskitz commented 9 months ago

Here is how things are configured:

Django BE is running at http://myapp.local:8000/

Nuxt is running at http://myapp.local:5010/. The problem I was having previously, is that I had Nuxt running at http://localhost:5010 and the cookies were not being set because of same site lax setting. So, once I changed this to run at myapp.local, then the front end of Nuxt is able to authenticate and make authenticated requests using your tool.

But then, I do a refresh on Nuxt after I've authenticated, and I get the ECONNREFUSED error. When I look at Django logs, Nuxt did not even try to make the connection. It didn't even get that far in trying to make the connection. Also, I've spit out what would have been sent, and it does look like the cookies would have been included if I can just get Nuxt to make these calls from the server when it's anything about 127.0.0.1.

I'm going to try 2 things:

  1. This is such weirdness that I'm going to restart my computer
  2. I'm going to try and see if I can get cookies to work just using 127.0.0.1 on both front end and Nuxt server since it seems like this is the only way that Nuxt will actually make the request.

I'm a bit new to Nuxt, and I find the abstraction between client and server quite confusing honestly. And this bug has certainly not helped my mental model. The Nuxt folks, including Daniel Roe, have been super helpful in trying to help me get this running, but so far, no one knows why it's not working.

JiProchazka commented 9 months ago

And do you have set NUXT_PUBLIC_BASE_URL=http://myapp.local:8000 in .env file?

You must set the backend on both places:

My configuration looks like this:

.env:

NUXT_PUBLIC_BASE_URL=http://myapp.local:8000

nuxt.config.ts:

export default defineNuxtConfig({
  ...
  runtimeConfig: {
    public: {
      BASE_URL: ""
    }
  },
 ...
  cookiesAuth: {
    apiBaseUrl: "http://myapp.local:8000",
    refreshTokenUrl: "/auth/refresh"
  }
})
jskitz commented 9 months ago

I did not have baseUrl set, but I have done that now. Should the baseUrl be set to the Django backend URL or to where Nuxt is running? I figured that should be set to where Nuxt is running, but the cookiesAuth.apiBaseUrl is set to where Django is running in your example.

Either way, I've tried both and it is still refusing to connect server side to my Django app.

I've spit out some information from useCookiesAuth and it all seems correct to me. But let me know if you see anything strange:

this is ...useCookiesAuth()

{
  retryStatusCodes: [ 401 ],
  retry: 1,
  baseURL: 'http://myapp.tj.local:8000/api',
  credentials: 'include',
  onResponseError: [AsyncFunction: onResponseError],
  onRequestError: [Function: onRequestError]
}

This is useCookiesAuth header variable:

{
  host: 'myapp.tj.local:5010',
  connection: 'close',
  '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/121.0.0.0 Safari/537.36',
  accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
  'accept-encoding': 'gzip, deflate',
  'accept-language': 'en-US,en;q=0.9',
  cookie: 'access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzA3NDMwODA1LCJpYXQiOjE3MDczNDQ0MDUsImp0aSI6IjIwODMxY2QwODI2NzQxM2U5MjE1M2Q5MjllMWQ3ODVhIiwidXNlcl9pZCI6MX0.tYuk309WtWIlKQWnDXRiiiMmPMQbQZvd27nJ3XqXOLU; csrftoken=kxCQtcZlybFUWORG6dltNSMPrSjF5es0',
  'x-forwarded-for': '127.0.0.1',
  'x-forwarded-port': '56099',
  'x-forwarded-proto': 'IPv4'
}

Here is the URL it failed to request on http://myapp.tj.local:8000/api/auth/users/me/ sending back the error ECONNREFUSED.

Question: Are you proxying your requests to your backend via Nuxt /server/api routes, because I am not doing this. I'm sending requests from a Pinia store.

jskitz commented 9 months ago

Alright, I am past the ECONNREFUSED problem. This was a node 18 problem. I upgraded to node 20, and now this is not happening anymore. Now I am getting back a 403 from my server when trying to make an authenticated request from the server. This is progress though. I can work through what my server is receiving and see if I can find any issues with what's coming over the wire.

And thanks again for your all your help. Super appreciate it!

jskitz commented 9 months ago

Hi @JiProchazka I'm pretty sure that I've confirmed that the reason your project is working is that your service returns a 401 on the first failed server call (mine returns 403), and then you refresh, which brings the state back in. Basically, I bypassed your project to retrieve the user from the server instead of doing it through userCookiesAuth, and it worked right away. Here is what I think I see as the problem. Please let me know if you think I'm correct:

In useCookiesAuth(), this is basically what you return:

  return {
    retryStatusCodes: [401],
    retry: 1,
    baseURL: config.public.apiBaseUrl,
    credentials: credential,
    onResponseError: async (context: FetchContext) => {
    ...

The line credentials: include will have no effect on server requests, because it won't have access to the cookie.

But, in your onResponseError callback, you are doing the right thing with the cookies by including it in the next request as shown here:

          headers: {
            cookie: header.cookie,
          },

My server returns a 403 when I can't authenticate. I'm guessing yours might return a 401 and then you just retry with the cookie brought in on the retry.

The test that I ran is just doing the following in my App.vue script setup bypassing useCookiesAuth()

const headers = useRequestHeaders(['cookie'])
const config = useRuntimeConfig()

const { data } = await useFetch('/auth/users/me/', {
  baseURL: config.public.apiBaseUrl,
  headers,
})

And this returned my user data right away, no problem. Let me know if the above seems reasonable or if I'm missing something.

JiProchazka commented 9 months ago

Hi, yes, it is exactly as you said. It is meant to be returned 401. Is there any particular reason why you are returning 403 instead? I could make a newer version with this value configurable, but 401 is the correct status here..

And thanks again for your all your help. Super appreciate it!

Yes, you are welcome! As you said - I was surprised that such basic thing as cookies based token authentication is not covered in Nuxt. I'm happy someone is using it!

jskitz commented 9 months ago

Hi,

You are absolutely correct, and my code is not working correctly. I should be returning a 401 from the server and not a 403 in this case. I'm going to work on that today, and hopefully everything then works as planned.

Now that I understand everything, I really think that they way you have implemented this is excellent! With this solution, we don't have to keep any state anywhere except for in the cookies, and everything just works as it should. It's a very clean and elegant solution to this, and I believe that your solution should be the recommended approach and discussed somewhere in the Nuxt docs. I find many of the examples in Nuxt docs to not be very clear. While the information is there, it's just not very useful unless you already really know the architecture of Nuxt fairly well.

Thanks for the thoughtful conversation here. Much appreciated!

JiProchazka commented 9 months ago

Thanks man! Good idea, will try to note it in the Nuxt discord.

Glad to help anytime!