manchenkoff / nuxt-auth-sanctum

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

How does one share cookies on localhost with different ports #170

Closed MarcoTroost closed 2 months ago

MarcoTroost commented 2 months ago

Hi!,

First of all, thank you for your great work on this module!

My Nuxt 3 frontend runs on localhost:3000. My Laravel 11 backend runs on localhost:8000 (by means of php artisan serve).

I cannot use the XSRF-TOKEN cookie in my frontend.

const handleVote = async (id: number) => {
  await $fetch(`${config.public.apiBase}/sanctum/csrf-cookie`, {
    credentials: 'include',
  })

  const xsrfToken = useCookie('XSRF-TOKEN').value

  console.log(xsrfToken)
});

"xsrfToken" is undefined, since the response i get is empty.

The set-cookie command seems to be executed though:

Scherm­afbeelding 2024-09-20 om 14 40 52

How can i get the XSRF-TOKEN cookie in localhost:3000 ?

kind regards,

Marco Troost

manchenkoff commented 2 months ago

Hey @MarcoTroost, could you please provide sanctum config from your nuxt.config.ts file?

Are you trying to get the token using the sanctum client or do you just want to get it manually in your code with plain $fetch?

I also noticed that in session domain you have localhost:8000 instead of regular localhost, did you specify that in the config on Laravel side (e.g. in .env file)?

MarcoTroost commented 2 months ago

Hi Artem,

Thank you for the quick reply. I want users to be able to vote on certain topics. I therefore need to be able to post from the frontend to the Laravel backend.

I do not need the token for login purposes.

Here are my configs:

.env (frontend)

NUXT_BASE_URL="http://localhost:3000"
NUXT_PUBLIC_API_BASE="http://localhost:8000/disco"

NUXT_SITE_ENV=preview

Nuxt.config.ts:

export default defineNuxtConfig({

  runtimeConfig: {
    public: {
      apiBase: process.env.NUXT_PUBLIC_API_BASE || 'https://api.trauminfo.de/disco',
    },
  },

  modules: [
    'nuxt-auth-sanctum'
  ],

   sanctum: {
     baseUrl: process.env.NUXT_PUBLIC_API_BASE || 'https://api.trauminfo.de/disco', // Laravel API
     endpoints: {
       csrf: {
        url: '/sanctum/csrf-cookie',
       },
       user: {
         url: '/user',
       }
     }
  }, 
});

.env (backend)

APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:SYlo/9LcvyqgoIEX7GxlJ2V3lYdFD/+V7Rnniy8k+eE=
APP_DEBUG=true
APP_URL=http://localhost:8000
FRONTEND_URL=http://localhost:3000

SESSION_DOMAIN=localhost:8000
SANCTUM_STATEFUL_DOMAINS=localhost:3000

...

Cors.php:

<?php
return [
    'paths' => ['api/*', 'disco/*', '/disco/sanctum/csrf-cookie'],
    'allowed_methods' => ['*'],
    'allowed_origins' => ['http://localhost:3000'],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['Content-Type', 'X-Requested-With', 'Authorization', 'X-XSRF-TOKEN','X-CSRF-TOKEN'],
    'exposed_headers' => [],
    'max_age' => 0,
    'supports_credentials' => true,
];
manchenkoff commented 2 months ago

@MarcoTroost while you work locally, you should not put SESSION_DOMAIN=localhost:8000 in your env config since it is not a domain but the full address, it should be either localhost or empty (better be empty to not interfere with some cases like 127.0.0.1), so Laravel will use localhost by default for cookies. I also mentioned that in the module docs, you can check the use case for SPA Cookie.

Regarding requests against your API, there is a composable provided by the module plugin that you can use instead of plain $fetch to call Laravel with any method including POST, please check the docs - useSanctumClient

MarcoTroost commented 2 months ago

@manchenkoff Changing SESSION_DOMAIN=localhost (without the port) did the trick!

Perhaps it's an idea to include your explanation of explicitly not including the port in the official documentation? I think it could prove beneficial for the understanding of the module.

That composable looks just awesome. I will certainly try this!

kind regards, Marco

manchenkoff commented 2 months ago

@manchenkoff Changing SESSION_DOMAIN=localhost (without the port) did the trick!

I'm glad it helped!

