manchenkoff / nuxt-auth-sanctum

Nuxt module for Laravel Sanctum authentication
https://manchenkoff.gitbook.io/nuxt-auth-sanctum/
MIT License
149 stars 18 forks source link

[Help needed] I cannot make it work with SSR enabled (two days trying to fix) #57

Closed maxacarvalho closed 5 months ago

maxacarvalho commented 5 months ago

Hi,

This is a desperate attempt since I'm two days in a row trying to find what's wrong without success.

I have a running Laravel application, using Laravel v10. I'm starting a new frontend app with Nuxt 3 and I'm using your module because it's very cool.

Everything works as expected if I set ssr: false. If not, I get the following error:

 ERROR  Unable to load user identity [GET] "https://manager.nicksaude.test/api/user": <no response> fetch failed

  at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
  at async $fetch2 (node_modules/ofetch/dist/shared/ofetch.37386b05.mjs:268:15)
  at async node_modules/nuxt-auth-sanctum/dist/runtime/plugin.mjs:23:120
  at async setup (virtual:nuxt:/Users/maaxcarvalho/Code/nuxt-example-app/.nuxt/plugins/server.mjs:46:116)
  at async Object.callAsync (node_modules/unctx/dist/index.mjs:72:16)
  at async applyPlugin (node_modules/nuxt/dist/app/nuxt.js:116:25)
  at async executePlugin (node_modules/nuxt/dist/app/nuxt.js:153:9)
  at async Module.applyPlugins (node_modules/nuxt/dist/app/nuxt.js:161:5)
  at async createNuxtAppServer (node_modules/nuxt/dist/app/entry.js:26:7)
  at async default (node_modules/@nuxt/vite-builder/dist/runtime/vite-node.mjs:33:18)

[nuxt-app] page:loading:start: 0.017ms
[nuxt-app] app:created: 0.882ms
[nuxt-app] vue:setup: 0.004ms

 WARN  Cannot stringify arbitrary non-POJOs FetchError

Module information

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: true },
  debug: true,
  modules: ['@nuxt/ui', 'nuxt-auth-sanctum'],
  sanctum: {
    baseUrl: 'https://manager.nicksaude.test',
    endpoints: {
      csrf: '/sanctum/csrf-cookie',
      login: '/login',
      logout: '/logout',
      user: '/api/user',
    },
    redirect: {
      keepRequestedRoute: false,
      onLogin: '/protected',
      onLogout: '/',
      onAuthOnly: '/login',
      onGuestOnly: '/',
    },
  },
  devServer: {
    host: 'app2.nicksaude.test',
    https: {
      key: "./key.pem",
      cert: "./cert.pem",
    },
  },
})

Nuxt environment:

Laravel environment:

Additional context

I tried many ways to try to figure out the issue, without success. For example, I tried to install a fresh Laravel application and point the app to it. It works as expected. So it must be something related to my Laravel application but I can't understand what, the error message doesn't make much sense to me. For example, I can see that the Nginx that sits in front of the Laravel app gets the request:

web-1  | 192.168.158.1 - - [26/Mar/2024:14:55:39 -0300] "GET /api/user HTTP/2.0" 401 30 "https://app2.nicksaude.test:3000/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" -- "0.363 0.363 ."

