Refresh tokens and rotation
Keep access tokens short without hourly logins, and detect a refresh token that has been stolen.
Open this lesson in the learning hubKey points
- A signed access token cannot be revoked, so keep it to minutes. The refresh token is the long-lived half.
- Store refresh tokens server side: an opaque random value, hashed, with an owner, an expiry and a revoked flag.
- Rotate on every use. Hand back a new refresh token and mark the old one used - never accept the same one twice.
- A used token coming back means it leaked. Revoke the whole family for that user and make them log in again.
- For browsers send it in an
HttpOnly,Secure,SameSite=Strictcookie so no script can read it. - Refresh changes state: rate limit the endpoint and log every rotation, because that log is how you spot the theft.
Example
@PostMapping("/api/auth/refresh")
public TokenResponse refresh(@CookieValue("rt") String presented) {
RefreshToken stored = tokens.findByHash(sha256(presented))
.orElseThrow(() -> new BadCredentialsException("unknown refresh token"));
if (stored.isUsed() || stored.isRevoked()) { // a replay: this one leaked
tokens.revokeFamily(stored.getFamilyId());
throw new BadCredentialsException("refresh token reuse detected");
}
stored.markUsed();
RefreshToken next = tokens.issue(stored.getUserId(), stored.getFamilyId());
return new TokenResponse(jwt.issue(stored.getUserId()), next.getValue());
}
Rotate on every refresh and treat a replay as theft, not as a retry.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Spring Security course, and every lesson in it is listed on the Spring Security contents page.