How to securely store JWT tokens

Last time I worked with some JWT tokens and had to resolve some issues related to security. Right now a lot of web applications and not only, because also mobile apps use these tokens to prove that we are valid and correctly logged-in users. The idea of JWT is clear, right and useful, but there are some dangerous traps during implementation on web applications. The main question is: where should we store JWT tokens to make them secure and still usable?

localStorage and sessionStorage are not good

On mobile clients, it is not a problem, because we can save them in some secure area. The thing is a bit complicated with web applications. If you already thought about this question and looked for some solutions, you probably found a lot of articles about that and some of them recommend using localStorage. During the latest years web developers got a lot of new abilities and localStorage is one of them: simple and very quick to use, it’s just something like:

// Write
localStorage.setItem('foo', 'bar')

// Read
cost myValue = localStorage.get('foo')

The big additional advantage: it is persistent storage, so even if the user closes the browser and opens again, it will still be logged in. Great! So, why not, why not use it just to store our JWT token? The problem with this approach is the possibility of stealing a token using an XSS attack. XSS is an injection and execution of malicious code on a user web browser. If there is any possibility that someone will do that on your web application, then it will be possible to just execute code to steal a JWT token – because it is accessible from JavaScript level. 

Of course, if we allow anyone to make an XSS attack, we have bigger problems… but think about that: in a big, complex web application, can you guarantee that there is no such possibility? We can not think in such a way and just assume that everything will be ok. The better approach is to think that everything will be bad. It is a reason why we do not have to use localStorage for JWT tokens or any sensitive data. 

What about sessionStorage? It is very similar: provides a quick, easy way to store some data, but it is not persistent as localStorage. If the user open a new tab or just close and reopen the browser, there will be no data we saved. It may be good for let say banking web applications, but for most others, it will be a very bad user experience. Also, sessionStorage is still vulnerable to XSS attacks. Maybe store them just in memory? It may be a solution, but you will have additional issues with persistence, and it will be very difficult to resolve. Conclusion: such storage are not a good option. 

Solution: use old school cookies

Yep, cookies come to rescue. Many people think that is old school, not used anymore. But in reality, they are still very good options in such cases. Why? First: they offer persistence. When we set cookies, we can also control expiration time. They are available in next tabs with the same web application, also after reloading and browser restart. Second: if configured correctly, they are not vulnerable for XSS attack. We can set the httpOnly flag on the cookie and after that it will not be visible from JavaScript level anymore. Wait a moment… you can ask: if it will not be available, how can we use a token from such a cookie in the authorization header?

The answer is: it will not be possible, but it is not a problem, because browsers will attach these cookies in each request and your API should just check cookies. In reality, cookies are also header, just different and process is automatic, browser will do that for you without any additional code. So, if the user has been authenticated and the API has set a proper cookie with JWT token, it will be automatically sent back, even in background (XHR, fetch) requests. So your frontend application does not have to handle that anymore.

Third: if configured correctly, it will be limited to only secure connections. Right now, all apps with public access should use secure, encrypted connections. Using a secure flag you can define that cookie should be used only on such connections, so if anyone will try to use your application on standard HTTP, such cookie will be not available and not used. Of course, you should still use different methods to provide HTTPS-only traffic: upgrade / rewrite and also send HSTS headers to inform browsers about rules for your websites. 

What about CSRF/XSRF issues?

We decided to move all things to cookies, our JWT token is not vulnerable to XSS attack, and we are happy… But just for a moment, because we remembered about different attack surfaces related to cookies: CSRF. Our API sets cookies and it looks fine, but an attacker can right now just prepare a special website and command our clients to send unauthorized requests in the background. Browsers will automatically add cookies in such requests as mentioned above, so it looks like the biggest advantage is also the biggest drawback. What can you do in such a situation? The first thing is to provide strict CSRF protection: send a special token from API to the browser, then attach this token to requests and validate on API: it guarantees that only the real client (i.e. our web application) makes all requests. The problem with that option is maybe we will need to change a lot of places in our code, also handling background requests will be not easy. 

Fortunately, there is a second solution, much simpler. We can add another flag to our cookies: sameSite. This option allows us to block sending cookies cross-site. Default option is “None” and it does not block anything, cross-site is possible (also with CSRF possibility). The second option is “Lax” – with such configuration browsers will not send cookies for images and frames, so it will prevent CSRF attacks. If the user clicks the URL to our website on a completely different website (or email), cookies will be added, so everything will work correctly. The latest option is “Strict”: in that case cross-site cookies are completely blocked, the most secure, but in some cases a bit limited option.

Conclusion

Use cookies to store JWT tokens – always secure, always httpOnly, and with the proper same site flag. This configuration will secure your client’s data, it will prevent XSS and CSRF attack and also should simplify web application, because you do not have to care about using tokens manually on frontend code anymore. 

Set-Cookie: jwt=OUR_TOKEN_CONTENT; secure; httpOnly; sameSite=Lax;

What about refresh tokens?

Exactly the same thing – they are also super important because they allow users to generate new JWT. If you use this token to determine: is a user logged in or not, you can stop doing that. Just save such information in localStorage (simple bool) if you need. It is not a problem: if the token expires, your API will inform the frontend about unauthorized requests, and then you can call the proper service to refresh the token. If the refresh token is expired or invalid, this service will inform you about that, so you will know that the user should be redirected to the login page. Yep, all these things without using tokens manually, it will be completely transparent from web application level.

Authorization service on different domain

Bonus case: what if our authorization service is on a completely different domain or subdomain? Let’s say we use oauth.mydomain to display and handle login pages, but our users will use different websites like app.mydomain. In this scenario, cookies will not work correctly, so what can we do? There are two options. First is to move login into the app subdomain or just the main domain (then cookies will be available on all subdomains – you can still control that) – may in many cases it may be not possible or just very, very complicated because we want to support many systems. Second option: just provide a proper URL on your app and use it like a “middleware” to call authorization service, so it will be Web App ⇔ API ⇔ Authorization Service. If oauth respond with correct data, you can set proper cookies for your current domain.