DIY: Auth in Next.js - less magic, more control

Ein einfacher schwarzer Umriss eines Vorhängeschlosses und eines Schlüssels auf einem hellblauen Hintergrund.

In my time as a Java developer (yes, I did it - Java! 🙈), especially with Spring Boot, the world of authentication was simple. Two lines in the application.yaml - and zagg-💥, a secure login was available.

The libs were mature and secure and preconfigured in such a way that you couldn't really do anything wrong. An unwritten law of mine back then was:

«You don't build Auth yourself.»

Good old Java Developer

Point.

But the reality of modern web applications, especially in the Next.js cosmos with the whole range from server-side rendering to streaming components and API endpoints, is different. It's about complex front-ends, customized user experience and specific requirements that often go beyond what all-in-one solutions offer as standard. And this is exactly where I came to a point where my perspective changed.

Not because security is suddenly easy. But because customizing is more important in modern Next.js projects than a rigid "one-size-fits-all" approach. If you really want to understand and control Auth, there is no way around a custom setup these days.

🤔 Anyone wondering now: "Wow cool, dann darf ich jetzt also alles selbst bauen – inklusive Benutzername/Passwort Verwaltung etc?"

Ein Mann im Anzug steht in einem Büro und wirkt schockiert oder überrascht, mit leicht geöffnetem Mund und großen Augen.

No way! Building your own password logins is still absolutely taboo!!!iii!!! 🙅 There are sophisticated solutions like Citadel or various social login providers. I'm only interested in the OIDC integration here.

If your Auth-Flow not Standard is

Modern web apps often have complex requirements for Auth:

  • Individual user/session models
  • External providers with slightly different behavior
  • Server-side control over sessions
  • UX without "login flicker" on refresh
  • Support of backchannel logout

Tools such as NextAuth (or new with v5 Auth.jswhich has been beta for a very long time) were never made for this. They are great for a quick start (#prototyping) - but if you have to patch 80% of your auth flow afterwards, you might as well do it right.

This is exactly where the Lucia auth library came in - a very simple auth library that I even used in a Blogpost have introduced. Unfortunately, Lucia is now deprecated. ⚰️🥲

But the exciting thing is that the little bit that Lucia has done - essentially the plugging together of the lower components - can also be easily implemented yourself. And in such a way that you are even more flexible and still leave the safety-critical parts to mature libraries.

And so today I am at a point where I say: Yes, you can also make OIDC yourself - and do it really well

What a stable, secure OIDC flow in Next.js really needs

🧊 1st Arctic - OIDC as it should be

Arctic takes care of what makes OIDC the most complex:

  • PKCE and state handling
  • Generate login URLs
  • Exchange code for token
  • Multi-provider support

And all this without telling you how to save your session or what your user model should look like.

🧪 2nd Oslo - JWTs & hashes without bloat

Oslo provides exactly what you need for security on the node side:

  • Verify JWT (e.g. ID tokens from the provider)
  • Hashes for secrets, passwords, refresh token keys, etc.
  • Generate CSRF tokens

Oslo deliberately remains small and unagitated. Perfect for your own car backends.

🧠 3rd Redis - Sessions as they should be

Session handling is often the shakiest part of web apps. Many rely on encrypted JWTs as session tokens - but this has its pitfalls:

  • No server-side invalidation
  • No simple TTL handling
  • No logout without token blacklisting
  • Problems with refresh tokens (cookie update in Next.js not always possible)

You can map sessions correctly with Redis:

  • Token references session ID
  • Session is saved and controllable on the server side
  • User and session data clearly separated
  • Preferably still encrypted on the server side

👉 Redis is my favorite here, but ultimately you can use any fast, reliable database. The only important thing is that sessions are stored on the server side and can be controlled centrally.

🔁 4. refresh tokens - for smooth UX without security compromises

Instead of keeping the access token valid forever, it is a must to rely on short-lived tokens plus refresh flow. This brings:

  • Security (token theft quickly becomes irrelevant)
  • UX (no login flicker, no hard redirects)
  • Control (refresh only allowed on server side)

The refresh flow runs fully on the backend side.

🚪 5. backchannel logout - if your provider allows it

Backchannel logout is the cleanest way to end sessions centrally. If, for example, a user is logged out of the central SSO system, the OIDC provider can send a logout hook to your backend - and you kill the session directly in Redis.

No polling. No more client-side checks necessary.

🔒 6. backend-for-frontend (BFF) - for maximum control

A dedicated backend-for-frontend (BFF) ensures that access tokens never end up in the client - no local storage, no risk of XSS. If the session on the server expires or is actively terminated, the client can no longer do anything - no flicker, no pseudo-cleanup.

The BFF thus becomes the central instance for login, refresh, logout and user data - and makes your auth flow secure, robust and clear. And if you've read this far: respect! 🎉

Conclusion: Auth is not rocket science - if you know what you're doing

For a long time, I thought auth was a minefield that you'd better not enter yourself. Today I see it differently: If you understand what you need - and know what you're doing - you can and should build an Auth flow with Arctic, Oslo & Redis that is more secure, more flexible and more maintainable in the long term than many all-in-one solutions.

And the best thing is: you have full control.

No "magic defaults". No library black box. Just Auth as it suits your app.

PS: The documentary about Arctic and Oslo is great, be sure to check it out.

Written by
Lorenz Bösch

Next.js|August 2025

More Articles