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:
To catch both rabbits, we assigned different roles.
Access Token (Entry Pass)
Refresh Token (Re-issuance Ticket)
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
✅ HttpOnly Cookie
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:
// 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
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”