DEV Community

Piyush Kumar Singh
Piyush Kumar Singh

Posted on Originally published at Medium

Your JWT lasts 7 days. Here’s What Happens When Someone Steals It.


This is the next piece in my Spring Security series, after how JWT authentication works, how OAuth2 login works, and how method-level security works. Those were about how a request gets in. This one is about how a user stays logged in without leaving the door open.

Imagine a user logs into your application. Your Spring Boot API issues a JWT valid for seven days. Three days later, someone steals that token.

The user changes their password immediately. Problem solved, right?

Not necessarily. Unless your application has a mechanism to invalidate that token, the stolen JWT can remain valid until it expires. That’s one of the biggest trade-offs of stateless JWT authentication. And it’s why refresh token rotation matters.

Why you can’t just “delete” a JWT

A JWT is stateless by design. Your server checks the signature and the expiry date, and that’s it. It never looks anything up in a database, which is exactly why JWTs are fast and easy to scale.

It’s also why they’re hard to take back. There’s no record on your side to delete. The token is valid because of what’s written inside it, and nothing you do later can change what’s written inside it. Changing the password doesn’t help. Logging out on the real user’s device doesn’t help. The token just sits there, valid, until the clock runs out.

Why short-lived access tokens aren’t enough
A JWT is typically validated using its signature and expiration time. If your API doesn’t check a revocation mechanism, changing a password or logging out doesn’t automatically invalidate an already-issued token.

One solution is to shorten the access token’s lifetime. But forcing users to log in every 10–15 minutes isn’t a great experience.

Instead, most applications separate authentication into two tokens:

Access token: Short-lived, often 5–15 minutes, and sent with API requests.
Refresh token: Longer-lived and used to obtain a new access token when necessary.
This limits the usefulness of a stolen access token. But what if the attacker steals the refresh token? They could keep obtaining new access tokens until the refresh token expires or is revoked. That’s where rotation comes in.

How refresh token rotation works

The rule is simple: every successful refresh replaces the old refresh token with a new one. Imagine this sequence:

The client sends Refresh Token A.
The server invalidates A and issues Access Token B and Refresh Token B.
Later, the client uses Refresh Token B to obtain the next pair.
Refresh Token A can no longer be used.
Now imagine someone tries to use Refresh Token A again. The server knows that A has already been consumed. This is called refresh token reuse detection. The server can revoke the entire token family and the chain of refresh tokens associated with that login session and require authentication again.

It cannot reliably determine which party is the attacker. It can only detect that a previously used token has appeared again.

Where to store the refresh token
This decides whether all the work above is worth anything. Don’t put tokens in localStorage. Any JavaScript on your page can read it, which means one XSS bug hands over the refresh token. The usual setup is:

Refresh token: httpOnly, secure cookie, so JavaScript can’t read it at all.
Access token: kept in memory (a plain JavaScript variable) and never written to disk.
Two trade-offs to know about. First, memory is wiped on page reload, so your frontend should quietly call /auth/refresh on startup to get a fresh access token. Second, if your frontend and API sit on different domains, the SameSite=Strict won’t work, and you’ll need SameSite=None plus CSRF protection on the refresh endpoint. On mobile, use the system keychain or keystore instead of a cookie.

**

Three production issues you shouldn’t ignore
**

  1. Network retries: Suppose the server rotates a token, but the response never reaches the client. The client retries with the old token. That can look like token theft.

Design retry handling deliberately. A carefully implemented idempotency mechanism may help, but blindly accepting reused tokens weakens replay detection.

  1. Parallel browser tabs: Two tabs may try to refresh simultaneously. Coordinate refresh operations on the client and enforce atomic token consumption on the server.

  2. Sessions that never expire: If each rotation extends the expiry by another seven days, an active session could continue indefinitely. Give each token family an absolute expiration time that rotation cannot extend.

Also revoke the user’s active token families when their password changes or they explicitly log out, according to your application’s security policy.

The takeaway

Refresh token rotation doesn’t make stolen credentials harmless. It gives your server a way to detect the reuse of a previously consumed refresh token and respond by revoking the affected session.

The important part isn’t just issuing a new token. It’s making token consumption atomic, ensuring revocation actually commits, and handling retries without silently weakening your security.

A short-lived access token limits exposure. Refresh token rotation helps detect replay. Together, they make a stronger authentication design.

Top comments (0)