Mathieu Agopian : Storing credentials or tokens on the frontend

Security is hard. Also, I'm not a security expert, so please take what you read here with a grain of salt, and let me know if there's anything wrong.

That being out of the way: I went down a rabbit hole lately, when someone told me: "we worked hard to store the auth token in cookies for our frontend, because we previously stored it in localStorage, and it's very insecure".

Indeed a search for "localStorage insecure" will return many results.

But then, is storing sensitive data in cookies, like we did in the old days, more secure?

Say you're developing a frontend for some API, or an SPA, and you need to authenticate a user. Let's break things down, and try to understand what are the risks, what are the tools at our disposal as web developers, and what are some of the solutions.

Attacks and vulnerabilities

There are two main ways to attack a website:

CSRF

A Cross Site Request Forgery is a very easy attack that works against cookies: a browser willingly attaches cookies to a request that goes to the associated website.

Say I have a cookie created by https://my.bank.com with my credentials. If I end up being tricked to follow a link to http://bad.com/evil_csrf_fake_form that displays a form looking like the following:

<form action="https://my.bank.com/account/transfer">
    <input type="hidden" name="to" value="evil_attacker" />
    <input type="hidden" name="amount" value="123456" />
    <input type="submit" value="Click here for a chance of winning a free gift!" />
</form>

The browser will happily attach the cookie from https://my.bank.com to the request, from any website or form. The backend, upon receiving the request with the attached valid credentials, will happily transfer the money.

XSS

A Cross Site Scripting attack is usually a bit more elaborate, as it needs to inject some javascript in the rendered page on the attacked website. Once it's done though, it's basically game over for the website, as the attacker will have access to everything the legitimate javascript code has. Including in-memory data, localStorage, cookies without httpOnly.

An attacker could dump the content of the localStorage using a very generic JSON.stringify(localStorage), or target the website specifically and access in-memory data, or forge a fake login form that would look totally legitimate AND be on the legitimate domain.

Storage and persistence of sensitive data

Nowadays you can store sensitive data like username/password, bearer token, jwt token... either in:

Memory

Storing in memory means holding the credentials or token in a variable, a redux state, closures or whatever.

Good: this is not vulnerable to CSRF, and an XSS attack will need to be targetted towards your specific way of storing the data.

Bad: still vulnerable to a targetted XSS attack, and you're not persisting the credentials or token: if the user opens a new tab or restarts their browser, they'll have to re-authenticate.

localStorage or sessionStorage

Both are nearly identical, the first one persists accross tabs and browser restarts.

Good: readable by javascript, very convenient, not vulnerable to CSRF attacks. They are both readable by javascript

Bad: readable in a generic way like JSON.stringify(localStorage) which makes it very easy for an attacker in case of an XSS

Cookies

Cookies can be created as httpOnly, which means the content isn't readable by javascript. It can also be sameSite to defeat CSRF attacks, but beware that this isn't supported by all the browsers yet, and is obviously only available if the frontend is served from the same domain.

Good: the cookie is automatically attached to any request sent by your frontend. Not vulnerable to XSS if httpOnly. Not vulnerable to CSRF if sameSite.

Bad: Vulnerable to CSRF if you can't use sameSite because not supported by all the browsers you target, or the frontend is not on the same domain or you're using an auth solution like auth0 or openID that you don't control.

The problem

Frontend and backend on the same domain

You control the backend, and the frontend is on the same domain. You're thus only vulnerable to XSS (but then again, an XSS is game over). To minimize the risks you might prefer staying away from local or session storage.

If there is an XSS in your frontend, once you fixed it you can remove the open sessions on the backend. Everybody will need to re-authenticate, and the attacker will have lost all privileges. Also make sure they didn't create an admin user in the meantime!

No control over the backend

Also applies if the backend is on another domain. In such a case, you can't use sameSite, and the backend might not support CSRF tokens (eg if it uses stateless authentication).

Let's take a concrete example (using hasura and a JWT token) which happens to be the place where I found out about the perfect solution.

A JWT token is a very sensitive piece of data: as long as it isn't expired it will be accepted. Even if you discovered an XSS on your frontend and fixed it, if the attacker has a victim's JWT token, they'll still be able to use it. There's no easy way to revoke a JWT token (see token invalidation).

So you need to:

This means that a user will have to re-authenticate every 15 minutes, and every new tab or browser restart. Which is obviously a pain.

So here comes the refresh token to the rescue!