Also, when I turn off SSR and use the app as a PWA everything works as expected. So I'm utterly lost.
I searched the issues and found some that seemed related but, none of the possible fixes solved my problem. This is a desperate attempt to get this thing fixed :(

Thanks.

manchenkoff commented 5 months ago

Hey @maxacarvalho, thanks for a detailed description!

Since I noticed the custom domain in the request, I would like to ask you to provide a configuration of the stateful domains from the Laravel app and also make sure that you have SESSION_DOMAIN set to .nicksaude.test.

By default your Nuxt tries to send a request with app2.nicksaude.test as origin, so it has to be registered in your backend as a stateful domain as well.

And also, could you please provide more details about your dev environment, for instance, do you use simple Docker or something else for Laravel? Do you run Nuxt app in a docker or just in your console?

maxacarvalho commented 5 months ago

Hi @manchenkoff

My Laravel application runs from a Docker and it's fully accessible from my local computer (host).
The SESSION_DOMAIN is set to .nicksaude.test.
The Nuxt app is running from outside the Docker env. For instance, it was running from another container but I started to run it outside in order to check if that wasn't the issue.

I know the Laravel Sanctum is working because I have other apps using it as a backend. The other apps are running Vue with Vite, PWA only. I can also confirm that the Nuxt app works if I set the ssr to false, I can login and get the logged user back as expected. It only fails when ssr is turned on.

manchenkoff commented 5 months ago

@maxacarvalho Okay, in my case, I use Docker for Laravel and Nuxt with SSR in console mode. Could you check if this combination works with localhost and empty SESSION_DOMAIN instead?

maxacarvalho commented 5 months ago

Hi @manchenkoff, I'm not sure I got it. What do you mean by Nuxt with SSR in console mode.
In order to access my Larave application using localhost I'll have to change a lot of things since it's sitting behind an Nginx. I'd have to change the way the container runs and execute php artisan instead (unless you know any other trick).

I also don't use, for example, Laravel Sail. I have a container based on Alpine with the necessary dependencies installed.

To give you a picture of it:

My docker-compose.yml will produce something like

manchenkoff commented 5 months ago

What do you mean by Nuxt with SSR in console mode.

I meant just running it via the default nuxt dev command, not as a docker container or via process managers. I have SSR enabled, so it works with Laravel app (that is in the sail-based docker container) for both client and server modes since they use same origin, which is localhost in my case.

In order to access my Larave application using localhost I'll have to change a lot of things since it's sitting behind an Nginx. I'd have to change the way the container runs and execute php artisan instead (unless you know any other trick).

Yeah, php artisan serve would also work, I guess in this case, you need to adjust just env variables to connect to other containers like mysql/redis

maxacarvalho commented 5 months ago

@manchenkoff I'm running the Nuxt app with npm run dev from my local machine, outside docker. you're correct, I suppose I can simply run php artisan serve from the container and check if it works. I'll do that and come back to you.

manchenkoff commented 5 months ago

@maxacarvalho I was checking around what might be a reason and found this Nuxt issue with a similar problem, so just in case, could you also provide an example of code you are working with?

maxacarvalho commented 5 months ago

Hi @manchenkoff I managed to play with the php artisan serve option and it works as expected.
But, when a switch back to the domain it gives me the same error.

Just to be very clear about it:

I put together a very simple Nuxt app, I used your playground example basically.

nuxt.config.ts

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: true },
  modules: ['nuxt-auth-sanctum'],
  sanctum: {
    baseUrl: "https://manager.nicksaude.test",
    endpoints: {
      csrf: '/sanctum/csrf-cookie',
      login: "/api/nick-patients/login",
      logout: "/front/auth/logout",
      user: "/api/nick-patients/patient",
    },
    redirect: {
      keepRequestedRoute: true,
      onLogin: '/protected',
      onLogout: '/',
      onAuthOnly: '/login',
      onGuestOnly: '/',
    },
  },
  devServer: {
    host: 'app2.nicksaude.test',
    https: {
      key: "./key.pem",
      cert: "./cert.pem",
    },
  },
})

app.vue

<template>
  <div>
    <NuxtPage />
  </div>
</template>

pages/index.vue

<script lang="ts" setup>
</script>

<template>
  <div>
    <p>
      Welcome to demo application for Nuxt & Laravel Sanctum integration! Feel
      free to use navigation menu to check pages' behavior
    </p>

    <NuxtLink to="/protected">
      About page
    </NuxtLink>
  </div>
</template>

pages/login.vue

<script setup lang="ts">
definePageMeta({
  middleware: ['sanctum:guest'],
});

const { login } = useSanctumAuth();
const route = useRoute();

const credentials = reactive({
  username: '',
  password: '',
  remember: false,
});

const loginError = ref('');

async function onFormSubmit() {
  try {
    await login(credentials);
  } catch (error) {
    loginError.value = error as string;
  }
}
</script>

<template>
  <div v-if="route.query.redirect">
    Hmmm, looks like you tried to open
    <em>"{{ route.query.redirect }}"</em> page, login first to access it and
    we can redirect you there
  </div>

  <h2>Login form</h2>

  <p v-if="loginError" class="error-message">Error - {{ loginError }}</p>

  <form class="login-form" @submit.prevent="onFormSubmit">
    <div class="input-group">
      <label for="email">username</label>
      <input
        id="username"
        v-model="credentials.username"
        type="text"
        name="username"
      />
    </div>

    <div class="input-group">
      <label for="password">Password</label>
      <input
        id="password"
        v-model="credentials.password"
        type="password"
        autocomplete="current-password"
        name="password"
      />
    </div>

    <div class="input-group">
      <label for="remember">Remember me</label>
      <input
        id="remember"
        v-model="credentials.remember"
        type="checkbox"
        name="remember"
      />
    </div>

    <button type="submit">Log in</button>
  </form>
