Added soft delete to RefreshTokenEntity to prevent deadlock and 500 errors.

This commit is contained in:
Jesse Brault 2024-08-20 11:07:41 -05:00
parent 0396e8e3b0
commit 3d6577fe02
5 changed files with 37 additions and 9 deletions

View File

@ -2,8 +2,10 @@ package app.mealsmadeeasy.api;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication @SpringBootApplication
@EnableScheduling
public class MealsMadeEasyApiApplication { public class MealsMadeEasyApiApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@ -5,6 +5,7 @@ import app.mealsmadeeasy.api.user.UserEntity;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@ -13,6 +14,7 @@ import org.springframework.stereotype.Service;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Service @Service
public class AuthServiceImpl implements AuthService { public class AuthServiceImpl implements AuthService {
@ -76,17 +78,18 @@ public class AuthServiceImpl implements AuthService {
final RefreshTokenEntity old = this.refreshTokenRepository.findByToken(refreshToken) final RefreshTokenEntity old = this.refreshTokenRepository.findByToken(refreshToken)
.orElseThrow(() -> new LoginException( .orElseThrow(() -> new LoginException(
LoginExceptionReason.INVALID_REFRESH_TOKEN, LoginExceptionReason.INVALID_REFRESH_TOKEN,
"No such refresh-token: " + refreshToken "No such refresh token: " + refreshToken
)); ));
if (old.isRevoked()) { if (old.isRevoked() || old.isDeleted()) {
throw new LoginException(LoginExceptionReason.INVALID_REFRESH_TOKEN, "RefreshToken is revoked."); throw new LoginException(LoginExceptionReason.INVALID_REFRESH_TOKEN, "Invalid refresh token.");
} }
if (old.getExpires().isBefore(LocalDateTime.now())) { if (old.getExpires().isBefore(LocalDateTime.now())) {
throw new LoginException(LoginExceptionReason.EXPIRED_REFRESH_TOKEN, "RefreshToken is expired."); throw new LoginException(LoginExceptionReason.EXPIRED_REFRESH_TOKEN, "Refresh token is expired.");
} }
final UserEntity principal = old.getOwner(); final UserEntity principal = old.getOwner();
this.refreshTokenRepository.delete(old); old.setDeleted(true);
this.refreshTokenRepository.save(old);
final String username = principal.getUsername(); final String username = principal.getUsername();
return new LoginDetails( return new LoginDetails(
@ -96,4 +99,9 @@ public class AuthServiceImpl implements AuthService {
); );
} }
@Scheduled(fixedDelay = 60, timeUnit = TimeUnit.SECONDS)
public void cleanUpDeletedRefreshTokens() {
this.refreshTokenRepository.deleteAllWhereSoftDeleted();
}
} }

View File

@ -7,4 +7,5 @@ import java.time.LocalDateTime;
public interface RefreshToken extends AuthToken { public interface RefreshToken extends AuthToken {
LocalDateTime getIssued(); LocalDateTime getIssued();
boolean isRevoked(); boolean isRevoked();
boolean isDeleted();
} }

View File

@ -30,6 +30,9 @@ public class RefreshTokenEntity implements RefreshToken {
@ManyToOne @ManyToOne
private UserEntity owner; private UserEntity owner;
@Column(nullable = false)
private Boolean deleted = false;
public Long getId() { public Long getId() {
return this.id; return this.id;
} }
@ -70,7 +73,7 @@ public class RefreshTokenEntity implements RefreshToken {
return this.revoked; return this.revoked;
} }
public void setRevoked(Boolean revoked) { public void setRevoked(boolean revoked) {
this.revoked = revoked; this.revoked = revoked;
} }
@ -82,6 +85,15 @@ public class RefreshTokenEntity implements RefreshToken {
this.owner = owner; this.owner = owner;
} }
@Override
public boolean isDeleted() {
return this.deleted;
}
public void setDeleted(boolean deleted) {
this.deleted = deleted;
}
@Override @Override
public long getLifetime() { public long getLifetime() {
return ChronoUnit.SECONDS.between(this.issued, this.expiration); return ChronoUnit.SECONDS.between(this.issued, this.expiration);

View File

@ -1,14 +1,19 @@
package app.mealsmadeeasy.api.auth; package app.mealsmadeeasy.api.auth;
import jakarta.persistence.LockModeType; import jakarta.transaction.Transactional;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import java.util.Optional; import java.util.Optional;
public interface RefreshTokenRepository extends JpaRepository<RefreshTokenEntity, Long> { public interface RefreshTokenRepository extends JpaRepository<RefreshTokenEntity, Long> {
@Lock(LockModeType.PESSIMISTIC_READ)
Optional<RefreshTokenEntity> findByToken(String token); Optional<RefreshTokenEntity> findByToken(String token);
@Modifying
@Transactional
@Query("DELETE FROM RefreshToken t WHERE t.deleted = true")
void deleteAllWhereSoftDeleted();
} }