Previously we had a hands-on look at how to move from stateful to stateless RESTful security. We used Spring and JWTs to authenticate and authorize our users, also protecting ourselves from CSRF attacks in case we store our tokens in session on the browser.
Now, session-based security has been going on for a long time. It is a proven approach whose inherent pitfalls are known: implementing CORS filtering and CSRF tokens is pretty well documented (feel free to refer to this and this post on my blog). Frameworks provide an almost out-of-the-box solution for your project. And remember: the objective here is not to be trendy, but to make our web application secure!
So is a stateless, JWT-based approach worth it? Does it bring enough advantages to you? Is it as secure as session-based solutions?
Please be aware that what follows is an open debate. I examine various points-of-views and evaluate them based on my knowledge. You can disagree with me and even correct me in the comments below.
- Session-based authentication
- Token-based authentication
- JWT-based authentication
- Stateless CSRF
- Should I go stateless?
- Reference material
Stateless vs stateful
Let’s weight some of the pros and cons of JWTs and compare them against stateful solutions.
Tokens can get big pretty quick. That’s kind of obvious: in stateful security the data is stored on the server, while in stateless the data is carried by your token, including the header and the signature! So you might want to be careful on the amount of claims you declare in a JWT’s body, knowing that tokens are sent back and forth between the client and the API server. With that in mind, many apps will probably just need the user’s username and access level, so maybe the price to pay in bandwidth is worth the memory spared on the server side.
Remember that your tokens are NOT encrypted, so claims can be read. Never store anything confidential in JWTs, unless you also use JWE (encryption). But then encrypting and decrypting tokens can quickly become CPU-intensive. On that aspect, sessions are safer indeed because the data itself is strictly stored on the server side.
One argument I once read is that, since most operations performed require the logged user’s information, one ends up hitting a database or a cache to fetch those user details anyway. So you lose the benefits of statelessness, because you end up replacing a session object with a call to the database. However most of the projects I have worked on lately use the bare essentials (username and access level) to identify a user and avoid putting too much data in session objects to prevent overloading the server’s memory. As a consequence, they end up hitting that DB no matter what!
Any way I go, I would try to keep authentication data small to avoid eating too much memory or too much bandwidth.
Disabling and expiring
Like cookies, JWTs can be set to expire. The tricky part here is to determine how long should JWTs live, because you cannot logout someone as long as he has a valid token!
What if you need to disable a user? Again, in “stateless land”, a user can keep on accessing your application as long as his token is not expired. As such, setting a lifetime of two weeks is probably not a good idea, unless you constantly check whether the JWT is part of a list of revoked tokens. But then you’re implementing a kind of state…
So let’s make our JWT expire in 30 minutes then. Fine, but now you either have the user typing his password every 30 minutes, or you have to implement some sort of logic to re-authenticate a user automatically (issue a new access token? Use refresh tokens?).
With server-side sessions instead, disabling a user can be done immediately by asking the server to invalidate that session.
Scalability and SSO
One aspect where stateless authentication shines is when working against a cluster of servers. When using server-side sessions, you need to use a load balancer and sticky sessions to always redirects the logged user to the server that holds his session ID, otherwise the load balancer might decide to redirect a request to a server with less traffic and -poof!- suddenly the user is not authenticated anymore. With tokens we do not need to store a state (a session) on a server anymore. So whether the load balancer sends a request to server A or server B doesn’t matter: both servers can validate the token sent and identify who’s making the request.
Also noticeable, JWTs make it possible to leverage single sign-on relatively easily… if you need it.
Web and mobile apps
Your RESTful API will likely be accessed by a browser, but possibly also from native or hybrid mobile applications! Good news: it’s worth pointing out that mobile apps can cope with both cookies or JWT tokens, so no worries about that.
Are stateless tokens more or less secure than sessions?
There’s a couple of blog posts that made the rounds a while ago. Their conclusions were that JWTs were no good for replacing sessions. Although I agree with much of their argumentation about the assumptions made about JWTs, I would like to try and bring some nuance about token-based security. Here more than ever, feel free to contradict me in the comments!
Storing tokens vs storing cookies
After authenticating, where shall we store our token? Normally you would store your JWTs in session storage, as advised in OWASP’s JWT Cheat Sheet. This way, the token is maintained even on a refresh, but cleared as soon as the browser is closed.
Now, when working with session-based security, your session ID comes as a cookie that is probably set as “secure” (the cookie is only sent on HTTPS) and “HttpOnly” (the cookie cannot be read by the browser, only sent back at each request). However with token-based security, using session as a storage mechanism, you cannot leverage “secure” and “HttpOnly“. As it is, your tokens are vulnerable to XSS because an attacker injecting a script would be able to read it. However you are safe from CSRF attacks.
Would it be better if you used session IDs or tokens sent as “secure” and “HttpOnly” cookies by the server?
Well, not quite. If your site is hacked by XSS, the malevolent script gets the same level of control than the web application the browser is running. It might not be able to read your cookie content, BUT it will be able to send it along with a request, effectively disabling CSRF protection to impersonate the user!
So what about CSRF attacks?
You should protect yourself against those if your store your token in a cookie AND your API expects to receive it as such. In that scenario, the server emits a token as a “secure” and “HttpOnly” cookie which will be stored by the browser and be unreadable from scripts. However it is vulnerable to cross-site request forgery! Hopefully you can apply stateless CSRF protection using the double submit cookie approach. I refer you to my previous post regarding this.
Alas, that also means that the token can be stolen with an XSS attack! But the good part is that a CSRF attack won’t work because it can only re-send that cookie, but cannot copy the token it contains into the header where the server expects it in a request. And so the server would simply block the access.
Watch out: usually session storage polyfills fall back to cookies if the browser does not support session storage, so it’s worth implementing that stateless CSRF attack protection just to make sure!
JWTs, like session IDs, are vulnerable to replay attacks (token side-jacking): intercepting a token and using it to impersonate a user. This can be mitigated by using short expiration times, but it’s not enough to prevent them.
Ideally one could fingerprint the user’s browser and send it as a hash when first authenticating. The returned JWT token would include that hash in its claims. The server can then compare at each request the fingerprint hash extracted from the JWT with the one that was used at authentication: if they differ, the JWT is rejected.
That’s all nice and well, but how can the server remember the browser fingerprint’s hash that was used at authentication? In a stateful context, it can store it in the session. But in a stateless context? Well, then the browser has to send its fingerprint at every request. But if the token can be intercepted, then so can the browser fingerprint!
So indeed: on this aspect, sessions have the upper hand because they store the original browser fingerprint on the server side. Except…
Except that Europe requires your explicit consent to allow fingerprinting your browser! You are legally obliged to inform the user that your site will “profile your browser”! Not sure everyone will like that…
Possibly the best protection against these attacks is to simply stick with HTTPS!
Stealing tokens and session IDs
Talking about replay attacks… To steal a JWT token or a session ID one would either perform a successful XSS attack (in which case we are pwned because the attacker can essentially do whatever the user can do) or perform a man-in-the-middle attack (MITM. Think “breaking HTTPS” or “breaching the server” to eavesdrop on everything that goes through).
To mitigate replay attacks, barred the browser fingerprinting technique (which won’t really work out in a stateless context), one needs to make token lifetimes short. Great, but now we either have to deal with refresh tokens or we are sending the user’s password on the wire on a more regular basis. If a MITM attack is ongoing then our attacker now has your refresh token, or even worse: your password… Damned!
So are you safer with session-based authentication? Well, nothing prevents the attacker from using an intercepted session ID to perform operations on the user’s behalf. And as for passwords: if that MITM breach has been going on for a while, your password has likely been intercepted too.
So all in all, tokens and sessions are both vulnerable on this level. At least you can mitigate XSS attacks by reading AND applying OWASP’s Cheat Sheet against XSS.
Securing your authentication data
What happens after you authenticate? In a stateful system, a session is stored by the server and you are linked to that session with a session ID. In a stateless context, your data is in plain view in a JWT. But it is signed, so we can assume that its claims are true since we verify its signature at each API request.
However JWTs are only secure as long as their signing key is secure! Anyone getting to your server and stealing the key would now be able to generate and sign any JWT he wants. That’s no good, obviously!
So we have to ensure our signing keys are safe. We can avoid storing our signing key in full on the server’s file system, in case a breach occurs. And we can set up a system of keys rotation to mitigate any leaks. Even better, to quote Joseph T. Lapp’s excellent post: “Only implement authentication that signs JSON Web Tokens if you also compartmentalize access to the signing keys“. In other words: never make the whole key accessible by the same user. His advice is to split the key in two and, for example, store one half in the file system and one half in the database.
Even better: what about third-party authentication servers such as Okta or Auth0? These guys probably go to extraordinary lengths to protect their systems, so one can feel safe about their signing keys, … unless they are subpoenaed, of course.
So, should I go stateless ?
Token-based authentication with JWTs is new and trendy, but requires quite some extra effort to keep everything secure… even when delegating part of the hard work to third-party authentication services.
Session-based authentication is old and not as sexy, but it’s a proven technology!
If you want to go for the advantages of stateless authentication then keep in mind that it requires more effort to make it safe enough. On the other hand, stateful authentication has never been easier thanks to frameworks such as Spring Security or Shiro.
So… my conclusion?
If you’re wondering about whether you should go stateless… then you’re probably safer with ye ol’ trusted session-based security. Stay with the reliable frameworks.
However, if you need to deal with multiple machines serving your API requests, then JWTs are a great choice for stateless security. But when making that choice, you must assume that your data is potentially more vulnerable. If that data is sensible then you might want to stay with the proven, stateful solution.
Then again, feel free to challenge my opinions. That’s what IT security is all about: keep on learning and never stop questioning one’s conclusions!
PS: many thanks to Emad Heydari Beni from KULeuven for proof-reading parts of this post and for the additional info he shared on the subject.