DEV Community

Cover image for An Auth Story...
ZIKORA CHUKWUKA
ZIKORA CHUKWUKA

Posted on

An Auth Story...

I love system design. I love geeking out about the tiny decisions that make software great — for me, for those I work with, and most of all for my users.

My biggest obsession as a budding engineer is ensuring that the products I work on actually work as expected, every time, no matter the edge case.

I am also pretty obsessed about security.

I mean, what is the use of software if it is unsafe to use or loses money when it gets taken down? If I can do anything to secure the software I work on, I would.

Security is something I want to get better at, and I believe talking about it and sharing my work and learnings publicly is one way to do just that. So that is the purpose of this article.

I want to share a concept I learned while implementing Auth as the frontend engineer at an AI startup.

This is my Auth story.


I build my frontends with Next.js.

It basically chose me. It felt like the best option while I was starting out.

It is an elegant framework for a one-man team. I love the idea of having both my frontend and backend within the same codebase. I love a single deployment pipeline. I love shipping fast without reinventing the wheel.

So when the opportunity to join a lean startup team came, choosing Next.js was a no-brainer. It meant I could build fast, deploy faster, and implement solid frontend architecture patterns and security as a one-man frontend team without getting overwhelmed.

Working on this project pushed me to think more carefully about something I had always glossed over: how authentication tokens are actually stored and protected in the browser. The answer to that question is cookies, and specifically, HttpOnly cookies.

Black and white illustration of HTTPOnly Cookie

Let's talk about Cookies. Specifically HttpOnly Cookies. What they are, why they are useful, when they are useful, and specifically why I used them in my project.

Disclaimer: I am not an expert on this topic. I am only sharing from my personal experience building real projects. Please feel free to give me feedback. I am here to learn.

So what are Cookies?

Cookies are technically key value pairs stored in a browser. For example:

sessionId = abc123xyz

To use an analogy, imagine you drank coffee several times a day at a coffee shop close to your home or office, and every time you visit, the barista asks you the same questions: your name, your preferences, and so on.

You are not just a casual visitor, remember? You are a regular, and several times a day at that. I am sure you would be frustrated every single time. I know I would. Can you not just write my name and preferences down or something!

Well, that is the same thing that happens when a user visits your app in their browser. By their very nature, HTTP requests are stateless. This means they carry no context about who is making them or where the request is coming from. So in the eyes of your server, your best and most regular users are just another request.

Your APIs do not know that a particular request is coming from the same user, so they treat every single request like what it is: just a request. Your users would have to manually authenticate every time they take an action on the frontend that sends a HTTP request to your server.

Cookies help solve this problem by giving the browser a way to tell the server that the HTTP requests are coming from the same logged-in user. So the user authenticates once and can use your app without having to re-authenticate every time their action sends a request to the server.

Cookies are not the only way to solve this problem. Most developers also use local storage:

localStorage.setItem('token', 'abc123xyz')

However, local Storage has its security concerns. It is readable by JavaScript, which means it is exposed to XSS attacks as are regular cookies.

When it comes to server calls and HTTP requests, cookies are actually the more suitable option, and as you will see, we can make them significantly more secure.

The Problem with Cookies

Regular cookies, as I established, are just key value pairs stored in a browser. And because they are stored in a browser they are exposed and can be read by anyone with access to it, including a malicious website.

You can open your browser console right now and type:

document.cookie

To see all your stored cookies.

Cookies can store a number of things. They are basically key value pairs, so you can put whatever string you want in them. They are used to store authentication and session tokens, preferences like dark mode and language, and tracking or analytics data.

The most common use case though is storing session tokens, and that is the whole reason for this article.

You set it up once via:

Set-Cookie: sessionId=abc123xyz

And from then on, the browser sends it back automatically with every request:

Cookie: sessionId=abc123xyz

This means that cookies, while convenient, create a risk surface that makes them a security concern.

In reality, anyone who gets hold of your user's token can authenticate as them and perform actions on their behalf. This vulnerability is the foundation of a very common attack called cross-site scripting (XSS), where a malicious site can read and steal your user's token and do whatever they want on your app acting as that user. Pretty scary.

HTTPOnly cookies solve this by adding a flag. A simple instruction that tells the browser one rule: this cookie is for HTTP requests only. JavaScript is not allowed to touch it.

Set-Cookie: sessionId=abc123xyz; HttpOnly

So when an attack runs:

document.cookie // "" ← the HttpOnly cookie is completely invisible