</template>

protected.vue

<script lang="ts" setup>
definePageMeta({
  middleware: ['sanctum:auth'],
});

interface MyUser {
  id: number;
  name: string;
  email: string;
  email_verified_at: string;
  created_at: string;
  updated_at: string;
}

const { isAuthenticated, user, refreshIdentity } = useSanctumAuth();
</script>

<template>
  <div>
    <h1>Protected</h1>

    <p>Your authentication status - {{ isAuthenticated }}</p>

    <code>Identity object - {{ user }}</code>

    <div><button @click="refreshIdentity">Refetch user</button></div>
  </div>
</template>

One important thing

If I ignore the error and go to the login page I can login, I can see the user data. But if I refresh the page it redirects me back to the login page.

So, the flow is:

  1. Access the protected page
  2. Since there's no session, redirects to the login page
  3. Login and see the user data
  4. Refresh the page, get the error and be redirected back to login
  5. Even without logging in again, if I simple click to go to the protected page again, I can see the user data, so the session is there.
  6. If I refresh, boom, back to the login page

And here's the error again

 ERROR  Unable to load user identity [GET] "https://manager.nicksaude.test/api/nick-patients/patient": <no response> fetch failed

  at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
  at async $fetch2 (node_modules/ofetch/dist/shared/ofetch.37386b05.mjs:268:15)
  at async node_modules/nuxt-auth-sanctum/dist/runtime/plugin.mjs:23:120
  at async setup (virtual:nuxt:/Users/maaxcarvalho/Code/nuxt-example-app/.nuxt/plugins/server.mjs:38:116)
  at async Object.callAsync (node_modules/unctx/dist/index.mjs:72:16)
  at async applyPlugin (node_modules/nuxt/dist/app/nuxt.js:116:25)
  at async executePlugin (node_modules/nuxt/dist/app/nuxt.js:153:9)
  at async Module.applyPlugins (node_modules/nuxt/dist/app/nuxt.js:161:5)
  at async createNuxtAppServer (node_modules/nuxt/dist/app/entry.js:26:7)
  at async default (node_modules/@nuxt/vite-builder/dist/runtime/vite-node.mjs:33:18)
manchenkoff commented 5 months ago

Hey @maxacarvalho, thanks for the details. It is definitely related to the request headers/cookies on the server side, I assume that the origin header is different or the cookie domain is incorrect when Nuxt sends an SSR request. I will try reproducing it on my local machine with your configuration and will get back to you!

manchenkoff commented 5 months ago

Hi @maxacarvalho, I've tried a lot of different combinations but couldn't reproduce exactly the same behavior as yours. Let's make sure that we have proper configurations 😄

  1. I have two frontend instances for the experiment
    • domain.test (or www.domain.test)
    • subapp.domain.test
  2. They have this configuration
    // https://nuxt.com/docs/api/configuration/nuxt-config
    export default defineNuxtConfig({
    devtools: { enabled: true },
    modules: ['nuxt-auth-sanctum'],
    sanctum: {
    baseUrl: "http://api.domain.test",
    origin: "http://domain.test", // for 2nd app I use 'http://subapp.domain.test'
    },
    devServer: {
    host: 'domain.test', // for 2nd app I use 'subapp.domain.test'
    },
    })
  3. Laravel backend works on api.domain.test, and here are some configs
    // .env
    APP_URL=http://api.domain.test
    FRONTEND_URL=http://domain.test
    BACKOFFICE_URL=http://subapp.domain.test
    SESSION_DOMAIN=.domain.test
    // config/sanctum.php
    'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
    '%s%s%s%s%s',
    'localhost,localhost:3000,localhost:3001,127.0.0.1,127.0.0.1:8000,::1',
    env('APP_URL') ? ',' . parse_url(env('APP_URL'), PHP_URL_HOST) : '',
    env('FRONTEND_URL') ? ',' . parse_url(env('FRONTEND_URL'), PHP_URL_HOST) : '',
    ',' . parse_url('http://www.domain.test', PHP_URL_HOST),
    env('BACKOFFICE_URL') ? ',' . parse_url(env('BACKOFFICE_URL'), PHP_URL_HOST) : '',
    ))),
    // config/cors.php
    'allowed_origins' => [
    env('FRONTEND_URL', 'http://localhost:3000'),
    env('BACKOFFICE_URL', 'http://localhost:3001'),
    'http://domain.test',
    'http://www.domain.test',
    ],

