Why JWT Requires Both Access and Refresh Tokens Instead of a Single Token

The article explains the inherent trade‑off of a single JWT’s expiration time, shows how using short‑lived Access Tokens together with long‑lived Refresh Tokens resolves both security and user‑experience issues, and provides detailed backend and frontend implementation guidance.

Programmer XiaoFu
Programmer XiaoFu
Programmer XiaoFu
Why JWT Requires Both Access and Refresh Tokens Instead of a Single Token

Problems with a Single Token

Using only one JWT for authentication forces a compromise on its expiration time: a short lifespan (e.g., 30 minutes) leads to poor user experience when a token expires during a long form submission, while a long lifespan (e.g., 7 days) creates a high security risk because a stolen token can be used for the entire period and cannot be revoked without re‑introducing server‑side state or a blacklist, which defeats JWT’s stateless advantage.

How Dual Tokens Solve the Issue

The solution splits responsibilities into two tokens:

Access Token : short‑lived (typically 15‑30 minutes), sent with every API request. Even if intercepted, its usable window is minimal.

Refresh Token : long‑lived (typically 7‑30 days), used only to obtain a new Access Token after the latter expires. It never accesses business APIs directly.

The overall flow is illustrated below:

Why Refresh Tokens Are Safer

Low transmission frequency : Access Tokens are sent on every request (hundreds to thousands of times per day), whereas Refresh Tokens are transmitted only when the Access Token expires, reducing exposure.

Different storage : Access Tokens are usually kept in memory or localStorage for quick access, while Refresh Tokens can be stored in an HttpOnly Cookie, making them inaccessible to JavaScript and immune to XSS theft.

Stricter validation : On a refresh request the server can verify device fingerprints or IP addresses, a cost that would be prohibitive if performed on every API call.

Refresh Token rotation : Each successful refresh issues a new Refresh Token and invalidates the old one, allowing immediate detection of theft.

Backend Core Logic

Login endpoint generates both tokens. The Access Token is a JWT with a short TTL; the Refresh Token is a random UUID stored in Redis with a long TTL (e.g., 7 days). Example code:

public LoginResponse login(String username, String password) {
    // validate credentials …
    String accessToken = jwtUtil.generateToken(userId, Duration.ofMinutes(30));
    String refreshToken = UUID.randomUUID().toString();
    // store Refresh Token in Redis
    redis.opsForValue().set("refresh:" + refreshToken, userId, 7, TimeUnit.DAYS);
    return new LoginResponse(accessToken, refreshToken);
}

The refresh endpoint validates the Refresh Token from Redis, generates a new Access Token, rotates the Refresh Token, and returns both:

public TokenResponse refresh(String refreshToken) {
    String userId = redis.opsForValue().get("refresh:" + refreshToken);
    if (userId == null) {
        throw new AuthException("Refresh Token expired, please log in again");
    }
    String newAccessToken = jwtUtil.generateToken(userId, Duration.ofMinutes(30));
    // rotate Refresh Token
    redis.delete("refresh:" + refreshToken);
    String newRefreshToken = UUID.randomUUID().toString();
    redis.opsForValue().set("refresh:" + newRefreshToken, userId, 7, TimeUnit.DAYS);
    return new TokenResponse(newAccessToken, newRefreshToken);
}

Refresh Token rotation is a recommended OAuth 2.0 security best practice.

Frontend Implementation

The client intercepts 401 responses, triggers a single refresh request, and queues other failed requests until a new Access Token is obtained. Key flags are isRefreshing and pendingRequests:

let isRefreshing = false;
let pendingRequests = [];
axios.interceptors.response.use(response => response, async error => {
    const originalRequest = error.config;
    if (error.response?.status === 401 && !originalRequest._retry) {
        if (isRefreshing) {
            return new Promise(resolve => {
                pendingRequests.push(token => {
                    originalRequest.headers.Authorization = 'Bearer ' + token;
                    resolve(axios(originalRequest));
                });
            });
        }
        originalRequest._retry = true;
        isRefreshing = true;
        try {
            const { data } = await axios.post('/auth/refresh', { refreshToken: getRefreshToken() });
            setAccessToken(data.accessToken);
            setRefreshToken(data.refreshToken);
            pendingRequests.forEach(cb => cb(data.accessToken));
            pendingRequests = [];
            originalRequest.headers.Authorization = 'Bearer ' + data.accessToken;
            return axios(originalRequest);
        } catch (e) {
            clearTokens();
            window.location.href = '/login';
            return Promise.reject(e);
        } finally {
            isRefreshing = false;
        }
    }
    return Promise.reject(error);
});

This ensures only the first 401 triggers a refresh; subsequent requests wait for the new token, avoiding concurrent refreshes.

When Dual Tokens Are Unnecessary

Small internal admin panels with a handful of users where a 2‑hour token expiry is acceptable.

Short‑lived H5 promotional pages where the engineering effort outweighs benefits.

Systems already using stateful sessions (e.g., Redis‑backed Session IDs); the added complexity of JWT does not provide extra value.

Ideal scenarios for the dual‑token approach are large‑scale, stateless JWT authentication with high security and seamless user experience requirements, such as mobile apps, SaaS platforms, and open‑API services.

Final Takeaway

Separating the responsibilities of Access Tokens (business API access) and Refresh Tokens (token renewal) allows both security and user experience to be optimized without sacrificing JWT’s stateless nature.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

securityAuthenticationJWTOAuth 2.0Access TokenRefresh TokenToken Refresh
Programmer XiaoFu
Written by

Programmer XiaoFu

xiaofucode.com – a programmer learning guide driven by the pursuit of profit

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.