Refresh tokens and rotation

Spring Security · lesson 24 of 31 · 4 min read

Keep access tokens short without hourly logins, and detect a refresh token that has been stolen.

Open this lesson in the learning hub

Key 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=Strict cookie 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.