feat(auth): revoke all sessions on password reset

After updating the user password during a reset flow, calls
authService.revokeAllSessions(email) to invalidate every active session
for the account — prevents an attacker with a stolen session from
retaining access after the owner resets their password.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-18 12:47:28 +02:00
parent 99a4230bb9
commit 924c76f99f
2 changed files with 28 additions and 0 deletions

View File

@@ -5,6 +5,7 @@ import java.time.LocalDateTime;
import java.util.HexFormat;
import java.util.Optional;
import org.raddatz.familienarchiv.auth.AuthService;
import org.raddatz.familienarchiv.user.ResetPasswordRequest;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
@@ -32,6 +33,7 @@ public class PasswordResetService {
private final UserService userService;
private final PasswordResetTokenRepository tokenRepository;
private final PasswordEncoder passwordEncoder;
private final AuthService authService;
@Autowired(required = false)
private JavaMailSender mailSender;
@@ -85,6 +87,8 @@ public class PasswordResetService {
resetToken.setUsed(true);
tokenRepository.save(resetToken);
authService.revokeAllSessions(user.getEmail());
}
/**

View File

@@ -27,6 +27,7 @@ import org.springframework.mail.MailSendException;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.raddatz.familienarchiv.auth.AuthService;
import org.springframework.test.util.ReflectionTestUtils;
@ExtendWith(MockitoExtension.class)
@@ -36,8 +37,10 @@ class PasswordResetServiceTest {
@Mock PasswordResetTokenRepository tokenRepository;
@Mock PasswordEncoder passwordEncoder;
@Mock JavaMailSender mailSender;
@Mock AuthService authService;
@InjectMocks PasswordResetService service;
private AppUser makeUser(String email) {
return AppUser.builder()
.id(UUID.randomUUID())
@@ -176,6 +179,27 @@ class PasswordResetServiceTest {
verify(mailSender).send(any(SimpleMailMessage.class));
}
@Test
void resetPassword_revokes_all_sessions_after_password_reset() {
AppUser user = makeUser("user@example.com");
PasswordResetToken token = PasswordResetToken.builder()
.id(UUID.randomUUID())
.token("validtoken123")
.user(user)
.expiresAt(LocalDateTime.now().plusHours(1))
.used(false)
.build();
when(tokenRepository.findByToken("validtoken123")).thenReturn(Optional.of(token));
when(passwordEncoder.encode(any())).thenReturn("hashed");
ResetPasswordRequest req = new ResetPasswordRequest();
req.setToken("validtoken123");
req.setNewPassword("newpass");
service.resetPassword(req);
verify(authService).revokeAllSessions("user@example.com");
}
// ─── cleanupExpiredTokens ─────────────────────────────────────────────────
@Test