This refresh token is stored in a httpOnly cookie: whenever the in-memory JWT token is about to expire (or has expired), the frontend can make a request to some /authorize endpoint that will return the new JWT token. The frontend can then store this new short lived token in memory.

It's safe to have this refresh token in a cookie because:

Conclusion

So, do we have the perfect solution with in-memory short lived sensitive data and a refresh token in an httpOnly cookie? Sure, if you're willing (and able) to set it up. It does mean that the auth backend has to support this flow (I believe it's not the case for auth0 as they recommend using silent authentication which still has the limitations of in-memory: no persistence accross tabs/restarts). It does add an extra layer of complexity compared to a simple solution like persisting the session in localStorage. And it's not a silver bullet in case of an XSS.

You might consider that if an XSS is game over, then you might as well use localStorage which is way more convenient. But then, it's not because a dedicated thief can break any lock that you leave your door open when you leave the house, right? It's a matter of mitigation and reducing the attack surface.

If you don't have anything of value in your house, you might choose to close the door, and leave the keys under the welcome mat for convenience. How did you like my analogy of using localStorage? ;)

By the way, if the "XSS is basically game over" sounds scary, know that there are ways to mitigate those, see an interesting article Github CSP journey, and a challenge to learn a bit more about this subject.

All posts

  • Making a pong game in elm (4)
  • Making a pong game in elm (3)
  • Making a pong game in elm (2)
  • Making a pong game in elm
  • Python et asyncio : la recette du bonheur ?
  • Latence et boucle de rétroaction
  • Heartbleed, conséquences pour les utilisateurs
  • DjangoCon Europe
  • Objets ou fonctions
  • Quitter Gmail : gestion des contacts
  • Quitter Gmail : migrer ses mails
  • Quitter Gmail : créer son compte mail
  • Quitter Gmail : réserver son nom de domaine
  • Quitter Gmail
  • Au revoir Novapost, bonjour FruitLab
  • Retour sur Pytong 2013
  • Retour sur Sud Web 2013
  • Django1.5 : passer au Configurable User Model
  • Le sport
  • Création d'un FabLab sur Valence
  • Lancement de FaitMain.org
  • Djangocon 2012 Tolosa Edition
  • Taxonomie des entreprises
  • Plan de carrière d'un développeur
  • Automatiser son flake8 avec vim et syntastic
  • Vim + Screen : le pair-prog des champions !
  • Le miroir PyPI du pauvre
  • Vim, Restructured Text et espaces insécables
  • VIM et la correction orthographique
  • Djangocong 2012 !
  • Point-virgule
  • La bidouille django du jour: appeller un templatetag depuis un autre templatetag
  • Contribuer à Django, premiers pas (patcher la doc)
  • Sud Web, c'est bon pour ton web
  • Contribuer à Django, premiers pas (les outils, l'environnement)
  • Contribuer à Django, premiers pas (revue de tickets)
  • Djangocong 2011 : une cuvée d'exception
  • django et le handler500: retourner une erreur 503
  • La technique pomodoro : retour après plus d'un mois d'utilisation
  • La technique pomodoro : retour après deux semaines d'utilisation
  • django: redimensionner une image à la volée en préservant son ratio
  • Django forms, HTML5 et fieldsets
  • Double encodage utf8 : afficher correctement avec python et django
  • La vie a la couleur qu'on veut bien lui donner
  • lancer gunicorn avec supervisord
  • PyCon.fr 2010 : retour sur une conférence organisée par l'AFPY
  • djangocong : rencontre Django à Marseille
  • MySQL, mysqldump et PHP : convertir de latin1 vers utf8
  • Django : Envoyer des emails HTML avec images inline (intégrées)
  • lancer gunicorn avec runit
  • gunicorn: un server wsgi ultra simple à utiliser et configurer
  • Installer PIL (Python Imaging Library) facilement avec pip
  • Obfuscation de l'email alternative et accessible
  • Linux: savoir si le processeur est 32bits ou 64bits
  • PyCON.fr, excellent!
  • PyCon.fr: venez m'y voir!
  • Le contrôle de versions de sources: pourquoi?
  • Django, sqlite et mod_wsgi, attention au piège!
  • Checklist: différences entre MySQL et les modèles Django
  • MySQL et les modèles Django
  • Apprendre à faire, et faire
  • Django FileField et ImageField, upload_to et shell python
  • Django svn et mod_wsgi, attention au piège!
  • 30 ans, et toutes mes dents