Environment
- Sanctum Version: v2.15.1
- Fortify Version: v1.13.0
- Laravel Version: v9.17.0
- PHP Version: PHP 8.1.6
- Database Driver & Version: Based on Laravel Sail (docker): image: ‘mysql:8.0’
Description:
I am trying to get the login flow of my web app working. The web app is written using Nuxt 3 for the frontend (SPA) running on http://localhost:3000 and Laravel as the backend running on http://localhost.
Because Nuxt 3 is using the fetch API and axios is currently not available for Nuxt 3 I am trying to get the login flow with Laravel Sanctum and the fetch API to work.
Steps To Reproduce:
I am calling the Laravel Backend using a composable function on the frontend Nuxt App like this:
export const useLogin = async (email, password) => {
const config = useRuntimeConfig()
const tokenResponse = await $fetch(config.baseURL + '/sanctum/csrf-cookie', {
method: 'GET',
credentials: 'include'
})
const token = getCookie('XSRF-TOKEN')
console.log(token)
const loginResponse = await $fetch(config.baseURL + '/login', {
method: 'POST',
headers: {
'X-XSRF-TOKEN': token,
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: {
"username": email,
"password": password
}
})
}
Then I call it in a LoginForm-Component:
<template>
<form @submit.prevent="login()" class="bg-blue-500 flex flex-col rounded-xl p-4 text-white">
<h1 class="text-center mb-4">Login</h1>
<label for="fname">Email</label>
<input v-model="form.email" class="rounded-md h-8 mb-4 text-black" type="text" id="email" name="email"><br><br>
<label for="password">Password</label>
<input v-model="form.password" class="rounded-md h-8 mb-4 text-black" type="password" id="password" name="password"><br><br>
<button class="rounded-md bg-orange-700 cursor-pointer" type="submit">Login</button>
</form>
</template>
<script setup>
const form = {
email: '',
password: ''
}
function login() {
useLogin(form.email, form.password)
}
</script>
The problem is that I get 419 errors (csrf mismatch) back from the login route although the X-XSRF-Token is set in the request headers (like described in the Laravel Sanctum docs):
Is it possible that Laravel Sanctum handles the Header name case-sensitive and so it can’t find the token? As far as I know the fetch API always sends header names in lowercase and there is no way to change this behaviour.
Additional information
The cookie is set correctly by the Laravel Backend:
The cookie data seems to be OK, too:
These are the Sanctum values inside my .env
:
SANCTUM_STATEFUL_DOMAINS='localhost,localhost:3000,127.0.0.1,127.0.0.1:3000,127.0.0.1:8000,::1'
SESSION_DOMAIN=.localhost
Already tried
I also tried setting the correct timezone in config/app.php
and I also changed the timezone inside the docker container (laravel sail) but the cookies still have the same data values and expiration times so that seems to be OK.
The same behaviour happens to me if I try to register a new user in a brand new installed Laravel 9 Project (which is configured as above) using Insomnia as REST Client:
The request/response timeline:
* Preparing request to http://localhost/register
* Current time is 2022-06-09T10:42:49.472Z
* Using default HTTP version
* Disable timeout
* Enable automatic URL encoding
* Enable SSL validation
* Enable cookie sending with jar of 2 cookies
* Hostname in DNS cache was stale, zapped
* Trying 127.0.0.1:80...
* Connected to localhost (127.0.0.1) port 80 (#21)
> POST /register HTTP/1.1
> Host: localhost
> User-Agent: insomnia/2022.3.0
> Cookie: laravel_session=eyJpdiI6IkpGdXZWRmh1WHdlc1F2VEpLVzllSFE9PSIsInZhbHVlIjoibk5JV0hmZHIzd1RpazRUOTJOOVBJZm1KNFZGUGlyaVJXTEk3NFl4YWVwSzRDNU5QQXh2Q1BrelNIWW55VWpEUTlXZmZaUjZPQUkrdjRDcnlCZUtkOENMMEhxOHVVWXhxcmFIdjdzRWh4dVgrLy9zRVhmOURFbFduR1hCcFIwcy8iLCJtYWMiOiJmZmZiZmI0MTk5OGNhZmRmMjFkZjgxMDk3MjgwMDBmYWFiNDA4YTZiYjQ1MjFkNjg0Mjk5MDRlMGEyNTU0NTUxIiwidGFnIjoiIn0%3D; XSRF-TOKEN=eyJpdiI6IkhtVjZrTWV2WC9KZFV0NWN4QUo2UVE9PSIsInZhbHVlIjoiQWJqQ2FuVHM1eENWZ212ZUZDNFZJVEErZk5ueGRUTnY3RFl3N2pJbEM4WEFTTE9UVFVWSXR1VkwvbkhhK1lqUkRySUJsWEtUT3RUWHI3UlcvTHpXSkNlNkNVZ3R2RXhkTVpWdWx2VmpZbDhPeWdTenVqVE5BWW1Rc1Qyb0t3L00iLCJtYWMiOiIwNTU2ZjgyMzdhNWU0Mjk1MzkyM2ZkN2Q4NWY2N2Y1MzAzYThlY2YxNmU2MzQyNTYxM2I3YjI2YjkyZjhiODZiIiwidGFnIjoiIn0%3D
> Content-Type: application/json
> Accept: application/json
> X-XSRF-TOKEN: eyJpdiI6IkhtVjZrTWV2WC9KZFV0NWN4QUo2UVE9PSIsInZhbHVlIjoiQWJqQ2FuVHM1eENWZ212ZUZDNFZJVEErZk5ueGRUTnY3RFl3N2pJbEM4WEFTTE9UVFVWSXR1VkwvbkhhK1lqUkRySUJsWEtUT3RUWHI3UlcvTHpXSkNlNkNVZ3R2RXhkTVpWdWx2VmpZbDhPeWdTenVqVE5BWW1Rc1Qyb0t3L00iLCJtYWMiOiIwNTU2ZjgyMzdhNWU0Mjk1MzkyM2ZkN2Q4NWY2N2Y1MzAzYThlY2YxNmU2MzQyNTYxM2I3YjI2YjkyZjhiODZiIiwidGFnIjoiIn0%3D
> Content-Length: 110
| {
| "name": "test",
| "email": "[email protected]",
| "password": "test1234",
| "password_confirmation": "test1234"
| }
* Mark bundle as not supporting multiuse
< HTTP/1.1 419 unknown status
< Host: localhost
< Date: Thu, 09 Jun 2022 10:42:49 GMT
< Connection: close
< X-Powered-By: PHP/8.1.6
< Cache-Control: no-cache, private
< Date: Thu, 09 Jun 2022 10:42:49 GMT
< Content-Type: application/json
< Content-Length: 11025
* Replaced cookie laravel_session="eyJpdiI6IlhpSzV3SnJLOExyeXU0NGtaM0piZEE9PSIsInZhbHVlIjoiMkhsekNJYy80OTJHb3o2OWppZ2pSMDg2aHZuNTkzc1pYMXh2VDdHSW9XaUVPSlRkSkphZ013cnNEMW1CY1I1Zy9zNGpHTGtra0tEY1BQWGdhUlFUbWNzM3FJOVNqbTlNSEhxSXFSck1oWUlvbURvRFlYbW1oLytBbGJIUW1wYksiLCJtYWMiOiI1MGZjOGNhNTNmOTcxNTg1MjhkY2FmZjcwMWYwODBlZGE1NzYwZjU2MGJiNzRlZDk0NTU3YzBmZmUxZTVjYmNiIiwidGFnIjoiIn0%3D" for domain localhost, path /, expire 1654778569
< Set-Cookie: laravel_session=eyJpdiI6IlhpSzV3SnJLOExyeXU0NGtaM0piZEE9PSIsInZhbHVlIjoiMkhsekNJYy80OTJHb3o2OWppZ2pSMDg2aHZuNTkzc1pYMXh2VDdHSW9XaUVPSlRkSkphZ013cnNEMW1CY1I1Zy9zNGpHTGtra0tEY1BQWGdhUlFUbWNzM3FJOVNqbTlNSEhxSXFSck1oWUlvbURvRFlYbW1oLytBbGJIUW1wYksiLCJtYWMiOiI1MGZjOGNhNTNmOTcxNTg1MjhkY2FmZjcwMWYwODBlZGE1NzYwZjU2MGJiNzRlZDk0NTU3YzBmZmUxZTVjYmNiIiwidGFnIjoiIn0%3D; expires=Thu, 09 Jun 2022 12:42:49 GMT; Max-Age=7200; path=/; domain=.localhost; httponly; samesite=lax
* Received 10.8 KB chunk
* Closing connection 21
* Saved 1 cookie
The error message:
{
"message": "CSRF token mismatch.",
"exception": "Symfony\Component\HttpKernel\Exception\HttpException",
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php",
"line": 379,
"trace": [
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php",
"line": 353,
"function": "prepareException",
"class": "Illuminate\Foundation\Exceptions\Handler",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php",
"line": 51,
"function": "render",
"class": "Illuminate\Foundation\Exceptions\Handler",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 185,
"function": "handleException",
"class": "Illuminate\Routing\Pipeline",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/View/Middleware/ShareErrorsFromSession.php",
"line": 49,
"function": "Illuminate\Pipeline\{closure}",
"class": "Illuminate\Pipeline\Pipeline",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 180,
"function": "handle",
"class": "Illuminate\View\Middleware\ShareErrorsFromSession",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php",
"line": 121,
"function": "Illuminate\Pipeline\{closure}",
"class": "Illuminate\Pipeline\Pipeline",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php",
"line": 64,
"function": "handleStatefulRequest",
"class": "Illuminate\Session\Middleware\StartSession",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 180,
"function": "handle",
"class": "Illuminate\Session\Middleware\StartSession",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Cookie/Middleware/AddQueuedCookiesToResponse.php",
"line": 37,
"function": "Illuminate\Pipeline\{closure}",
"class": "Illuminate\Pipeline\Pipeline",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 180,
"function": "handle",
"class": "Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Cookie/Middleware/EncryptCookies.php",
"line": 67,
"function": "Illuminate\Pipeline\{closure}",
"class": "Illuminate\Pipeline\Pipeline",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 180,
"function": "handle",
"class": "Illuminate\Cookie\Middleware\EncryptCookies",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 116,
"function": "Illuminate\Pipeline\{closure}",
"class": "Illuminate\Pipeline\Pipeline",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
"line": 726,
"function": "then",
"class": "Illuminate\Pipeline\Pipeline",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
"line": 703,
"function": "runRouteWithinStack",
"class": "Illuminate\Routing\Router",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
"line": 667,
"function": "runRoute",
"class": "Illuminate\Routing\Router",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
"line": 656,
"function": "dispatchToRoute",
"class": "Illuminate\Routing\Router",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php",
"line": 167,
"function": "dispatch",
"class": "Illuminate\Routing\Router",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 141,
"function": "Illuminate\Foundation\Http\{closure}",
"class": "Illuminate\Foundation\Http\Kernel",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php",
"line": 21,
"function": "Illuminate\Pipeline\{closure}",
"class": "Illuminate\Pipeline\Pipeline",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ConvertEmptyStringsToNull.php",
"line": 31,
"function": "handle",
"class": "Illuminate\Foundation\Http\Middleware\TransformsRequest",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 180,
"function": "handle",
"class": "Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php",
"line": 21,
"function": "Illuminate\Pipeline\{closure}",
"class": "Illuminate\Pipeline\Pipeline",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php",
"line": 40,
"function": "handle",
"class": "Illuminate\Foundation\Http\Middleware\TransformsRequest",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 180,
"function": "handle",
"class": "Illuminate\Foundation\Http\Middleware\TrimStrings",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ValidatePostSize.php",
"line": 27,
"function": "Illuminate\Pipeline\{closure}",
"class": "Illuminate\Pipeline\Pipeline",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 180,
"function": "handle",
"class": "Illuminate\Foundation\Http\Middleware\ValidatePostSize",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php",
"line": 86,
"function": "Illuminate\Pipeline\{closure}",
"class": "Illuminate\Pipeline\Pipeline",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 180,
"function": "handle",
"class": "Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php",
"line": 49,
"function": "Illuminate\Pipeline\{closure}",
"class": "Illuminate\Pipeline\Pipeline",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 180,
"function": "handle",
"class": "Illuminate\Http\Middleware\HandleCors",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Http/Middleware/TrustProxies.php",
"line": 39,
"function": "Illuminate\Pipeline\{closure}",
"class": "Illuminate\Pipeline\Pipeline",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 180,
"function": "handle",
"class": "Illuminate\Http\Middleware\TrustProxies",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 116,
"function": "Illuminate\Pipeline\{closure}",
"class": "Illuminate\Pipeline\Pipeline",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php",
"line": 142,
"function": "then",
"class": "Illuminate\Pipeline\Pipeline",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php",
"line": 111,
"function": "sendRequestThroughRouter",
"class": "Illuminate\Foundation\Http\Kernel",
"type": "->"
},
{
"file": "/var/www/html/public/index.php",
"line": 52,
"function": "handle",
"class": "Illuminate\Foundation\Http\Kernel",
"type": "->"
},
{
"file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/resources/server.php",
"line": 16,
"function": "require_once"
}
]
}
I don’t know how to investigate any further.
2
Answers
I misinterpreted my error logs and found that there is another small difference using Axios and Fetch API: Axios seems to decode the X-XSRF-TOKEN whereas Fetch API does not.
So this example token is sent by Fetch API:
eyJpdiI6IkRScW9GNGtIbndWWFdKbzRiN0VWVkE9PSIsInZhbHVlIjoiNUNuQStiZGt0Y3l0cXhReThHSEJxbCsxRmZvbFFHKzhQV3ArSlg0cjVQVm5qSDZoQ3ZsSERnUTREUXRvczdMOHhYSHFqbm5FUjd2dFpGMlN4bU81NWJ5SWpmem5BQi9vRmJBRFZZWmFSZFlqcHNxbTZ5N1Z0cGJzSmMwcUFRaXUiLCJtYWMiOiI4MjM1MTQ2ODExNzhlY2ExNDk1NDhhOWEwNzE0OWJlMzViOGQxNDJhMTY0YTI2NzYwMThjMzQ5ODVmMDYwMjk1IiwidGFnIjoiIn0%3D
And this example token is sent by Axios:
eyJpdiI6IisydDZOOFJoREp5ZnZudUtjRk1teXc9PSIsInZhbHVlIjoiV1dQdjJDZlNrcW43Zlg4TW1yRFdjOVJWVnJkaC9CZndxejJLQ3JEUkJIRXJ3Z2pNb2pxSUJYN0Y2RDBxZ1hKd01mNHF6empsRkFIeEFOSkJKbi8vT0hWOFBYMDdlMkZybzBZdllJQlBFa1lHTytZd1E0aU9vL2pOM1ZRZWwxV0ciLCJtYWMiOiI3NTczZGJiMjg5MWZmOTUzMjhiMTZhNjAxM2ZiZmVjODVjYjc2MGRiMGJkMTFkOGYzOWQzYWQ5MjI5YWIwOTA5IiwidGFnIjoiIn0=
There is a very small difference at the end of the token string (= vs. %3D).
Laravel itself sends the Token value URIEncoded (with %3D) but seems to expect the token with = at the end.
There is nothing about that in the docs but this issue kept me searching for days and was not that obvious.
I changed my code to:
And now the login is working using the standard Fetch API.
I hope this helps someone else debugging their login flow.
Greetings.
This is happening due to page expiration as it is post request. Can you add these below credentials in .env file and run php artisan config:cache
Then see the result. YOu should also pass headers token as X-CSRF-TOKEN