Logout and Revocation

What Logout Does Now

  1. React calls POST /api/auth/logout; the browser sends the refresh cookie.
  2. The server revokes the refresh token family in the database.
  3. The server answers with Set-Cookie: shopease_rt=; Max-Age=0, which deletes the cookie.
  4. 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