Careeroza
Logout and Revocation
What Logout Does Now
- React calls
POST /api/auth/logout; the browser sends the refresh cookie. - The server revokes the refresh token family in the database.
- The server answers with
Set-Cookie: shopease_rt=; Max-Age=0, which deletes the cookie. - React forgets the access token. It would still be valid for up to 15 minutes, but nobody has it any more.
Example
curl -i -X POST localhost:8080/api/auth/logout --cookie "shopease_rt=Qm9x3...Zk"
curl -i -X POST localhost:8080/api/auth/refresh --cookie "shopease_rt=Qm9x3...Zk"Example output
HTTP/1.1 204 No Content
Set-Cookie: shopease_rt=; Path=/api/auth; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; HttpOnly; SameSite=Strict
HTTP/1.1 401 Unauthorized
{"status":401,"error":"Unauthorized","message":"Refresh token reuse detected","path":"/api/auth/refresh"}Logout from All Devices
"Log out of all devices" (after a password change, or from a security settings page) just revokes every refresh token of the user:
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
Optional<RefreshToken> findByTokenHash(String tokenHash);
@Modifying
@Query("update RefreshToken t set t.revoked = true where t.family = :family")
void revokeFamily(String family);
@Modifying
@Query("update RefreshToken t set t.revoked = true where t.user.id = :userId")
void revokeAllForUser(Long userId);
@Modifying
@Query("delete from RefreshToken t where t.expiresAt < :now")
int deleteExpired(Instant now);
}Note: If you must kill access tokens immediately too (for example a banned user), keep a small deny-list of token ids (jti) or a per-user "tokens valid after" timestamp in Redis, and check it in the JWT filter. That adds a lookup per request, so many apps accept the 15-minute window instead.
Exercise
Exercise: Add a scheduled job that deletes expired refresh tokens every night at 3 AM.
Show answer
@Component
public class RefreshTokenCleanup {
private final RefreshTokenRepository repo;
public RefreshTokenCleanup(RefreshTokenRepository repo) { this.repo = repo; }
@Scheduled(cron = "0 0 3 * * *", zone = "Asia/Kolkata")
@Transactional
public void purge() {
int n = repo.deleteExpired(Instant.now());
log.info("Deleted {} expired refresh tokens", n);
}
}
// plus @EnableScheduling on a @Configuration class