The Problem & Code source
I am using Laravel 10, Livewire 3 & Jetstream 5. My website is on a custom domain, on an owned server. I have this website also placed in an iframe and it needs to work with an iframe, in another website I own.
When users try to log in the iframe, they get a 419 CSRF token expired error. Before changing the settings, the 419 error would be thrown as soon as the user would visit the website where the iframe is. Keep in mind, this only happens on mobile, most commonly for iOS users, but also sometimes for Android users. So, the user would visit the site with the iframe and a POP-UP of an error page for 419 would pop out, and then an alert asking If I want to refresh the page. And it was stuck in a loop and the website page was unusable basically for mobiles.
However, with the current settings & code, the 419 stopped appearing on page visit, but now appears whenever a user tries to log in(for /login
).
I did my research (Webkit.Org Full 3rd-Party Cookie Blocking | Mozilla Dev’s 3rd Party Cookies Article )
The src attribute for the <iframe>
is: https://domain.de/
Current state of my code and config:
config/session.php
:
return [ /*Values in comments are .env values, if nothing, then its not set in the .env*/
'driver' => env('SESSION_DRIVER', 'database'), /*database*/
'lifetime' => env('SESSION_LIFETIME', 120),/*120*/
'expire_on_close' => false,
'encrypt' => false,
'files' => storage_path('framework/sessions'),
'connection' => env('SESSION_CONNECTION'),
'table' => 'sessions',
'store' => env('SESSION_STORE'),
'lottery' => [2, 100],
'cookie' => env(
'SESSION_COOKIE',
Str::slug(env('APP_NAME', 'laravel'), '_').'_session'
),
'path' => '/',
'domain' => env('SESSION_DOMAIN'),/*.domain.de*/
'secure' => env('SESSION_SECURE_COOKIE'), /*true*/
'http_only' => true,
'same_site' => 'none',
'partitioned' => true,
];
config/cors.php
:
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['https://www.domain-with-iframe.de'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,
];
routes/web.php
:
//Routes that DON'T require auth/login
Route::middleware([
'web'
])->group(function () {
Route::get('/', function () {/*HOMEPAGE*/
return view('welcome');
});
Route::get('/impressum', [ImpressumController::class, 'show'])->name('impressum.show');
Route::get('listings/view/{listing}', ListingView::class)->name('listings.view');
Route::get('applications/create/{listing}', ApplicationCreate::class)->name('applications.create');
});
app/Providers/AppServiceProvider
:
public function boot(): void
{
if($this->app->environment('production'))
{
URL::forceScheme('https');
}
}
I tried to completely ignore the csrf token validation for the domain, but that didnt change much. If I add /login
and /register
it works as its supposed to, but thats not my goal, I want the tokens to be verified
app/Http/Middleware/VerifyCsrfToken
:
protected $except = [
'stripe/*',
'paypal/*',
'https://www.domain-with-iframe.de/*',
];
The index-all livewire component does not contain any form, nor are there any forms on the homepage
welcome.blade.php
which is the home page and the redirect from login:
<x-app-layout>
<div>
<div>
<div>
{{-- <div class="">@livewire('listing.pop-up-listing-modal')</div>--}}
<div class="z-30">@livewire('listing.index-all')</div>
...
So then comes the layouts/app.blade.php
:
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>[{{ trim(config('app.name')) }}] {{ trim($__env->yieldContent('title')) }}</title>
<!-- Meta -->
<meta name="robots" content="@yield('meta-robots', 'noindex, nofollow')">
@yield('meta-tags')
<link rel="canonical" href="{{ config('app.url') }}" />
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Scripts -->
<script src="{{ secure_asset('storage/assets/js/ui.js') }}" defer></script>
@yield('extra-js')
@vite(['resources/css/app.css', 'resources/js/app.js'])
<!-- Styles -->
@yield('extra-css')
@yield('code-css')
@livewireStyles
</head>
<body class="font-sans antialiased">
<x-banner />
<div class="min-h-screen bg-main-gray">
@livewire('navigation-menu')
<!-- Page Heading -->
@if (isset($header))
<header class="bg-white shadow">
<div>
{{ $header }}
</div>
</header>
@endif
<!-- Page Content -->
<main>
<x-auth-session-status :status="session('status')" :isSuccess="session('isSuccess')" />
{{ $slot }}
</main>
</div>
<!-- Footer -->
<div class="footer">
@include('layouts.footer')
</div>
@yield('code-js')
@stack('modals')
@livewireScripts
</body>
</html>
EDIT
I also noticed that the login.blade.php
is using the guest layout, which I forgot to put here:
<x-guest-layout>
<x-authentication-card>
<x-slot name="logo">
<x-authentication-card-logo />
</x-slot>
<x-validation-errors class="mb-4" />
@if (session('status'))
<div class="mb-4 font-medium text-sm text-main-text">
{{ session('status') }}
</div>
@endif
<form method="POST" action="{{ route('login') }}">
@csrf
<div>
<x-label for="email" value="{{ __('messages.email') }}" />
<x-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" placeholder="E-Mail" required autofocus autocomplete="username" />
</div>
The guest.blade.php
is the same as the one that comes out of livewire:
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
...
Things I have tried
- Made sure:
- There is a
@csrf
tag after each<form>
- ‘sessions’ table exists and is filled in. Made sure the sessions driver works (Works for desktop)
- Made sure the session’s table has ‘id’ as
varchar
'encrypt' => false
&'encrypt' => true
- Changed laravel cookie name to contain only letters and no dashes or underscores.
SESSION_DOMAIN=
.domain.de
domain.de
- Clearing cache and optimize don’t change anything
Folder permissions are as they should be and weren’t changed. I dont use a file driver, regardless my/storage
&/vendor
have 755 permissions - Made sure the live site has a good SSL certificate
- There is a
- Laravel 5.7 throws 419 Error (Posted 2018, answer updated 2021):
- Its related to Laravel 5, 6 & 7 and since mine works fine for desktop, but has problems for the mobile view, its unrelated
- Laravel returns "419 PAGE EXPIRED;" After Signin/Register on Chrome & Edge browser etc [duplicate] (Posted 2022)
same-site
is alreadynone
SESSION_SECURE_COOKIE=true
because I have a valid SSL certificate
- Laravel 7 session break on IFRAME in a different domain (Posted 2020)
same-site
is alreadynone
- iFrame loading via chrome and safari – status of 419 (Posted 2023) which then leads to disable csrf in laravel for specific route
- I need the csrf verification, because It’s bad practice not to have it, plus there will be many users, I dont want to risk an attack. My VerifyCsrfToken shouldn’t be changed to include any other of my live site routes; I want each CSRF token to be validated and verified, so this article is not only useless, but actually bad practice for regular routes.
- When I have tried commenting out the VerifyCsrfToken. It doesnt throw 419 anymore, however it doesnt log the users in as well. Again, ONLY on mobile. When I comment it out on desktop, it works normally, the user gets logged in.
- Embedding Laravel Form in iFrame shows 419 (CSRF token mismatch) Error
- Same as 4.
- Problem with Livewire in an iFrame
- My
VerifyCsrfToken
already has the URL with the iframe in the$except
array - I dont have a
/livewire
route, so no need to put that in the array
- My
EDIT
- Used IOS Webkit Debug Proxy to grab the Console output through Chrome DevTools.
I managed to get it working on desktop, but this still isnt working for mobile, not even a single console.log gets printed(While it works fine on desktop):
class CSRFTokenManager {
constructor() {
console.log('CSRFTokenManager constructor called');
this.init();
}
init() {
console.log('Initializing...');
document.addEventListener('DOMContentLoaded', () => {
if (this.inIframe() && this.isMobile()) this.fetchCSRFToken();
else console.log('Not in iframe or not on mobile');
});
}
inIframe() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}
isMobile() {
return /Android|webOS|iPhone|iPad|iPod|Opera Mini|Mobile/i.test(navigator.userAgent);
}
fetchCSRFToken() {
console.log('Fetching CSRF token...');
axios.get('/sanctum/csrf-cookie').then(response => {
this.setCSRFToken();
console.log('CSRF token fetched');
}).catch(error => {
console.error('Error fetching CSRF token:', error);
});
}
setCSRFToken() {
const token = this.getCookie('XSRF-TOKEN');
if (token) {
axios.defaults.headers.common['X-XSRF-TOKEN'] = token;
console.log('CSRF token set in axios headers');
} else {
console.error('CSRF token not found in cookies');
}
}
getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
}
new CSRFTokenManager();
Its like this whole thing is working against me, and Its getting really frustrating and desperate. To make things worse, on the DevTools for chrome, every tab is working fine, the Console, Application, Memory, etc, but OF COURSE the ‘Network’ tab is just a blank white page when I click on it, so I cant even see what files are loaded.
What else can I try? How can I even debug this? I have control over both sites, the one that has the iframe is a Ionos website.
2
Answers
Seems like the only solution is making it an API application and then use one website to send APIs requests to the other one and build views on recieved data. This complicates things a lot and makes it more difficult. I dont understand why there's such a constrain when I'm the owner of both websites.
Really glad this thing
isdocumented in Laravel & Livewire, I thought iFrames are commonly used.Is this not related to this update? https://blog.heroku.com/chrome-changes-samesite-cookie