lunes, 16 de junio de 2025

Lravel Sanctum: cookie de sesión para orígenes con distinto puerto

En ambiente de desarrollo tengo frontend y backend expuestos en localhost, pero el primero en el puerto 3000 y el segundo en el puerto 8000. ¿Cómo hago para usar autenticación mediante cookies de sesión? 


Usar la autenticación de cookies de sesión de Laravel Sanctum con un frontend en localhost:3000 y un backend en localhost:8000 es un escenario muy común en desarrollo. Aunque ambos están en localhost, el hecho de que usen puertos diferentes los convierte en orígenes diferentes desde la perspectiva del navegador (por la Política del Mismo Origen).

Para que esto funcione, necesitas configurar correctamente tanto el backend (Laravel) como el frontend.


I. Configuración en el Backend (Laravel - localhost:8000)

Asumimos que ya tienes Laravel Sanctum instalado y configurado lo básico (migraciones ejecutadas, HasApiTokens trait en el modelo User).

1. Configurar CORS (Cross-Origin Resource Sharing)

Este es el paso más crítico para permitir que localhost:3000 hable con localhost:8000 y que las credenciales (cookies) se envíen.

Abre el archivo de configuración config/cors.php y ajústalo así:

PHP
<?php

return [
    /*
     * Rutas a las que se aplicarán las reglas CORS.
     * Generalmente, las rutas de tu API y el endpoint de CSRF de Sanctum.
     */
    'paths' => ['api/*', 'sanctum/csrf-cookie'],

    /*
     * Orígenes permitidos para realizar solicitudes de origen cruzado.
     * DEBE ser el origen exacto de tu frontend. ¡NO uses '*' si supports_credentials es true!
     */
    'allowed_origins' => ['http://localhost:3000'],

    /*
     * Encabezados permitidos. '*' es común en desarrollo.
     */
    'allowed_headers' => ['*'],

    /*
     * Métodos HTTP permitidos. '*' es común en desarrollo.
     */
    'allowed_methods' => ['*'],

    /*
     * Si las solicitudes CORS pueden incluir credenciales (cookies, encabezados de autorización).
     * ¡ESTO DEBE SER TRUE para la autenticación basada en cookies de Sanctum!
     */
    'supports_credentials' => true,

    /*
     * Tiempo en segundos que la respuesta preflight puede ser cacheada.
     */
    'max_age' => 3600,
];

2. Configurar los Dominios "Stateful" de Sanctum

Sanctum necesita saber qué dominios tratar como "first-party" para la autenticación basada en cookies.

Abre el archivo de configuración config/sanctum.php y modifica la clave stateful. Lo más fácil es hacerlo a través de tu archivo .env:

  • En config/sanctum.php:

    PHP
    // ...
    'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1')),
    // ...
    

    (Normalmente, localhost:3000 ya está incluido en el valor predeterminado si es una instalación reciente de Laravel/Sanctum. Si no, asegúrate de añadirlo).

  • En tu archivo .env: (No es estrictamente necesario si localhost:3000 ya está en el valor por defecto, pero si lo modificaste o tienes problemas, puedes forzarlo aquí):

    Code snippet
    SANCTUM_STATEFUL_DOMAINS=localhost:3000,localhost:8000,127.0.0.1:8000
    

    Añade localhost:3000 explícitamente para asegurar que Sanctum lo reconozca como un dominio "first-party" para cookies.

3. Configurar el Dominio de la Cookie de Sesión

Para localhost con puertos diferentes, la configuración por defecto de Laravel suele funcionar. El dominio de la cookie de sesión debe ser null o no establecido, lo que significa que la cookie se establecerá para el host actual (localhost). El navegador, combinado con la configuración supports_credentials = true de CORS, manejará correctamente el envío y recepción de la cookie entre los puertos.

Abre config/session.php y asegúrate de que la clave 'domain' esté configurada a null (o que SESSION_DOMAIN en tu .env esté vacío/no definido):

PHP
// config/session.php
'domain' => env('SESSION_DOMAIN'), // Por defecto, `SESSION_DOMAIN` en .env está vacío, lo que resulta en null
  • En tu archivo .env:
    Code snippet
    SESSION_DOMAIN=
    
    No le pongas un punto (.) aquí para localhost (.localhost). A veces puede causar problemas con algunos navegadores para el desarrollo local con puertos diferentes.

4. Rutas de Autenticación

Asegúrate de tener tus rutas de login y logout en routes/api.php. El login será una ruta POST que crea la sesión y envía una cookie (implícitamente si todo está configurado).

PHP
// routes/api.php

use App\Http\Controllers\Auth\LoginController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

// Ruta para obtener el token CSRF (¡importante!)
Route::get('/sanctum/csrf-cookie', function () {
    return response('')->withCookie(csrf_cookie());
});

Route::post('/login', [LoginController::class, 'login']);
Route::post('/logout', [LoginController::class, 'logout'])->middleware('auth:sanctum');

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

5. Reiniciar Servicios