Perhaps it's an idea to include your explanation of explicitly not including the port in the official documentation?

I believe it was mentioned in the Laravel docs, but I will definitely add this note to my docs as well 👍

manchenkoff commented 2 months ago

Page has been updated - SPA Cookie

MarcoTroost commented 2 months ago

Super! Thank you for your time & help.

If it is not a bother, i do have one question: Since voting in my website doesn't need a user to be logged in, do i really need a user? I only need the XSRF-TOKEN.

When vite starts up, the nuxt-auth-sanctum module looks for a user, which it can't find (i have no routes for in place for users nor login)

Scherm­afbeelding 2024-09-21 om 10 07 37

Is it possible to opt out of this?

p.s. Making API calls works just fine

greetz, Marco

manchenkoff commented 2 months ago

Is it possible to opt out of this?

Yes, since 0.4.12 version you can disable user request by setting sanctum.client.initialRequest to false in the config.

MarcoTroost commented 2 months ago

Great, works perfectly! Thanks a bunch!

MarcoTroost commented 2 months ago

Hi @manchenkoff

What worked on localhost, unfortunately doesn't work on my (apache) production server. The xsrf cookie is set, but the actual api call doesn't work.

Scherm­afbeelding 2024-09-25 om 10 20 39

Vue component:

const config = useRuntimeConfig()

const handleVote = async (id: number) => {
  // const client = useSanctumClient()

  // const response = await client(`${config.public.apiBase}/emotion`, {
  //   method: 'PUT',
  //   body: JSON.stringify({
  //     emotie_id: id,
  //     droom_id: props.dreamId,
  //   }),
  //   credentials: 'include',
  // })

  // choice.value = id

  // if (response) {
  //   result.value = response.value
  // }

  await $fetch(`${config.public.apiBase}/sanctum/csrf-cookie`, {
    credentials: 'include',
  })

  const xsrfToken = useCookie('XSRF-TOKEN').value

  if (xsrfToken) {
    const response = await $fetch(`${config.public.apiBase}/emotion`, {
      method: 'POST',
      headers: {
        Referer: config.public.baseUrl,
        Accept: 'application/json',
        'X-XSRF-TOKEN': xsrfToken,
      },
      credentials: 'include',
      body: JSON.stringify({
        emotie_id: id,
        droom_id: props.dreamId,
      }),
    })

    choice.value = id

    if (response) {
      result.value = response
    }
  }
}

env (backend):

APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:SYlo/9LcvyqgoIEX7GxlJ2V3lYdFD/+V7Rnniy8k+eE=
APP_DEBUG=true
APP_URL=https://api.trauminfo.de
FRONTEND_URL=https://trauminfo.de

SESSION_DOMAIN=.trauminfo.de
SANCTUM_STATEFULL_DOMAINS=trauminfo.de

...

config/cors.php:


<?php

return [
    'paths' => ['api/*', 'disco/*', 'disco/emotion', '/disco/sanctum/csrf-cookie'],
    'allowed_methods' => ['*'],
    'allowed_origins' => ['https://www.trauminfo.de', 'https://trauminfo.de'],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['Content-Type', 'X-Requested-With', 'Authorization', 'X-XSRF-TOKEN','X-CSRF-TOKEN'],
    'exposed_headers' => [],
    'max_age' => 0,
    'supports_credentials' => true,
];

Nuxt.config.ts:

export default defineNuxtConfig({

  runtimeConfig: {
    public: {
      baseUrl: 'https://trauminfo.de',
      apiBase: 'https://api.trauminfo.de/disco',
    },
  },

  modules: [
    'nuxt-auth-sanctum'
  ],

   sanctum: {
     baseUrl: 'https://api.trauminfo.de/disco', // Laravel API
     endpoints: {
       csrf: {
        url: '/sanctum/csrf-cookie',
       },
     }
  }, 
});

It's driving me crazy. Do you know what i'm doing wrong?

kind regards,

Marco

manchenkoff commented 2 months ago

Hi @MarcoTroost, I highly recommend using client from the module (that handles all the things related to cookies and headers), try it with the following configurations:

nuxt.config.ts

