feat(security): CSRF protection, session revocation, login rate limiting (#524) #617

Merged
marcel merged 26 commits from feat/issue-524-csrf-session-rate-limit into main 2026-05-19 09:23:03 +02:00
4 changed files with 34 additions and 9 deletions
Showing only changes of commit 778402fec7 - Show all commits

View File

@@ -1,12 +1,8 @@
package org.raddatz.familienarchiv.auth;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.stereotype.Service;
@Service
@ConditionalOnBean(JdbcIndexedSessionRepository.class)
@RequiredArgsConstructor
class JdbcSessionRevocationAdapter implements SessionRevocationPort {

View File

@@ -1,10 +1,5 @@
package org.raddatz.familienarchiv.auth;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.stereotype.Service;
@Service
@ConditionalOnMissingBean(SessionRevocationPort.class)
class NoOpSessionRevocationAdapter implements SessionRevocationPort {
@Override

View File

@@ -0,0 +1,19 @@
package org.raddatz.familienarchiv.auth;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
@Configuration
class SessionRevocationConfig {
@Bean
SessionRevocationPort sessionRevocationPort(
@Autowired(required = false) JdbcIndexedSessionRepository sessionRepository) {
if (sessionRepository != null) {
return new JdbcSessionRevocationAdapter(sessionRepository);
}
return new NoOpSessionRevocationAdapter();
}
}

View File

@@ -119,6 +119,21 @@ class AuthSessionIntegrationTest {
assertThat(me.getStatusCode().value()).isEqualTo(401);
}
// ─── Task: CSRF rejection at integration layer ────────────────────────────
@Test
void post_without_csrf_token_returns_403_CSRF_TOKEN_MISSING() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// Deliberately omit XSRF-TOKEN cookie and X-XSRF-TOKEN header
ResponseEntity<String> response = http.postForEntity(
baseUrl + "/api/auth/logout",
new HttpEntity<>("{}", headers), String.class);
assertThat(response.getStatusCode().value()).isEqualTo(403);
assertThat(response.getBody()).contains("CSRF_TOKEN_MISSING");
}
// ─── helpers ─────────────────────────────────────────────────────────────
/**