I used to think authentication was simple. You log in, the server gives you a JWT, and you attach that JWT to every request. Something like: Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Then the obvious question appears: Why not just make that token live for 30 days?
If the user needs to stay logged in for 30 days, a 30-day token seems much simpler than having both an access token and a refresh token.
The answer is that authentication is not only about staying logged in. It's also about limiting the damage when credentials are stolen. This post walks through the common access-token/refresh-token flow, why two tokens exist, where to store them, how logout actually works, and interview questions that test whether you really understand the design.
1. The common authentication flow
Let's start with a typical setup:
- Access token: 15 minutes
- Refresh token: 30 days
The user logs in:
// language: javascript
POST /login
{
"email": "[email protected]",
"password": "..."
}The server responds:
// language: javascript
{
"access_token": "...",
"refresh_token": "...",
"expires_in": 900
}
The client uses the access token for API requests:
// language: javascript GET /api/orders Authorization: Bearer <access_token>
The API validates the access token and returns the data. This continues until the access token expires. When that happens:
// language: javascript
access token expired
↓
POST /auth/refresh
↓
refresh token
↓
new access token
The user doesn't have to log in again. This is essentially the model described by OAuth 2.0: an access token is used to access protected resources, while a refresh token is used to obtain a new access token. Refresh tokens are intended for the authorization server rather than the resource server. Reference
2. Why don't we just use one token?
Suppose we use only one JWT with expiration = 30 days. Every request contains it: GET /profile , GET /orders , POST /payment , DELETE /account , ... Now imagine an attacker steals it. The attacker has a 30-day API credential. They don't need your password. They don't need to refresh anything. They can simply call the API.
This is the problem with making the credential used for every API request long-lived. So instead, we separate the responsibilities.
This is the problem with making the credential used for every API request long-lived. So instead, we separate the responsibilities.
// language: javascript
Access token
↓
Used frequently
↓
Short lifetime
Refresh token
↓
Used rarely
↓
Longer lifetime
Now, if an access code is stolen, an attacker can use it during the remaining short window of time.
3. But isn't a stolen refresh token just as bad?
Yes. This is the part that is easy to miss. Suppose:
- Access token: 15 minutes
- Refresh token: 30 days
An attacker steals the refresh token. They can do:
// language: javascript
refresh token
↓
POST /refresh
↓
new access token
↓
APISo a refresh token is actually more sensitive than an access token. OAuth security guidance explicitly treats refresh-token leakage as a serious risk because a stolen refresh token can potentially be used to continuously obtain new access tokens. Modern guidance recommends refresh-token rotation or sender-constrained refresh tokens, together with a bounded refresh-token lifetime.
So why have refresh tokens at all?
Because they allow us to make the credential exposed to the entire API short-lived. It's a trade-off, not magic security.
4. Refresh Token Rotation Technique
Instead of reusing the same refresh token, the server issues a new refresh token every time the client refreshes its access token.
// language: javascript R1 → R2 → R3 → R4
The server invalidates the previous token after it's used. This limits the impact of a stolen refresh token and helps the server detect token reuse, which can indicate that the token has been compromised. For example, if an attacker tries to reuse R2 after the legitimate client has already exchanged it for R3, the server can detect the reuse and revoke the session.
Rotation does not make refresh tokens risk-free, but it makes stolen-token attacks easier to detect and contain.
5. Refresh tokens give us a login/session list
Imagine I log in from: 📱 iPhone, 💻 MacBook, 🌐 Chrome, we can maintain:
// language: javascript User: Alice Sessions ────────────────────────────── iPhone Active MacBook Active Chrome Active
Each login creates a refresh-token session. For example:
// language: javascript session_id = 123 user_id = 42 refresh_token_hash = ... device = "iPhone" last_used_at = ... revoked_at = NULL
Now the user can see
This is one of the biggest practical advantages of treating refresh tokens as server-managed sessions.
6. What happens when the user logs out?
This is another interview trap. People often say:
"We delete the JWT."
But if the access token is a self-contained JWT, the server may not have anything to delete. The JWT already exists on the client.
If it has exp = 10:05 and the user logs out at 10:01 , the token could technically remain valid until 10:05 .
That's why short-lived access tokens are useful. On logout, we can revoke the refresh token. The existing access token may remain valid until it expires. For applications that require immediate revocation, you need additional server-side state, such as a token denylist or session/version check.
If we need to revoke an access token immediately, we need some server-side state. One common approach is a token denylist: each access token has a unique
For a highly sensitive operation, don't rely only on token expiration. Require additional authorization or re-authentication where appropriate.
7. Where should we store the tokens?
This is where things become browser-specific. There isn't one answer for every client.
Browser application
The dangerous choice is:
// language: javascript
localStorage.setItem("access_token", token)Why?
Because JavaScript can read
// language: javascript
XSS
↓
JavaScript
↓
localStorage.getItem("access_token")
↓
attacker gets tokenOWASP explicitly recommends not storing authentication tokens, session identifiers, JWTs, or refresh tokens in
For example:
// language: javascript Set-Cookie: refresh_token=...; HttpOnly; Secure; SameSite=Lax;
Now, JavaScript cannot read the refresh token, while the browser automatically sends cookie. You still need to think about CSRF when using cookies.
You can read my post about this topic here CORS and CSRF: How Attackers Exploit the Gaps
Mobile application
On iOS/Android, you have platform secure storage. For example, Keychain on iOS or Keystore on Android. The general idea is that tokens are stored in OS-protected secure storage rather than putting credentials into an ordinary application database or plain preferences. The exact implementation depends on the platform and framework.
The Mental Model
After all of this, I think the simplest way to remember the design is this:
- The access token is optimized for fast authorization.
- The refresh token is optimized for maintaining a login session.
- The server-side refresh session is optimized for control: revoke, logout, device management, rotation, and replay detection
And that leads to the most important takeaway:
Access tokens are about accessing resources. Refresh tokens are about maintaining authorization over time.
Once you see them as two different responsibilities, the whole design becomes much easier to reason about.
.png)
.png)

