Notes on auth token persistence Published the 2020-03-17 In a "traditional" website, authentication is usually handled through sessions, a session being a temporary server-side stockage associated to a cookie identifying the session. For example, by default in PHP, the `PHPSESSID` cookie contains a "random" string that identifies a server-side object containing the user's session details. But with the hype around JWTs and the "stateless" craze, we're seeing a worrying, unsecure trend. We're seeing the following habit: - The session token stored in local storage - The session token manually handled on request-time ```js const token = localStorage.getItem('auth-token'); fetch('/api/super-secure-data', { headers: { 'Authorization': `Bearer ${token}`, }, }); ``` So, what issue can we see here? - Relying on the developer to always think about setting the authorization header *will* let some mistake slip - The token being stored in JS-accessible memory makes it accessible to *every* script, thus a simple XSS attack can get you to access someone else's account without their knowledge But, what solution does exist? Well, cookies. Both `XMLHttpRequest` and `fetch` automatically support cookie passing, so you won't have to handle tokens yourself. > Note that for `fetch`, there is [a setting](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters) > to set. "But, there's a lot of topics on how to access cookies from Javascript. How is it any better?" Cookies, like most things in IT, has some flags that can be enabled to instruct the browser to handle them differently. Two of them are interesting here. [httponly]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#Secure_and_HttpOnly_cookies - [`sameSite`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#SameSite_cookies), if set to `Lax`, will forbid the browser from including cookies in cross-site "sub-requests" using anything else than a `GET` request (such as images or frames), but will still include the cookies if the user follows the URL from an external site (for example, by following a link). If it's set to `Strict`, it will require the browser to *not* send the cookie to another domain than the one defined in the cookie, effectively preventing CSRF attacks. - [`httpOnly`][httponly] cookies *won't* be accessible through Javascript's `Document.cookie` object. > Additionally, the [Secure][httponly] attribute will force cookies to > only work over HTTPS. That means that sending an Ajax request to another domain, the cookies won't be transfered no matter what happens. That also means that, since *you* don't see them, a malicious code will also *not* be able to access them, so no auth data extraction! ```js fetch('/api/super-secure-data', { init: { credentials: 'same-origin', }, }); ``` *But, what if I interact with an authenticated API service which doesn't use cookies and requires me to use something less secure, such as an `Authorization` header?* The thing, with those rules, is that it only applies to the browser. You can (and probably should) set up a proxy that will transparently convert the defined cookie to an `Authorization` header. Most HTTP reverse proxies, such as Apache, NGINX, Caddy, Traefik, and such, are able to do such a job for you, with little configuration. ## Additional steps [csp]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy If you want some more fine-grained control over which network feature is or isn't allowed, you should definitely take a look at the [`Content-Security-Policy`][csp] header. This will allow you, for example, to specify sources you want to allow for media resources (such as images or videos).