Can you check that it is the same with yours Laravel configuration and if it is not, then could you please share it?

maxacarvalho commented 5 months ago

Hey @manchenkoff For a moment I thought today would be an amazing Friday!

Here's what I did

  1. Configured everything to use http instead of https, backend and frontend
  2. I noticed that the Sanctum setting origin on the Nuxt app isn't working. It still uses the app hostname, localhost:3000 for both Origin and Referer. I had to update the nuxt.config.ts file devServer.host for it to work.
  3. After that I could login and I didn't see the error that led me to this issue.

Second phase, https.

Now I turned everything back to https, including the Nuxt app, with a self-signed certificate created with mkcert. With https I see the same error again, so no lucky. But, on the bright side, I could narrow down the issue to https.

After realising that, I added NODE_TLS_REJECT_UNAUTHORIZED=0 to my .env. Once I did that I stopped seeing the error ERROR Unable to load user identity [GET] "https://manager.nicksaude.test/api/nick-patients/patient": <no response> fetch failed in the console, but, it's still not behaving properly.
Here's the flow:

  1. Open the login form
  2. Type in the credentials and submit
  3. I can see the request on the Chrome Devtools, so using a client call
  4. Everything goes well and I get authenticated.
  5. If I refresh the page I see the login page for a sec, then I see a call to the backend from the Chrome Devtools, then I'm redirected back to the dashboard.

So, with the NODE_TLS_REJECT_UNAUTHORIZED=0 set I don't see the terminal error anymore. But it seems to me that the Nuxt backend call still fails. It's clear to me that's related to HTTPS, I just don't know how to fix it and make it work for local development.
The fact that the origin setting didn't work also bugs me.

manchenkoff commented 5 months ago

2. I noticed that the Sanctum setting origin on the Nuxt app isn't working. It still uses the app hostname, localhost:3000 for both Origin and Referer. I had to update the nuxt.config.ts file devServer.host for it to work.

Here is the behavior of the module, it checks config first but if it is undefined or null, it falls back to useRequestUrl().origin which might have your localhost:3000. Anyway, looks weird to me that setting does not work for some reason 🤔

5. If I refresh the page I see the login page for a sec, then I see a call to the backend from the Chrome Devtools, then I'm redirected back to the dashboard.

When you refresh the page, Nuxt sends a request from the SSR side, so I assume we can find a difference between those 2 calls (CSR / SSR). Do you have any way to extract logs from your Laravel proxy nginx? For instance headers and cookies

maxacarvalho commented 5 months ago

Ok, let's go!

First of all: it's working!!!

Now, here are my findings.

If I don't set NODE_TLS_REJECT_UNAUTHORIZED=0 it won't work with SSL, that's a fact.

When I test from the console (not from Docker) running npm run dev I must set the devServer.host anyway, otherwise I cannot use https since my self-signed cert is bound to the domain I'm running with.

I did investigate the Nginx headers and, when the call comes from the client side both referer and origin headers are set properly. When the request runs from the server side it only sets the referer. Which is expected.

So, with the NODE_TLS_REJECT_UNAUTHORIZED=0 in the .env, devServer.host set to my app's URL, which is accepted by the backend, and both sanctum.origin and runtimeConfig.public.sanctum.origin set on the nuxt.config.ts the app works as expected 👍🏽

By chance I checked this issue and saw your comment about laravel.test.
I got curious once I realised the "trick" in there. So I renamed my container to reflect my backend's URL, manager.nicksaude.dev. Then I updated the Nuxt app to point to that "URL".
After that I reinstalled my dependencies running from inside the container that's dedicated to my Nuxt app. Then I ran the app from inside the Docker container and, Voilà, it works!!!

One tiny suggestion from my end is for you to set the Origin header as well. You see, Laravel Sanctum checks like this $domain = $request->headers->get('referer') ?: $request->headers->get('origin');. If by any chance that order changes in the future the module might stop working.

This was an amazing debug journey and I thank you immensely for your dedication and kindness. Have an awesome weekend!

manchenkoff commented 5 months ago

Holy moly! What a great investigation you did @maxacarvalho, very useful for later issues as well, thank you 😄 I'm glad it works for you now, I for sure will add origin header as well.

For a moment I thought today would be an amazing Friday!

I think eventually it is! 😄 Have a nice weekend!