Después de hacer estos cambios, detén y reinicia tu servidor Laravel (php artisan serve) y también tu servidor de desarrollo frontend (si lo tenías corriendo).


II. Configuración en el Frontend (SPA - localhost:3000)

Tu aplicación frontend (React, Vue, Angular) debe manejar el flujo de cookies/CSRF correctamente. La mayoría de los frameworks de frontend tienen herramientas o librerías HTTP que facilitan esto (como Axios).

1. Realizar la Solicitud CSRF al inicio

Antes de cualquier intento de login o cualquier otra solicitud autenticada (especialmente las no-GET), tu SPA debe hacer una solicitud a /sanctum/csrf-cookie.

  • Usando Axios (Recomendado):

    Axios es popular y tiene un excelente soporte para esta funcionalidad de Sanctum.

    JavaScript
    import axios from 'axios';
    
    // Configura la base URL de tu API
    axios.defaults.baseURL = 'http://localhost:8000/api'; // O http://localhost:8000 si tus rutas no están en /api
    axios.defaults.withCredentials = true; // ¡Esto es CRÍTICO! Le dice al navegador que envíe cookies cross-origin.
    
    // Interceptor para enviar el token CSRF en cada solicitud
    // (Axios 0.27+ lo hace automáticamente si la cookie XSRF-TOKEN está presente)
    // Para versiones antiguas de Axios o si quieres ser explícito:
    // axios.interceptors.request.use(request => {
    //     const csrfToken = document.cookie.split('; ').find(row => row.startsWith('XSRF-TOKEN='));
    //     if (csrfToken) {
    //         request.headers['X-XSRF-TOKEN'] = csrfToken.split('=')[1];
    //     }
    //     return request;
    // });
    
    // Tu componente/archivo principal (ej. App.js en React, main.js en Vue)
    // Antes de renderizar la app o antes de cualquier solicitud de login
    async function initializeApp() {
        try {
            // Obtener el token CSRF. Esto establecerá la cookie XSRF-TOKEN en el navegador.
            // Axios con withCredentials=true enviará automáticamente esta cookie en POST/PUT/DELETE
            // y la leerá para ponerla en el encabezado X-XSRF-TOKEN si la cookie está presente.
            await axios.get('http://localhost:8000/sanctum/csrf-cookie');
            console.log('CSRF cookie obtained!');
            // Ahora puedes iniciar tu aplicación y permitir el login
        } catch (error) {
            console.error('Failed to obtain CSRF cookie:', error);
            // Manejar el error (ej. mostrar un mensaje al usuario)
        }
    }
    
    initializeApp();
    
  • Usando Fetch API:

    Con Fetch, necesitas manejar withCredentials y el encabezado X-XSRF-TOKEN más explícitamente:

    JavaScript
    async function fetchCsrfToken() {
        try {
            await fetch('http://localhost:8000/sanctum/csrf-cookie', {
                method: 'GET',
                credentials: 'include' // ¡CRÍTICO! Permite enviar/recibir cookies cross-origin
            });
            console.log('CSRF cookie obtained!');
            // Ahora puedes proceder con el login
        } catch (error) {
            console.error('Failed to obtain CSRF cookie:', error);
        }
    }
    
    async function loginUser(email, password) {
        // Después de fetchCsrfToken()
        const csrfToken = document.cookie.split('; ').find(row => row.startsWith('XSRF-TOKEN='))?.split('=')[1];
    
        const response = await fetch('http://localhost:8000/api/login', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json',
                'X-XSRF-TOKEN': csrfToken // Envía el token CSRF en el encabezado
            },
            credentials: 'include', // ¡CRÍTICO! Envía la cookie de sesión y el token CSRF
            body: JSON.stringify({ email, password, device_name: 'browser' })
        });
        const data = await response.json();
        console.log(data);
    }
    
    fetchCsrfToken().then(() => {
        // Luego puedes llamar a loginUser o cualquier otra función autenticada
        // loginUser('test@example.com', 'password');
    });
    

Resumen de los puntos clave para localhost:3000 y localhost:8000:

  1. Backend (config/cors.php): allowed_origins debe ser ['http://localhost:3000'] y supports_credentials debe ser true.
  2. Backend (config/sanctum.php o .env): Asegúrate de que localhost:3000 esté en los SANCTUM_STATEFUL_DOMAINS.
  3. Backend (config/session.php o .env): El SESSION_DOMAIN debe ser null (vacío). No uses .localhost.
  4. Frontend: Todas las solicitudes a la API deben incluir credentials: 'include' (Axios withCredentials = true).
  5. Frontend: Haz una solicitud GET a http://localhost:8000/sanctum/csrf-cookie al inicio de tu aplicación para obtener la cookie XSRF-TOKEN. Axios (con withCredentials=true) se encargará de enviar esta cookie en el encabezado X-XSRF-TOKEN para futuras solicitudes.

Con esta configuración, tu SPA en localhost:3000 podrá autenticarse usando las cookies de sesión de Laravel servidas por localhost:8000, proporcionando una experiencia de desarrollo fluida.

No hay comentarios: