Back to Blog
SecurityJune 28, 20264 min read

JWT Security: Preventing Improper Refresh Token Rotation Risks

Master JWT security by implementing robust refresh token rotation. Learn how to prevent session hijacking and token reuse in Node.js and Laravel apps today.

JWTsecurityauthenticationnodejslaravelwebdevWebBackend

We’ve all been there: staring at a bug report where a user’s session seems to persist long after they’ve logged out, or worse, seeing suspicious activity logs that suggest a token was used from two different continents simultaneously. Managing session management correctly is the single biggest headache in modern authentication, and relying on static JWTs is a recipe for disaster.

If you are building an API, you need to implement refresh token rotation. It’s the only reliable way to detect when a stolen token is being replayed by an attacker.

Understanding the Token Reuse Problem

When a client uses a refresh token to get a new access token, the server should invalidate the old refresh token and issue a new pair. If an attacker manages to steal a refresh token, they’ll try to use it. If you don't rotate, the original user and the attacker can both use the same token until it expires.

We first tried simple blacklisting, but that failed because the database hit became a bottleneck during high traffic—we were seeing latency spikes of around 150ms just for token validation. We switched to a rotation strategy where the refresh token is marked as "used" in the database. If we ever see a "used" token being presented again, we know it’s a breach, and we invalidate the entire family of refresh tokens associated with that user.

Implementing JWT Security in Node.js

In a Node.js environment, typically using jsonwebtoken or jose, you need a stateful backend to track refresh tokens. Do not store them in memory; use Redis.

Here is a simplified flow of how I handle this in a production Express app:

  1. Client sends refresh_token to /auth/refresh.
  2. Server checks if the token exists in Redis.
  3. If it exists, check if it's already marked as used.
  4. If used is true, someone is replaying the token. Revoke all tokens for that sub (user ID) immediately.
  5. If valid, issue a new access_token and refresh_token, and delete the old one.

This approach aligns with the principles discussed in OAuth2 security: Preventing Refresh Token Rotation Issues, where the state of the token is the source of truth.

Token Rotation in Laravel

Laravel’s Sanctum or Passport handles a lot of this, but if you’re rolling a custom JWT implementation, you need to be careful with your database schema. I typically use a refresh_tokens table with an is_revoked boolean.

PHP
#6A9955">// Simple logic for checking reuse
$token = RefreshToken::where('token', $hashedToken)->first();

if ($token->is_revoked) {
    #6A9955">// SECURITY ALERT: Token reuse detected!
    $this->revokeAllUserTokens($token->user_id);
    return response()->json(['error' => 'Security breach detected'], 403);
}

The key is that the security of your app depends on this "detect and burn" strategy. If you aren't careful, you might accidentally lock out legitimate users due to network retries, so make sure your client-side implementation handles 403 responses gracefully by forcing a full re-authentication.

Comparing Session Management Strategies

StrategyComplexitySecurity LevelPerformance Impact
Static JWTLowLowNegligible
BlacklistingMediumMediumHigh (DB load)
RotationHighVery HighLow (Redis)
OIDC/PKCEHighExcellentLow

Why Refresh Token Rotation Matters

Proper JWT security goes beyond just signing tokens. As I’ve covered in JWT Security: Preventing Signature Bypass and Algorithm Confusion, the validation logic is critical, but session lifecycle is where most production breaches happen.

If you don't rotate, you are effectively leaving the door unlocked. If you're concerned about other vectors, ensure you've also addressed Preventing Session Hijacking: Secure Cookies and Fingerprinting, as no amount of rotation will save you if the token is leaked via an insecure cookie.

Frequently Asked Questions

Q: Does rotation increase database load? A: If you use Redis, the impact is minimal. The cost of a few key-value lookups is nothing compared to the security gains.

Q: What if the user loses their connection during the token swap? A: This is a classic edge case. I usually implement a small "grace period" (like 30 seconds) where the old token can still be used if the new one hasn't been acknowledged, though this slightly lowers security in exchange for UX stability.

Q: Should I store refresh tokens in LocalStorage? A: Never. Use HttpOnly and Secure cookies to mitigate XSS risks.

Final Thoughts

I’m still experimenting with rotating tokens on every single request versus just on refresh, but the overhead of the former is usually too high for most CRUD applications. The current "rotate on refresh" pattern is a solid middle ground. Don't treat token rotation as an optional feature; in any system where users care about their data, it's a fundamental requirement for secure session management.

Next time, I’d probably look into implementing DPoP (Demonstrating Proof-of-Possession) to bind tokens to the client’s private key, but that's a rabbit hole for another day.

Similar Posts