React|Optimization & Security

Access Token and Refresh Token: Safe Authentication Strategy between Security and UX

3
Access Token and Refresh Token: Safe Authentication Strategy between Security and UX

Chris chose the easiest path while implementing the login feature.

He saved the token received from the server in localStorage and retrieved it to send in the header with every API request.

// ❌ Chris's dangerous code
localStorage.setItem('token', 'secret-token-123');
// Anyone can steal it just by typing localStorage.getItem('token') in the console.

One day, during a security audit, he received a warning: "Vulnerable to XSS (Cross-Site Scripting) attacks." This means a hacker could inject JavaScript code and steal the user's token.

So, where should we hide the token? And why do we need to use two tokens?

Today, we dig into the Authentication Token Strategy, a tightrope walk between Security and User Experience (UX) that every frontend developer must know.

1. Why Split Tokens into Two?

The dilemma of authentication systems is as follows:

  • Security: Token expiration needs to be short. (Minimize damage if stolen)
  • UX: Don't make users log in frequently. (Annoying)
  • To catch both rabbits, we assigned different roles.

    Access Token (Entry Pass)

  • Role: The key used when requesting actual API data.
  • Lifespan: Very short (30 minutes ~ 1 hour).
  • Feature: Since it travels frequently, there is a risk of theft. We keep its lifespan short so it becomes useless quickly if stolen.
  • Refresh Token (Re-issuance Ticket)

  • Role: A certificate to exchange for a new Access Token when the old one expires.
  • Lifespan: Very long (2 weeks ~ 1 month).
  • Feature: Not used for API requests; used only for extending the login session. Must be stored in a very secure place.
  • 2. Where is Safe to Store? (Storage Wars)

    This is a highly debated topic. To give you the conclusion first: "Store the Refresh Token in an HttpOnly Cookie, and the Access Token in Memory (variable)." This is the safest approach.

    ❌ LocalStorage / SessionStorage

  • Pros: Easiest implementation in the world.
  • Cons: Accessible via JavaScript (window.localStorage). If an XSS attack succeeds, hackers can run off with the token.
  • ✅ HttpOnly Cookie

  • Pros: Inaccessible via JavaScript (invisible to document.cookie). Safe from XSS attacks.
  • Cons: Can be vulnerable to CSRF (Cross-Site Request Forgery) attacks, but this is defensible using the SameSite option.
  • 3. The Ideal Authentication Flow (Best Practice)

    Here is the secure login scenario Chris should adopt.

    1. Login Success

    The server sends two things in the response:

  • Body: accessToken (JSON data)
  • Header: Set-Cookie with refreshToken (HttpOnly, Secure, SameSite=Strict)
  • // Client receives only accessToken and stores it in a variable.
    let accessToken = response.data.accessToken; 
    // The browser automatically stores the refreshToken deep inside the cookies.

    2. API Request (Using Access Token)

    The client sends the request with the accessToken from memory in the Authorization header.

    headers: {
      Authorization: `Bearer ${accessToken}`
    }

    3. Token Expiration (401 Error)

    The Access Token's lifespan (30 mins) is up. The server sends a 401 Unauthorized error.

    At this point, you shouldn't log the user out. You must renew the token without the user knowing (Silent Refresh).

    4. Token Re-issuance (Refresh)

    The client sends a request to the /refresh endpoint.

    At this moment, the browser automatically includes the Refresh Token stored in the cookie. (We don't need to add it in code).

    The server verifies the cookie and issues a new Access Token.

    4. Securing the Details

    RTR (Refresh Token Rotation)

    What if even the Refresh Token gets stolen? To prevent this, we use RTR.

    It’s a method where a Refresh Token is discarded immediately after a single use, and a new one is issued.

    If a hacker tries to use a stolen Refresh Token? Since it's already used (or invalid), the server detects this and immediately blocks all tokens associated with that user.

    Solving the Refresh Problem

    If you store the Access Token in memory (variable), it disappears every time you refresh the page.

    Therefore, when the app initializes (Mount), you need a process that first pings the /refresh API to fetch a new Access Token.

    Key Takeaways

  • Access Token: Lives short. Used for API requests. Principle is to store in Memory (variable).
  • Refresh Token: Lives long. Used for renewal. Store in HttpOnly Cookie to block JavaScript access.
  • XSS Defense: Avoid using Local Storage.
  • UX: The token should be renewed automatically in the background so the user doesn't even know it expired.

  • The concept is perfect. But we can't write if (tokenExpired) refresh() code for every single API request.

    It is time to implement Interceptors, the gatekeepers that handle this complex renewal logic automatically in one place.

    Continuing in: “Token Renewal and Black/White Listing via Axios Interceptors”

    🔗 References

  • MDN - HttpOnly Cookie
  • SameSite Cookies Explained
  • OWASP - Session Management
  • Comments (0)

    0/1000 characters
    Loading comments...