Nothing happens. The cookie is invisible but still gets sent with each HTTP request. Sweet!

A quick Note

HTTPOnly Cookie is not a silver bullet. While it solves the XSS problem sufficiently, it still leaves two very common cookie vulnerabilities that a serious attacker can exploit: attacks over insecure connections (HTTP instead of HTTPS) and cross-site request forgery (CSRF).

We can address both using two additional flags:

Set-Cookie: sessionId=abc123xyz; HttpOnly; Secure; SameSite=Lax

The Secure flag ensures the cookie is only ever sent over HTTPS. The SameSite=Lax flag helps block CSRF attacks by restricting when the browser sends the cookie across sites. If you want to be even stricter, SameSite=Strict is an option, though it can affect user experience in some cases. Lax is a sensible default for most apps.

Making it all work in Next.js

This is where it all comes together, the juicy part if you will.

Going forward I try to explain my implementation of these concepts in a recent project, the unique features in Next.js that made my decisions possible, and my specific constraints.

For a bit of context: the frontend I am building is for an AI startup. We are building an AI agent for unified customer communication. Our stack of choice is roughly Next.js for the frontend and FastAPI for the backend. We are a small team trying to ship an MVP fast. I lead development on the frontend, consuming a robust FastAPI backend.

The constraint and opportunity

One of the many design decisions we made was to handle cookie setup on the frontend. This is in addition to largely limiting how much the client directly calls the FastAPI server.

While I can't fully go into our system design here, Next.js offers features that are uniquely suited to these constraints. I will be going deep into my frontend implementation of the Auth flow specifically.

A Next.js Primer

To fully follow along there are a few things you need to understand about Next.js and its unique architecture:

  1. The Next.js Server Next.js (specifically the modern app router) is basically a running Node.js process that receives every request, does server-side work and sends back a response. It is a real backend just one that happens to also serve your react UI.

  2. Server Actions A newer Next.js feature that lets you write a function on the Next.js server and call it directly from a form in your UI.

  3. Middleware (Proxy.ts) Code that runs before every request hits your pages. If you are on Next.js 15 or earlier, this file was called middleware.ts. The rename happened in Next.js 16 to better reflect what the file actually does: it sits at the network boundary and proxies requests before they reach your app. The logic is the same either way.

4.Server components vs Client components Server components are React components that never ship their code to the browser. They run on the server and can directly access databases and APIs. Client components run in the browser. Every component in Next.js is a server component by default.

5.Route Handlers Custom request handlers that allow you to build custom RESTful API endpoints in Next.js

These five concepts unlock a lot of benefits and are what allow me to implement my Auth flow within the defined constraints.
Here is the breakdown:

Auth flow diagram in Next.js

To elaborate: I am designing this flow to handle cookie setting on the frontend, hide the FastAPI base URL from the browser, and largely isolate my backend from external interaction. This creates a server-to-server setup instead of a client-to-server one. Next.js gives me all the pieces to make this possible.

When a new user creates an account and logs in for the first time, the form's action calls a loginAction server action. This server action takes the user's credentials and forwards them to FastAPI directly (server to server), so the browser never talks to FastAPI at all. FastAPI validates the credentials and returns a token string to the server action, which then writes the token into a cookie marked httpOnly: true. The browser receives and stores the cookie but can never read it with JavaScript.

On every subsequent request, the proxy checks for the cookie before any page loads. If the cookie is missing, it redirects to /login. If present, it lets the request through. I want to be clear that I am using the proxy for a lightweight presence check here, not as a full authentication solution. The real auth logic lives on the FastAPI side.

Once through, the Next.js server handles the request in one of two ways depending on what is being loaded. For pages and data that can be fetched at render time, server components read the cookie directly and call FastAPI server to server. For data that needs to be fetched live on the client (in my case, a live inbox that polls every 30 seconds), client components call a Route Handler instead. The Route Handler runs on the Next.js server, reads the cookie, calls FastAPI, and returns the response to the client. The token never leaves the server either way.

So there you go. My not so complicated implementation of HttpOnly Cookies with Next.js within my specific constraints.

I acknowledge there are simpler ways to achieve this, but I want to say again that every case is unique and I implemented what made sense for mine.

Have you handled Auth differently in your Next.js projects? I'd genuinely love to know what you did differently and why.

Top comments (1)

Collapse
 
chiemelie profile image
Chiemelie

Genuinely, this is masterpiece of system design you got here. I use Appwrite for my own backend. I learn much from this, will be refactoring my app while looking out for this. Thank you. 👍🏾