Updated 31 July 2026
A shopper pays, returns to your store, and is suddenly logged out.
Their cart is empty, their session is gone, and no error was ever thrown.
We hit this on a Bagisto store during a Stripe payment redirect.
The cause was not Stripe. It was one cross-site POST and a cookie rule we all forget.
The payment succeeds on the gateway, then the shopper lands back on your domain.
From that request onward, auth() sees a guest. The old session id is simply gone.
Nothing in your logs looks wrong, because technically nothing failed.
Laravel signs users in with a session cookie. Bagisto ships it as SameSite=Lax.
Lax has one rule that matters here, and it is very easy to overlook.
The browser sends a Lax cookie on same-site requests and on cross-site top-level GET.
It does NOT send it on a cross-site POST. That is the whole bug in one sentence.
When a gateway POSTs the shopper back to your site, the session cookie is withheld.
The return request now arrives with no session cookie. Laravel does not error.
StartSession reads the cookie, finds nothing, and starts a fresh, empty session.
Then it writes that new session back to the browser on the very same response.
|
1 2 3 4 5 6 7 8 |
// Illuminate\Session\Middleware\StartSession (simplified) $session = $this->getSession($request); // reads the session cookie // cross-site POST -> cookie missing -> a brand new, empty session id $response = $next($request); // This ALWAYS queues a Set-Cookie with the current session id. $this->addCookieToResponse($response, $session); |
That Set-Cookie uses the same cookie name, so it overwrites the authenticated one.
The browser keeps the new empty cookie, and every later request is now a guest.

Browser-cookie-flow-and-session-overwrite
The reliable fix is to bring the shopper back with a top-level GET, not a POST.
A GET top-level navigation is exactly the case where Lax still sends the cookie.
|
1 2 3 |
// Return the shopper on a normal GET redirect, so the Lax cookie travels. 'success_url' => route('shop.stripe.recurring.success').'?session_id={CHECKOUT_SESSION_ID}', 'cancel_url' => route('shop.stripe.recurring.cancel'), |
Keep gateway-to-server callbacks stateless, and out of the browser session entirely.
A webhook or IPN is a server-to-server POST. It must never read or write that session.
|
1 2 3 |
// Stateless, CSRF-exempt, no session dependency. Route::post('webhook', 'webhook')->name('webhook'); Route::post('ipn', 'ipn')->withoutMiddleware(VerifyCsrfToken::class)->name('ipn'); |
If a cross-site POST return is truly unavoidable, do not trust the cookie at all.
Carry a short-lived signed token in the URL and rebuild identity from it server-side.
Setting the cookie to SameSite=None; Secure does let it survive a cross-site POST.
But it re-opens the CSRF surface that Lax was protecting, and it forces HTTPS.
Treat it as a last resort, not a fix. Prefer a GET return and stateless callbacks.
1. A cross-site POST drops your SameSite=Lax session cookie.
2. Laravel then mints a new empty session and overwrites the old cookie.
3. The shopper is logged out silently, with nothing in the logs.
4. Bring users back with a top-level GET, so the Lax cookie is sent.
5. Keep webhooks and IPNs stateless and off the browser session.
Thank You for Reading!
You now understand how one cross-site POST can quietly reset a Laravel session.
Audit your payment returns, keep identity on GET, and keep callbacks stateless.
If you have questions, suggestions, or feedback, we would love to hear from you.
Leave a comment below, and we will be happy to help.
You can also explore our Bagisto Extensions.
If you are planning to build with Laravel, consider hiring laravel developers.
If you have more details or questions, you can reply to the received confirmation email.
Back to Home
Be the first to comment.