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
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 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.
sameSite
, if set toLax
, will forbid the browser from including cookies in cross-site "sub-requests" using anything else than aGET
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 toStrict
, 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
cookies won't be accessible through Javascript'sDocument.cookie
object.
Additionally, the Secure 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!
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
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
header.
This will allow you, for example, to specify sources you want to allow for media resources (such as images or videos).