export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      baseUrl: 'https://trauminfo.de',
      apiBase: 'https://api.trauminfo.de/disco',
    },
  },
  modules: ['nuxt-auth-sanctum'],
  sanctum: {
    baseUrl: 'https://api.trauminfo.de/disco',
    endpoints: {
      csrf: '/sanctum/csrf-cookie', // is not necessary since the default value is the same, but you had incorrect nestedness
    },
  },
})

.env

APP_NAME=Laravel
APP_ENV=production
APP_KEY=base64:SYlo/9LcvyqgoIEX7GxlJ2V3lYdFD/+V7Rnniy8k+eE=
APP_DEBUG=true
APP_URL=https://api.trauminfo.de
FRONTEND_URL=https://trauminfo.de

SESSION_DOMAIN=.trauminfo.de
SANCTUM_STATEFULL_DOMAINS=trauminfo.de
# ...

config/cors.php

<?php

return [
    'paths' => ['*'],
    'allowed_methods' => ['*'],
    'allowed_origins' => [env('FRONTEND_URL'), 'https://www.trauminfo.de'],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['*'],
    'exposed_headers' => [],
    'max_age' => 0,
    'supports_credentials' => true,
];

Vue Component

<script setup lang="ts">
const client = useSanctumClient()
const choice = ref(0)
const result = ref(0)

const props = defineProps<{
  dreamId: number
}>()

const handleVote = async (id: number) => {
  const response = await client('emotion', {
    method: 'PUT',
    body: JSON.stringify({
      emotie_id: id,
      droom_id: props.dreamId,
    }),
  })

  choice.value = id

  if (response) {
    result.value = response.value
  }
}
</script>

<template>
  <button @click="handleVote(1)">
    Click me!
  </button>
</template>

<style scoped>
</style>

You can also find other example in the template applications:

MarcoTroost commented 1 month ago

Hi @manchenkoff

Thank you for your feedback, But unfortunately, the api call doesn't work. I really haven't got a clue.

All works fine on localhost, but not in production @ https://trauminfo.de/droomwoordenboek/krokodil

Whenever i click on "Bang" (= Scared in dutch):

Scherm­afbeelding 2024-09-27 om 14 11 30

I get a Cors error:

Scherm­afbeelding 2024-09-27 om 14 13 38

My website runs on an apache server.

manchenkoff commented 1 month ago

Hi @MarcoTroost

This issue indeed is related to CORS settings on Laravel side: Access to fetch at 'https://api.trauminfo.de/api/emotion' from origin 'https://trauminfo.de' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled..

Please make sure that your config is properly updated, cache was reset and fpm restarted. Also, check that correct middleware covers your endpoint.

If nothing helps, I would recommend debugging it on your production server to see what are the headers/details available in the Laravel request/response.

MarcoTroost commented 1 month ago

Good afternoon @manchenkoff

I've removed cors.php on production altogether & placed all the necessary headers in apache. This now works in production:

RewriteEngine On                  

RewriteCond %{REQUEST_METHOD} OPTIONS 
RewriteRule ^(.*)$ $1 [R=200,L]  

# Set environment variable for allowed origins
SetEnvIf Origin "^https?://(www\.)?trauminfo\.de$" ORIGIN_ALLOWED=$0

Header always set Access-Control-Allow-Origin "%{ORIGIN_ALLOWED}e" env=ORIGIN_ALLOWED              
Header always set Access-Control-Allow-Methods "POST, GET, OPTIONS"
Header always set Access-Control-Allow-Headers "X-Requested-With, Content-Type, Authorization, X-CSRF-TOKEN, X-XSRF-TOKEN"
Header always set Access-Control-Allow-Credentials true

I would still rather have it to work using Laravel's default Cors.php.

In order to test this thoroughly, i've changed the top level domain on my computer from localhost to trauminfo.disco, using Laravel Valet.

frontend: trauminfo.disco:3000 backend: api.trauminfo.disco

Using these settings, the module does not work.

Altered configs:

Nuxt.config.ts

export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      baseUrl: 'http://trauminfo.disco',
      apiBase: 'http://api.trauminfo.disco/api',
    },
  },
  modules: ['nuxt-auth-sanctum'],
  sanctum: {
    baseUrl: 'http://api.trauminfo.de/disco',
    endpoints: {
      csrf: '/sanctum/csrf-cookie',
    },
  },
})

.env

