0%
Reading Settings
Font Size
18px
Line Height
1.5
Letter Spacing
0.01em
Font Family
Table of contents
    blog cover

    Access Tokens and Refresh Tokens: Why Do We Need Both?

    Software Engineer
    Software Engineer
    Interview
    Interview
    published 2026-09-15 20:29:54 +0700 ·
    5 mins read
    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.
    // 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
         ↓
        API

    So 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 Settings → Security → Logged-in devices and click Revoke MacBook. The server marks that session as revoked. MacBook cannot use this token to obtain a new access token anymore. The iPhone session continues working.
    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 jti, and when the user logs out, the server stores that jti in Redis until the token expires. On every request, the API checks both the JWT and the denylist. Another approach is a session/version check, where the token contains a session version and the server rejects it when the user's current version no longer matches. The trade-off is that the API is no longer completely stateless because it needs a server-side lookup.

    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 localStorage. If your application has an XSS vulnerability:
    // language: javascript
    XSS
     ↓
    JavaScript
     ↓
    localStorage.getItem("access_token")
     ↓
    attacker gets token

    OWASP explicitly recommends not storing authentication tokens, session identifiers, JWTs, or refresh tokens in localStorage/sessionStorage, because JavaScript running in the origin can access them. A common safer browser architecture is cookie with HttpOnly + Secure + SameSite. The browser stores the cookie, but JavaScript cannot read an HttpOnly cookie.
    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. HttpOnly protects against JavaScript reading the cookie; it does not magically make every cross-site request safe.
    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.