¿Cómo guarda Laravel la cookie de sesión y csrf?
Tags: laravel, php
Published: 2024-05-05 06:17:22

Hoy tuve un problema con una SPA que se comunica con un backend en Laravel. Son contenedores de docker, por lo que en teoría deberían funcionar igual, pero en otra computadora, la comunicación con el backend fallaba:

`CSRF token mismatch`

Después de revisar todas las configuraciones de la sesión, de sanctum, de las cookies, no daba con la razón por la que Laravel no reconocía el token. Así que para debuggear, me puse a leer el código de Laravel para entender cómo se genera la sesión, su cookie y el token de csrf.

Por cierto, estoy usando el driver database para guardar la sesión.

## La sesión

![Tabla sessions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/96b44iib3okndjtsl82v.png)

Si usas los ajustes por default:

- La sesión se guarda en la tabla `sessions`
- El ID son 40 caracteres aleatorios creados con la función `Str:random`
- El payload es una cadena de caracteres codificada en base64

### Ejemplo

El payload de la base de datos:

```
YTozOntzOjY6Il90b2tlbiI7czo0MDoiamtYNUVmUXZTNVc4bnFtVGt3RWk4d3g5QmM0RFZ3aTlDTmdRWlJOSCI7czo5OiJfcHJldmlvdXMiO2E6MTp7czozOiJ1cmwiO3M6MzA6Imh0dHA6Ly9sb2NhbGhvc3Q6ODg4OC9hcGkvY3NyZiI7fXM6NjoiX2ZsYXNoIjthOjI6e3M6Mzoib2xkIjthOjA6e31zOjM6Im5ldyI7YTowOnt9fX0=
```

Decodificado da:

```
a:3:{s:6:"_token";s:40:"jkX5EfQvS5W8nqmTkwEi8wx9Bc4DVwi9CNgQZRNH";s:9:"_previous";a:1:{s:3:"url";s:30:"http://localhost:8888/api/csrf";}s:6:"_flash";a:2:{s:3:"old";a:0:{}s:3:"new";a:0:{}}}"
```

Esto es el resultado de serializar con php el array de atributos de la sesión.

- `jkX5EfQvS5W8nqmTkwEi8wx9Bc4DVwi9CNgQZRNH` es el token csrf actual.

## La cookie de sesión

 Al llamar al endpoint de sanctum/csrf obtenemos dos cookies: `laravel_session` y `XSRF-TOKEN`

El contenido de estas cookies está encriptado con el algoritmo `AES-256-CBC`. La llave es el contenido de la variable `APP_KEY`. (A menos que se hayan cambiado los ajustes por defecto)

Desencriptamos el contenido de la cookie de sesión con la función `Crypt::decryptString`.

```
Crypt::decryptString("eyJpdiI6Ims3SkhEdWFxWGV0WER5Tm91dG5yK0E9PSIsInZhbHVlIjoiOER5ME8xMVhMNmlBcmkwWDJieDRtZmFRT2RGT21yWFpjYm94YXV4S21KdDZvaG9xaUF2cHk5WXNadzR0RS85b1BpbFlxWXB0blB0eUczYjhDQUhkazhYM0dRRjdEQnI4TlZIak50TDFhMGlmSy9URlpkdW5zSmZRSmd3YzlscTEiLCJtYWMiOiJmZjI5MzBiMTVmYzE3MjdhOTk3ZGM3MzZhYjdlN2IxZWY5MmVlMDljMDNiYzgzZGVmNWRjZmIzMjgxMjM0ZDBmIiwidGFnIjoiIn0")
```

Nos da:

`204dfbcb7897b8904470ad24455c430fbee52e49|Tx8V3BmKRGrLQDONQAol5H5QsSRXYpzYvVXhyAQw`

No logré identificar que significan los primeros 40 caracterés. Los últimos 40, despues del `|`, son el ID de la sesión (`Tx8V3BmKRGrLQDONQAol5H5QsSRXYpzYvVXhyAQw`), coincide con el ID en la base de datos.

Y si desencriptamos la cookie con el token csrf:

```
Crypt::decryptString("eyJpdiI6InEvVHVzWWpGcndLM1k1WVg4aFlvL3c9PSIsInZhbHVlIjoiUmcrOS9vNGJFZXgyUUFMOGlyTFYxU08rYUVXNmxReDI1Vm5rK1BVNkxvUVNpZXptby9xVjcyZnBFZVl1VGRTTFFiLzgwUGhwdDFKRVV2N00xQTJZa0ZLWnVoVVhtN1lvdkduamFZZU5Oc3dua1RmRk5JdEZHZnRQaHMzZGduZ0siLCJtYWMiOiJkNTFkMTMxZjUyMWU4MmYwNDczOGQyZDQ5ODJlNmU0NzBmYTA0YTg0MjM1M2E0MzQyYjY1Mzc5MGY1YjJiNWFhIiwidGFnIjoiIn0")
```

Da:

`c27a7fa10ed3b8fdd1c10265852343fa8e088ef8|jkX5EfQvS5W8nqmTkwEi8wx9Bc4DVwi9CNgQZRNH`

Al igual que con la cookie de sesión, no pude identificar que son los primeros 40 caracteres, parece un hash de algo. Los ultimos 40 caracteres son el token csrf.

## CSRF

El token encriptado se debe enviar en todas las peticiones tipo POST/PUT/DELETE. Si es un request AJAX, debe ir en un header.

El middleware `VerifyCSRFToken` obtiene el token del request y lo compara con el token que se encuentra en la base de datos. Si no coinciden, ocurre el error `CSRF token mismatch` y la petición no se continua procesando.

En mi caso, los tokens en las cookies y la base de datos coincidían. Así como los sitios permitidos para CORS. Las peticiones de preflight también regresaban los valores correctos.

**¿Que pasó?**

Olvidé que había actualizado las dependencias del `package.json` de la SPA. Uno de los paquetes que se actualizó fue `axios`.

Axios automáticamente agrega el token de la cookie en un header, pero en la última versión ya no lo hace.

Hay que agregar `withXSRFToken = true` a la configuración de la instancia de axios. 

```
axios.create({
  withCredentials: true,
  withXSRFToken: true,
});
```

Fin.

Si alguien sabe que son o como se generan los primeros 40 caracteres del payload de la cookie, agréguelo en los comentarios.