APP_NAME=Laravel
APP_ENV=production
APP_KEY=base64:SYlo/9LcvyqgoIEX7GxlJ2V3lYdFD/+V7Rnniy8k+eE=
APP_DEBUG=true
APP_URL=https://api.trauminfo.de
FRONTEND_URL=https://trauminfo.de

SESSION_DOMAIN=.trauminfo.disco
SANCTUM_STATEFULL_DOMAINS=trauminfo.disco
# ...

Cors.php

return [
    'paths' => ['api/*', '/api/sanctum/csrf-cookie'],
    'allowed_methods' => ['*'],
    'allowed_origins' => ['http://trauminfo.disco:3000'],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['Content-Type', 'X-Requested-With', 'Authorization', 'X-XSRF-TOKEN','X-CSRF-TOKEN'],
    'exposed_headers' => [],
    'max_age' => 0,
    'supports_credentials' => true,
];

Cookie seems to be set, but i can't read it:

Scherm­afbeelding 2024-10-02 om 16 54 08

Do you know what's wrong when using different top level domains instead of localhost?

kind regards,

Marco

manchenkoff commented 1 month ago

Hey @MarcoTroost, using a custom domain on a local machine might be tricky. I always recommend using localhost for development and a real domain in production. I have never used Laravel Valet, only plain artisan serve or docker-based sail.

I can try to help to point out the possible issue, but I would need you to upgrade to the latest version of the module 0.4.18, change sanctum.logLevel to 5 in your nuxt.config.ts and then to attach logs output here in the comments (from both SSR console and CSR browser console).

P.S.

I've removed cors.php on production altogether & placed all the necessary headers in apache.

This is not how you are supposed to configure your server 😄 for example, if you need to migrate to nginx (which Valet is based on afaik), you will face the same problem again. You should double-check your config to set it up properly.

manchenkoff commented 1 month ago

Just for reference, here are some examples that I use in production in one of my projects

config/cors.php

<?php

declare(strict_types=1);

return [
    'paths' => ['*'],
    'allowed_methods' => ['*'],
    'allowed_origins' => [
        env('FRONTEND_URL', 'http://manchenkoff.me'), // http://localhost:3000 on local machine
        env('BACKOFFICE_URL', 'http://admin.manchenkoff.me'), // http://localhost:3001 on local machine
    ],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['*'],
    'exposed_headers' => [],
    'max_age' => 0,
    'supports_credentials' => true,
];

config/sanctum.php

<?php

declare(strict_types=1);

use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;

return [
    'stateful' => explode(
        ',',
        env('SANCTUM_STATEFUL_DOMAINS', 'manchenkoff.me,admin.manchenkoff.me,api.manchenkoff.me') // localhost:3000,localhost:3001 on local machine
    ),
    'guard' => ['web'],
    'expiration' => null,
    'middleware' => [
        'verify_csrf_token' => VerifyCsrfToken::class,
        'encrypt_cookies' => EncryptCookies::class,
    ],
];

.env

# ...
APP_URL=http://api.manchenkoff.me
FRONTEND_URL=http://manchenkoff.me
BACKOFFICE_URL=http://admin.manchenkoff.me
SESSION_DOMAIN=.manchenkoff.me
# ...

and this is what I use in the Nuxt instance

nuxt.config.ts

export default defineNuxtConfig({
    modules: [
        'nuxt-auth-sanctum',
    ],

    sanctum: {
        baseUrl: 'https://api.manchenkoff.me',
    },
});
MarcoTroost commented 1 month ago

Good morning @manchenkoff

Thanks for your elaborate reply. You're absolutely right, a server shouldn't be configured this way.

Point is that i got so frustrated, that i chose to alter the apache config just to get going.

I DO want to use middleware. That's why i've set up my local top level domain to trauminfo.disco & api.trauminfo.disco using valet to find where the problem lies.

On localhost; something, somewhere manipulates the Access-Control-Allow-Origin header, adding another * resulting in multiple origins:

Scherm­afbeelding 2024-10-03 om 07 16 58

I haven't got a clue what proces is responsible for this.

I will investigate further this weekend, using tha latest version & increasing the log-level.

regards, Marco

manchenkoff commented 1 month ago

adding another * resulting in multiple origins

Yes, it can be either multiple domains or * to allow everything

I will investigate further this weekend, using tha latest version & increasing the log-level.

Sure thing, I will try to help once more details are shared!