DEV Community

Cover image for My Next.js 16 Auth Passed Every Test. Five Bugs That Only Showed Up When I Wired It Together.

My Next.js 16 Auth Passed Every Test. Five Bugs That Only Showed Up When I Wired It Together.

Shubhra Pokhariya on June 30, 2026

The three-layer model works. Part 1 of this series is the invoice incident that proved it. Part 2 is the proxy.ts matcher gaps that fail silently. ...
Collapse
 
webdeveloperhyper profile image
Web Developer Hyper

Good debugging post as always! 😀
Authentication features are complicated, and even a small mistake can lead to serious security issues or sensitive data leaks. I'm always nervous when working on login-related features. 😅

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks! I feel the same way. Even a tiny mistake in auth can end up having much bigger consequences than you expect. It definitely made me slow down and test every flow more thoroughly. Appreciate the kind words.

Collapse
 
hemapriya_kanagala profile image
Hemapriya Kanagala

I enjoyed reading this 😄

I haven't worked much with Next.js auth, but it's always interesting seeing the kinds of issues that only show up once everything is connected together. Thanks for sharing them!

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks, glad it was a fun read! Every piece passed on its own, but wiring everything together exposed bugs I never would have found by testing each piece in isolation. Appreciate you reading through it!

Collapse
 
nazar-boyko profile image
Nazar Boyko

The forgot-password case for an OAuth-only account is one almost everyone ships without noticing, because your test users always remember how they signed up and real users genuinely don't. Sending the "you used Google" email instead of a dead reset link is the right call. The natural next step, if you haven't already, is letting those accounts set a password from their settings, so the person who's tired of the Google button has a way out that isn't resetting a password that never existed. Bug 2 was my favorite catch though, the router.push versus window.location thing burns people precisely because nothing errors, the cookie just silently never travels.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks, that's a good catch. I haven't built that yet. The forgot-password flow handles the "you used Google" case correctly, but you're right, there's no actual path for that person to set a password and move off the provider. That's a good feature to add.

And yeah, the router.push thing was the worst kind of bug. Everything looks fine, no errors anywhere, the cookie just never makes the trip. It took me longer than I'd like to admit to figure out why.

Collapse
 
allenrichard12 profile image
Allen Richard

Most auth systems don’t fail at the architecture level, they fail at the integration boundaries where browser behavior, timing, and state all interact in unpredictable ways.

Cookie issues like this are especially tricky because everything looks correct on the surface, until you trace what the browser actually persisted versus what the app assumed it persisted.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Yeah, the browser side of it is brutal. The cookie write returned true, document.cookie showed it right there, and then the next request just... didn't have it. No exception, no failed promise, just a silent gap between what the app assumed it stored and what the browser actually sent. Nothing looks broken when you check each piece alone, that's what made it slow to catch.

Collapse
 
mudassirworks profile image
Mudassir Khan

the JWT sub integer one costs the most debug time because the wrong tool gives you a false clear. reach for jose.decodeJwt() during investigation, no secret required, quick to drop in for a spot check. it shows the claim just fine: number or string looks identical in a console.log, so you call the token good and go looking elsewhere for twenty minutes. rule we settled on: always debug JWT payloads through the same code path that runs in production, meaning jwtVerify with the same options, not the convenience decode. otherwise you are testing different code from what is failing. does your extractPayload also enforce iss or aud? the sub type guard caught this bug — curious if you gate on issuer too.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

decodeJwt got me exactly like that. Console looked clean, token seemed valid, so I burned twenty minutes checking the proxy and the database before realizing the verify path was failing on a type I couldn't even see. Right now extractPayload only gates on sub type and expiration, no iss or aud yet.

Collapse
 
wrencalloway profile image
Wren Calloway

What ties all five together is that they're the same bug in five costumes: a success signal that was true on your side of a boundary and false on the other side. The cookie write returned true, then wasn't there next request. router.push "navigated," but no HTTP request fired. The JWT signed cleanly, but sub crossed as a number. Every one reported success locally and failed at the handoff.

The reusable version I've landed on: for anything crossing a boundary — client→server, JS value→signed token, write→next-read — the assertion has to live on the far side. You verified the write; the thing that mattered was the read. document.cookie telling you it's there is grading your own homework; the real check is "did the next request carry it."

