This article is published in English.
Refresh Token Strategy for Node.js Authentication Systems
Learn how to design, rotate, revoke, and securely store refresh tokens in Node.js so token theft and logout actually behave as expected.
When a Node.js project first gets a login flow wired up, it can feel like the job is basically finished within a short time.
The pattern seems simple enough: a user submits email and password, the server checks the credentials against the database, and if they match, a JWT gets signed and returned to the client. That token then rides along with every subsequent request. On the surface, this looks like a complete authentication system.
But a closer look reveals a long list of questions that this simple flow never answers:
- What happens once that token expires?
- Is the user expected to log in again from scratch each time?
- If a token gets stolen, how long is the attacker able to use it?
- How does logging a user out actually work in practice?
- Is there a way to revoke a token that has already been issued?
- What happens if a stolen token gets reused even after it was supposedly killed off?
- Where should this token even be stored on the client?
None of these are answered by "sign a JWT and validate it later." This is usually the point where you have to move past a copy-pasted login tutorial and actually dig into how refresh tokens work.
Why access tokens don't last long
A short-lived access token isn't a default value someone forgot to change — it's a deliberate design choice.
Consider what happens if an access token stays valid for 30 days and it ends up in the wrong hands: the attacker now has a full month of access to that user's account. That's an unacceptable trade-off. This is why access tokens are typically scoped to minutes rather than weeks — a short lifespan shrinks the blast radius if the token ever leaks.
That design choice introduces its own problem, though. If a token expires every 15 minutes, does the user have to type their password back in every 15 minutes? Clearly that's not workable. Refresh tokens exist specifically to close that gap.
So what does a refresh token actually do?
At first glance a refresh token looks like just another token, but its role in the system is completely different from an access token's.
The flow generally works like this:
- The user logs in.
- The server returns both an access token and a refresh token.
- The access token is used to make API requests.
- Eventually the access token expires.
- The client sends the refresh token to a dedicated refresh endpoint.
- If the server validates it successfully, it issues a brand-new access token.
The concept worth internalizing here is that a refresh token is never meant to call your API endpoints directly. Its only job is to obtain a new access token. Once these two responsibilities are kept mentally separate, the rest of the design starts to click into place.
Access token vs. refresh token, side by side
An access token is used to hit protected APIs, while a refresh token is used only to obtain a new access token. An access token is short-lived; a refresh token is longer-lived. An access token gets sent on nearly every request, while a refresh token is used only occasionally. If an access token leaks it's bad, but if a refresh token leaks it's worse, since it can be used to keep minting new access tokens. An access token is checked on every API call, while a refresh token is checked only by dedicated refresh or session logic.
A commonly cited split is something like a 15-minute lifetime for the access token and 7 days for the refresh token, but there's nothing magic about those numbers. The right values depend entirely on what your application and your users can tolerate.
Why refresh tokens deserve more respect than a first glance suggests
A refresh token can effectively keep a session alive for as long as it remains valid. If an attacker gets hold of one, they don't just get a single window of access — they can keep generating fresh access tokens until that refresh token expires or gets revoked.
That reality changes how the token should be treated. It isn't just another piece of application data; it behaves more like a physical house key. Treating it that way means actually working through:
- expiration
- revocation
- rotation
- where and how it's stored
- how it's transported
- reuse detection
- what logout is actually supposed to do
- session management as a whole
This is typically the point where a seemingly straightforward JWT setup starts to reveal its weak spots.
Rotation: not letting one token live forever
A concept that reshapes how this system is designed is refresh token rotation. Rather than letting a single refresh token be reused indefinitely, the server issues a new one every time the current one is successfully used, and the old one is immediately retired.
Instead of one long-lived credential circulating forever, you end up with a chain of tokens, where each one replaces the last:
Token A gets used, which causes Token B to be issued while A is retired. Token B then gets used, causing Token C to be issued while B is retired. This pattern continues indefinitely, with only the most recent token in the chain ever being valid.
Why this actually helps
Consider a scenario where an attacker somehow obtains a copy of Token A while the legitimate user still holds it too.
If the real user happens to use it first, Token A becomes marked as used and effectively dead, and Token B gets issued in its place. Now, when the attacker tries to use that same Token A later, the server recognizes it as a token that has already been consumed, which is an obvious red flag. Depending on how strict the system is configured, that detection can trigger revocation of the entire chain of tokens tied to that session, not merely the single compromised one.
This puts you in a far stronger position than a setup where whoever grabs the token first effectively wins control forever.
Revocation, because logging out should actually mean something
JWTs are frequently described as stateless, and in a technical sense that's accurate. But any real-world system needs at least some state, and logout is the clearest place where that need shows up.
If a user clicks logout and all that happens is the token gets deleted from the frontend, the token itself remains perfectly valid until it naturally expires on its own. The server has no awareness that the user "logged out" in any meaningful sense, so it will keep accepting that same token if it gets presented again before its expiration.
Refresh tokens give you an actual mechanism for tracking and killing sessions from the server side. Before a logout event, a session's refresh token sits in an active state. Once logout happens, that token should be flipped to revoked, and any subsequent attempt to use it for refreshing should fail outright. That's what a genuine logout looks like, rather than one that's purely cosmetic.
Logout belongs on the backend, not only the frontend
The simplified version of logout that beginners often implement is just deleting the token from local storage or wherever the client happens to keep it, then moving on.
A more complete and honest logout flow tends to follow a sequence closer to this:
- The client sends a
POST /auth/logoutrequest - The server identifies which session that request corresponds to
- The server revokes the refresh token or session tied to it
- Only then does the client clear its own local state
The key point worth emphasizing here is that logout is fundamentally a server-side security event. If your logout implementation only touches what's stored on the client, you haven't actually logged the user out at all, you've just made your interface forget that user existed.
Deciding where this token should actually be stored
Storage choices carry more weight than they seem to at first. For browser-based applications, a common approach is placing the refresh token inside a cookie configured with a few specific attributes:
HttpOnly, which prevents client-side JavaScript from reading it directlySecure, which restricts it to traveling only over HTTPSSameSite, which reduces exposure to certain categories of cross-site attacks
None of those settings, however, make a cookie automatically invulnerable. You still need to account for CSRF protections, how the domain and path are scoped, how session expiration is handled, and how logout interacts with all of these pieces together. There isn't a single storage pattern you can copy from a tutorial and trust without adapting it to your own system's architecture.
What happens if the refresh token leaks anyway
This particular scenario is what made the value of rotation click into place.
If both the legitimate user and an attacker somehow end up in possession of the same refresh token, and your system allows that token to be reused without limit, there's genuinely no way to distinguish between the two parties. From the server's perspective, both requests look equally legitimate.
With rotation in place, the first use of that token retires it immediately. So if the same token gets presented a second time, that's abnormal behavior and functions as a signal. A properly designed system can treat that repeat usage as a red flag and react accordingly, whether that means revoking the session, flagging the account, or applying whatever policy fits your risk tolerance.
This is really the core reason refresh tokens should be treated as a security design challenge, rather than something you solve by simply generating another JWT.
Refresh tokens also need to expire
It's easy to overlook, but refresh tokens shouldn't be permanent either. Without an expiration, a stolen refresh token effectively becomes a backdoor that never closes.
A common starting point is something like 15 minutes for access tokens paired with 7 days for refresh tokens, though the exact numbers you choose should reflect your own risk tolerance rather than whatever figures happen to appear in the first tutorial you come across.
It's worth keeping a clear mental separation between token lifetime and session lifetime, since they aren't the same concept. A session can remain active for a long stretch of time through repeated rotation cycles, even though each individual token involved only lives for a short window.
Mistakes worth unlearning or watching for
- Access tokens that live too long, which increases the damage if one gets exposed
- Refresh tokens with no expiration at all, leaving a permanent way in
- Skipping rotation entirely, which makes theft much harder to spot
- Having no revocation mechanism, leaving no way to kill a session before its natural expiry
- Treating logout as something that only happens on the frontend
- Being careless with secrets, letting tokens end up in logs, URLs, or client-side storage where they shouldn't be
- Ignoring reuse detection, since rotating tokens without checking for reuse doesn't actually gain you much protection
Testing the refresh flow for real
Authentication deserves just as much test coverage as any other critical path in your app. Here's a basic sequence worth running through:
- Log in and confirm you get back both an access token and a refresh token
- Call a protected route with a valid access token and confirm you get a
200 OK - Call a protected route with an expired access token and confirm you get a
401 - Call the refresh endpoint and confirm you receive a new access token (plus a new refresh token, if rotation is enabled)
- Try reusing the old refresh token that was already rotated out, and confirm it's rejected
- Log out, then attempt to refresh using that now-dead session, and confirm it's rejected too
Catching problems at this stage is far cheaper than discovering them after the app is live.
The full picture
Login
↓
Access Token + Refresh Token issued
↓
API requests using Access Token
↓
Access Token expires
↓
Refresh Token sent to refresh endpoint
↓
Server validates session
↓
Refresh Token rotated
↓
New Access Token issued
↓
API requests continue
If validation fails at any point in that refresh sequence — the token is expired, revoked, or simply invalid — the response is a 401, and the user has to log back in from the beginning. That separation between "short-lived permission to call the API" and "longer-lived permission to stay logged in" is really the core idea behind this whole setup, distilled into one sentence.
A pre-production checklist
Token design
- Do access tokens actually expire quickly?
- Do refresh tokens carry their own expiration?
- Are refresh tokens restricted to the refresh endpoint only, rather than usable against arbitrary routes?
- Do token payloads avoid carrying more information than necessary?
Security
- Is HTTPS required across the board?
- Are refresh tokens stored somewhere safe?
- Are secrets kept separate from your codebase?
- Are tokens scrubbed from logs?
- Is CSRF protection applied wherever it's relevant?
Session handling
- Can you revoke a refresh token on demand?
- Does logout happen on the server, not just the client?
- Is token rotation actually implemented?
- Can you detect when a token gets reused?
Test coverage
- A valid refresh succeeds
- An expired refresh token fails
- A revoked refresh token fails
- A malformed or invalid refresh token fails
- A previously rotated-out token fails
- Logging out correctly kills the right session
What this all boils down to
Authentication isn't just about confirming someone's identity at the moment they sign in. It's about deciding — and then actually enforcing — how long that trust should hold afterward.
JWTs make the "verify who you are" part simple. What they don't give you for free is session management, revocation, proper logout, protection against stolen tokens, reuse detection, safe storage, or reasonable expiration windows. Every one of those is a deliberate choice you have to make yourself. The bigger and more used your application becomes, the more those choices actually matter.
What comes next
With access and refresh tokens finally making sense, the next area worth digging into is session management at a larger scale:
- How should a user staying logged in across five different devices be handled?
- Should users be able to view — and end — their own active sessions?
- Is it possible to log someone out of one device without killing every session they have?
- What does a "suspicious" session look like, and how do you flag it?
- What's the right way to store refresh-token families?
- At what point does a database-backed session model make more sense than staying fully stateless with JWTs?
Writing the login route itself might take ten lines of code. Building authentication you can genuinely trust takes considerably more effort than that.