Why OAuth Login Breaks With SameSite Strict, and the Fix
SameSite Strict silently signs users out mid-OAuth; the Lax plus CSRF-token combo keeps logins working and safe.
You added "Sign in with Google" to your app. The OAuth dance works: the user clicks the button, gets bounced to Google, approves, and lands back on your callback URL. And then your app insists they are not logged in. The session your callback just created is somehow not there. You stare at the code, the flow looks correct, the cookie is being set, and yet getSession() returns null every single time.
Your OAuth login breaks because the session cookie is set to SameSite Strict, and the provider's redirect back to your callback is a cross-site top-level navigation, so the browser refuses to send a Strict cookie on it. Change the session cookie to SameSite Lax, which still blocks cross-site form posts and fetches but allows the top-level OAuth return, and keep your CSRF token as the real mutation defense.
This is one of the most maddening bugs in web authentication because nothing in the code is wrong. The bug lives in a single cookie attribute, and the symptom, a user who appears signed out immediately after a successful sign-in, points you everywhere except the actual cause. The fix is one word, but understanding why it works is what stops you from reintroducing it the next time.
The OAuth callback is a cross-site navigation
Walk through what actually happens at the browser level. The user is on your-app.com, clicks the OAuth button, and the browser navigates to accounts.google.com. They approve. Google redirects the browser back to your-app.com/api/auth/callback. That final redirect, from Google's domain to yours, is a cross-site top-level navigation. The browser is following a link from one site to a different site.
Now the question that decides everything: does the browser send your session cookie on that incoming request? That depends on the cookie's SameSite attribute, and most security-minded developers set it to the most restrictive value out of good instinct.
Why SameSite Strict breaks the flow
SameSite=Strict means the cookie is never sent on any cross-site request, not even when the user clicks a link from another site to yours. The cookie only rides along when the user is already navigating within your own site. It is the most locked-down option, and it feels like the responsible choice.
But the OAuth callback is, by definition, a cross-site navigation arriving from the provider's domain. With Strict, the browser refuses to attach your session cookie to that request. Your callback handler runs, looks for the session, finds nothing, and concludes the user is not authenticated. So it redirects them to a login page, mid-flow, right after they successfully authenticated. The user experiences it as "I signed in and it logged me out." There is no error in your logs because nothing errored. The cookie simply was not present, exactly as Strict instructed the browser.
We hit this exact failure in production once. A session cookie was set to SameSite=Strict, the Slack OAuth callback was a cross-site top-level GET from slack.com back to the app, the cookie was not sent, the session lookup returned null, and the callback redirected the user to a "not authenticated" page in the middle of the OAuth handshake. The code was correct. The cookie attribute was the bug.
The fix: SameSite Lax for session cookies
The correct value for a session or auth cookie is SameSite=Lax. Lax is the modern browser default, and it strikes the balance the OAuth flow needs. It belongs alongside the rest of the security headers every Next.js app should ship. It allows the cookie on top-level navigations, which includes a user clicking a link to your site and includes the OAuth provider redirecting back to your callback. It still blocks the cookie on cross-site subresources: cross-site form POSTs, fetch requests, iframes, and image loads. Those subresource contexts are the common CSRF attack vectors, so Lax keeps the cookie from being sent in the situations that matter for CSRF while letting it ride on the legitimate top-level redirect.
res.cookies.set("session", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
maxAge: 60 * 60 * 8,
});
Switching from Strict to Lax does not meaningfully weaken your defenses, because of the second half of the picture.
Lax plus CSRF tokens is the real defense
The reason Strict felt necessary is that people lean on SameSite as their CSRF protection. It is not supposed to be your only one. SameSite is a secondary mitigation. The real CSRF defense is a dedicated CSRF token on every state-changing request.
Here is how the two layers cover each other. Every POST, PUT, PATCH, and DELETE endpoint requires a CSRF token, an HMAC-signed value with a nonce and a timestamp, verified with the same timing-safe comparison you use for password hashes before the handler does anything. That token is what actually stops a malicious site from submitting a forged request on the user's behalf, because the attacker cannot produce a valid token. SameSite=Lax then adds a layer on top by preventing the session cookie from being sent on cross-site subresource requests at all. With both in place, loosening SameSite from Strict to Lax does not open a CSRF hole, because the CSRF token was always doing the heavy lifting. The Strict setting was buying you a redundant bit of protection at the cost of breaking OAuth entirely.
So the rule we apply: session and auth cookies use Lax. State-changing endpoints are protected by CSRF tokens regardless of the cookie's SameSite value. The only place Strict is appropriate is a cookie that should genuinely never traverse a cross-site flow, like a short-lived, high-privilege admin token that never participates in OAuth, and when you set it there you document why. A token like that should also be scoped to least privilege so a leak cannot touch everything.
How to recognize this bug fast
The signature is specific enough to diagnose in minutes once you know it. The user authenticates successfully, and immediately afterward the app treats them as logged out. There is no error, just a session that should exist and does not. The flow works perfectly when the login is entirely first-party and breaks the moment a third-party provider redirects back. If you see "logged in, then immediately logged out, only with OAuth," check the SameSite attribute on the session cookie before you touch anything else. It will almost always be Strict.
The 2025 browser changes around cookie defaults made this more common, not less, as more sites set SameSite attributes explicitly and some reach for the strictest option by reflex. Setting attributes explicitly is correct; setting auth cookies to Strict is the trap.
Secure by default, not broken by default
The lesson generalizes beyond OAuth. Security settings have behavioral consequences, and the most restrictive option is not automatically the most correct one. Strict is more locked down than Lax, and it is the wrong choice for a session cookie because it breaks a legitimate, necessary flow while adding protection that your CSRF tokens already provide. Good security is layered and deliberate, not maximally restrictive at every knob.
This is the kind of subtle auth-flow failure that a code review can miss and a checklist can hide, because every individual line looks correct, the same way a leaked admin login URL slips past a review or a forgeable JWT claim reads as fine until someone replays it. We have learned to verify auth flows by walking them, not just reading them, and getting cookies, CSRF, and OAuth callbacks right is a standing part of the security work we do on every project. If you want to know whether your own setup has this trap or a dozen others like it, our free instant security scan is a fast first look.






