Working with Refresh Tokens

1. Understanding Refresh Token Flow

Login → {access_token (15m), refresh_token (30d)}
        │
Access expires → POST /token (grant_type=refresh_token)
        ↓
{new access_token, new refresh_token} (rotation)
        │
Old refresh_token marked revoked, family tracked
      

2. Generating Refresh Tokens

PropertyValue
FormatOpaque random string (NOT JWT)
Entropy≥256 bits (CSPRNG, 32 bytes)
EncodingBase64URL
Storage at issueSHA-256 hash in DB (never plaintext)

3. Storing Refresh Tokens Securely

SideApproach
Server (DB)Store sha256(token) + userId + familyId + expiresAt + revokedAt
BrowserHttpOnly, Secure, SameSite=Strict cookie scoped to /oauth/token
MobileiOS Keychain / Android EncryptedSharedPreferences
DesktopOS keyring (libsecret, Credential Manager)

4. Implementing Token Refresh Endpoint

Example: OAuth-compliant refresh

@PostMapping(value = "/oauth/token", consumes = APPLICATION_FORM_URLENCODED_VALUE)
public TokenResponse refresh(@RequestParam String grant_type,
                              @RequestParam String refresh_token) {
  if (!"refresh_token".equals(grant_type)) throw new InvalidGrant();

  var stored = refreshRepo.findByTokenHash(sha256(refresh_token))
      .orElseThrow(InvalidGrant::new);

  if (stored.isRevoked()) {                    // reuse detection
    refreshRepo.revokeFamily(stored.getFamilyId());
    alertingService.notifyReuse(stored.getUserId());
    throw new InvalidGrant();
  }
  if (stored.getExpiresAt().isBefore(Instant.now())) throw new InvalidGrant();

  refreshRepo.revoke(stored.getId());
  var newRefresh = issueRefreshToken(stored.getUserId(), stored.getFamilyId());
  var access = jwtService.issueAccess(stored.getUserId(), stored.getScope());
  return new TokenResponse(access, newRefresh, "Bearer", 900);
}

5. Validating Refresh Tokens

CheckFailure
Hash existsinvalid_grant
Not revokedinvalid_grant + revoke family
Not expiredinvalid_grant
Client ID matchesinvalid_client
User still activeinvalid_grant

6. Rotating Refresh Tokens

AspectDetail
WhenEvery successful refresh
Old tokenMark revoked, keep row for reuse detection (until family expiry)
Family IDSame across rotations, regenerated on full re-auth
Grace window10–30 sec to handle network races

7. Revoking Refresh Tokens

TriggerScope
LogoutSingle token
Reuse detectedEntire family
Password changeAll user tokens
Admin force-logoutAll user tokens
Suspected compromiseAll sessions + force re-auth

8. Setting Refresh Token Expiration

App TypeLifetime
Web (BFF cookie)1–7 days
Mobile30–90 days, sliding
Sensitive (finance)1 day max
Sliding vs absoluteBoth: sliding inactivity (7d) + absolute cap (90d)

9. Implementing Refresh Token Reuse Detection

Reuse Detection Flow

  1. Issue token T1 (family F)
  2. Client refreshes: T1 → T2 (T1 marked revoked, retained)
  3. Attacker replays T1 → server sees revoked token in family F
  4. Revoke entire family F (T1, T2, ...)
  5. Alert user + force re-authentication

10. Handling Refresh Token Errors

OAuth ErrorMeaningClient Action
invalid_grantExpired, revoked, or unknownRedirect to login
invalid_clientBad client credentialsCheck config
invalid_requestMalformed bodyFix request
unauthorized_clientClient not allowed grant typeRegister correct grant
429Rate limitBackoff per Retry-After