Where do I store auth tokens on client side?

Hey folks! In this article I am going to briefly address the topic of where to store our precious auth tokens on the client side.

So I have developed a web app using .NET framework and now I want to implement authentication in it. In order to do that, I have implemented token-based authentication using .NET Identity. How that works is like this -

  • The first time a user tries to access any page of the web app, they are directed to the login page where they provide their username and password.

  • Once they provide their credentials, a JWT oauth token is generated by our web server. Along with that, a refresh token is also generated, which allows us to generate a fresh access token once the current one expires.

  • As long as subsequent requests to our server contain the access token, the user can browse our web app without having to worry about entering credentials repeatedly.

To achieve this feat though, our app should be smart enough to do below tasks:

  1. It should safely store the access token and refresh token, either on the client side or the server side.

  2. For every request sent from our client-side code to our server, the access token should also be attached

  3. In case an access code has expired, a refresh request should be sent using the refresh token, to obtain a fresh access token. Thereafter, any further requests should use this new access token.

Now, taking care of points 2 and 3 is relatively easy. What is hard is implementing point #1.

- Where should we store the access and refresh tokens? On the server or the client?

Storing access token on the server side is not of much use. The token needs to be available to the client so that it can authenticate itself in subsequent requests. However, storing refresh token on both the client and the server is a common practice. On the client side, the refresh token is stored for the event when access token expiry occurs and new access token needs to be requested. On the server side, the same refresh token is saved, so that when a refresh request is incoming, the refresh token in the request can be cross checked with the refresh token saved in the server, before providing a fresh access token. It also allows the server to invalidate refresh tokens in case the user id has been compromised.

- What is the safest approach to store the tokens on the client side?

There is no 100% secure way of storing the tokens on the client system. Let's visit a few approaches one by one, in increasing order of their increased safety level.

The most unsafe approach is to embed the tokens in the html page itself, most probably in a couple of hidden fields. Although a non-technical user will not see these tokens on the web page, any developer or malicious attacker can easily open the source code of the page and locate them. Once they get hold of these tokens, the user's id is compromised. If the attack is caught early on, the user may have a chance to change their passwords and revoke all refresh tokens. But depending on the speed and target of the attack, the user may not even get a chance to do this. For example, if the tokens were being used for accessing a bank or email account, the attacker may have taken control of these accounts already by the time the user becomes aware of the attack. The attacker can also share these tokens with others freely.

Another relatively un-safe approach is to save the tokens in localStorage/SessionStorage. However, Javascript can easily access and read values stored in this storage, thus making this approach vulnerable to XSS attacks. Here, Cross-Site Scripting (XSS) attacks are a type of injection, in which malicious scripts are injected into trusted websites because of a vulnerability in those websites that allows such an injection. So an attacker may inject a javascript code into your trusted website that can read all the values stored in localStorage or SessionStorage, including tokens, and send them over to the attacker's server. So unless you can 100% guarantee that your web app is completely safe from XSS attack, this approach is not ideal.

A relatively safe approach is to store the tokens in a Cookie with httpOnly, secure and SameSite=strict flags. An httpOnly cookie is a tag added to a browser cookie that prevents client-side scripts from accessing data. Only a server can read such a cookie. With the secure flag, the cookie will only be sent over HTTPS. Adding the SameSite=strict flag specifies that the cookies will only be sent in a first-party context and not be sent along with requests initiated by third party websites (mostly ad tracking websites). With the SameSite=Strict value, the browser prevents cookie data from being transferred during cross-domain requests, thereby making our cookies more secure. However, even this approach has a pitfall that if our web app has an XSS vulnerability, an attacker can inject a script which can send requests to our server, and even though the script cannot access the tokens, the browser automatically sends the tokens to our server, as it does during any legitimate form submit action. So basically, an XSS script can allow attacker to continue sending requests using the tokens stored in cookie. However, the attacker cannot read this token so cannot send it to any other machine, thereby limiting the impact of a compromised cookie to one machine. There is also the inherent CSRF attack risk with any cookie, so sufficient protections should be put in place to handle CSRF attacks.

Below is more information on the various ways to store auth tokens on the client side in JavaScript:

  1. HttpOnly Cookies: (Already explained above) Storing tokens in HttpOnly cookies is a secure approach. HttpOnly cookies cannot be accessed by JavaScript, which helps mitigate the risk of cross-site scripting (XSS) attacks. This method is particularly suitable when your token is used for authentication against a server.

  2. Session Storage: (Already explained above) Session Storage is a storage mechanism that persists data for the duration of the page session. It is not shared across tabs or windows and is more secure than Local Storage. However, it is vulnerable to XSS attacks if your site is compromised.

  3. Local Storage: (Already explained above) Local Storage allows you to store data on the client's browser with no expiration time. However, it is important to note that Local Storage is vulnerable to XSS attacks. Avoid storing sensitive information like tokens in Local Storage unless you have mechanisms in place to protect against such attacks.

  4. In-Memory: Storing tokens in JavaScript variables (in-memory) is secure against attacks like XSS since the data doesn't persist across page refreshes. However, it's not suitable if you need to maintain authentication across page refreshes or if you want the user to remain authenticated across different tabs.

  5. IndexedDB: IndexedDB is a low-level API for storing large amounts of structured data. It's more complex to use compared to other storage options but can provide a secure storage solution if implemented properly. However, this one is also vulnerable to XSS attacks.

In general, if you need to store bearer tokens on the client side, consider the following guidelines:

  • Always prioritize security. Avoid storing tokens in locations vulnerable to XSS attacks.

  • Use HttpOnly cookies when possible, as they provide a strong level of security against XSS.

  • If using client-side storage mechanisms like Session Storage or Local Storage, ensure that you have appropriate mechanisms in place to mitigate XSS risks (e.g., input validation, output encoding).

  • Use token expiration and refresh mechanisms to manage token validity and renewal.

Remember that tokens, especially long-lived ones, can potentially be stolen or abused if not managed carefully. Implement secure practices, and if you're unsure about the best approach for your specific application, consider consulting security experts or following best practices provided by reputable sources.