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 Symptom: Logged Out After Paying
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.
The Cause: SameSite=Lax Withholds Your Cookie
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.
What Laravel Does With a Missing Cookie
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 Fix: Never Depend on a Cross-Site POST for Identity
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.
What About SameSite=None?
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.
Key takeaways
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.