The router.push one is the nastiest because there's no boundary crossing at all — the failure is that the network trip you assumed happened just didn't, and nothing errors because nothing ran. Great write-up. "Passed every test, broke when wired together" is exactly the class of bug unit tests structurally can't see, because the seam isn't inside any one unit.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

That's exactly the pattern. I was verifying the write, not the read, every time. The router.push one is the worst because there's no error at all. Just a missing network trip that everything downstream assumed happened. Good catch.

Collapse
 
wrencalloway profile image
Wren Calloway

"A missing network trip that everything downstream assumed happened" — that generalizes way past auth, too. Optimistic UI updates, fire-and-forget analytics, anything that'll "sync later": same shape, no error, just a quiet gap between what you think happened and what actually did. Really enjoyed the series — "each part passed alone, the seams failed" is the lesson most of us end up learning the expensive way.

Collapse
 
raju_dandigam profile image
Raju Dandigam

This is a very practical reminder that passing tests at the unit or layer level does not guarantee the full auth flow is safe. The cookie-write and router.push issues are especially good examples of bugs that only exist at the browser/server boundary. I like the pattern of moving security-sensitive behavior back to the server and making every mutation verify the caller before touching data. For production frontend platforms, these integration-edge cases deserve their own checklist because they rarely show up in isolated tests.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks! That ended up being my biggest takeaway too. Every layer passed its own tests, but the real bugs showed up where state crossed from one layer to another. I've started actually testing those boundaries now, not just assuming they'll work because each piece works on its own. Appreciate you reading through it.

Collapse
 
voltagegpu profile image
VoltageGPU

Great read — it's a good reminder that even with solid unit tests, integration is where the real pressure tests happen. I've seen similar issues when building secure enclaves with VoltageGPU; the layers work in isolation, but data flow across components can expose subtle timing or permission issues. Always value end-to-end testing in realistic scenarios.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks! That was the biggest takeaway for me too. Every piece worked on its own, but the full flow exposed bugs I never would have found by testing each part in isolation. It sounds like you've run into the same kind of integration problems, just in a different context.

Collapse
 
mnemehq profile image
Theo Valmis

This is the most honest bug-report title I've seen this week. Passing every unit test and breaking at integration is the exact gap that matters, because tests verify the pieces and say nothing about the seams. It's also why AI-generated code is deceptive here: each function looks correct in isolation, so the tests go green, and the bug lives in the assumption between two of them that no test exercised. Green tests prove the parts work. They don't prove the system does.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

That title came from staring at green tests while the login flow was still redirecting back to itself. Only noticed the AI-code angle after untangling this one. Every function tests fine alone, so you assume the system's fine too. But nobody writes a test for "did the cookie survive the handoff from client write to server read," and that's exactly where this one was hiding.

Collapse
 
kenielzep97 profile image
Self-Correcting Systems

The individual parts were fine, it was the gaps that got me” is the realest line in here. Every layer passed its own test and the system still failed, because the failure didn’t live in any one layer. It lived in the seam where they hand off.
The JWT sub bug is the cleanest example of it. The type guard was correct, the claim was malformed, and a number vs a number-as-string look identical in a console log. Surface reads valid, the layer underneath disagrees, nothing errors. Those are the ones that eat a full day.
On the OAuth forgot-password case hit a version of that myself. The fix that aged best was treating password_hash null as the source of truth instead of inferring the provider from UI state. Solid series.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

That line came from a real afternoon of staring at logs that all looked fine. The JWT sub bug was exactly that. console.log showed what looked like a perfectly valid user ID, the type guard was doing its job, and the mismatch turned out to be the claim type instead. Those are the bugs that take the longest to track down because every layer looks innocent on its own.

And you were right about password_hash. It ended up being the simplest source of truth instead of relying on UI state to infer anything. Glad to hear that approach held up well for you.

Thanks for the kind words on the series. It means a lot coming from someone who's clearly run into the same kinds of integration bugs.

Collapse
 
leob profile image
leob

Deep stuff, maybe Vercel should hire you!

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Appreciate it. Every bug here came from wiring things together. The individual parts were fine. It was the gaps that got me.

Collapse
 
vanvikki1986 profile image
vandhuukumar

Hello everyone.