feat(auth): server-side sessions replacing Basic-auth cookie promotion (#523) #612

Merged
marcel merged 32 commits from feat/issue-523-server-side-sessions into main 2026-05-17 23:08:22 +02:00
2 changed files with 25 additions and 2 deletions
Showing only changes of commit ea65611690 - Show all commits

View File

@@ -4,6 +4,7 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.raddatz.familienarchiv.user.AppUser;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
@@ -18,6 +19,7 @@ import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
@Slf4j
public class AuthSessionController {
private final AuthService authService;
@@ -53,17 +55,26 @@ public class AuthSessionController {
@PostMapping("/logout")
public ResponseEntity<Void> logout(Authentication authentication, HttpServletRequest httpRequest) {
String email = authentication.getName();
String ip = resolveClientIp(httpRequest);
String ua = resolveUserAgent(httpRequest);
authService.logout(authentication.getName(), ip, ua);
// CWE-613 defense: invalidate the session first — that is the contract the user
// is relying on when they click "Log out." Audit is best-effort and must not
// bubble up: if the user record was deleted while the session was live, the
// audit lookup throws, but the session row in spring_session must still die.
HttpSession session = httpRequest.getSession(false);
if (session != null) {
session.invalidate();
}
SecurityContextHolder.clearContext();
try {
authService.logout(email, ip, ua);
} catch (Exception ex) {
log.warn("Audit logout failed for {}; session was already invalidated", email, ex);
}
return ResponseEntity.noContent().build();
}

View File

@@ -129,4 +129,16 @@ class AuthSessionControllerTest {
mockMvc.perform(post("/api/auth/logout"))
.andExpect(status().isUnauthorized());
}
@Test
void logout_returns_204_even_when_audit_throws() throws Exception {
// CWE-613 defense: the session MUST be invalidated even if the audit lookup
// explodes (e.g. user deleted between login and logout). Audit is best-effort.
doThrow(new RuntimeException("audit DB down"))
.when(authService).logout(anyString(), anyString(), anyString());
mockMvc.perform(post("/api/auth/logout")
.with(user("ghost@test.de")))
.andExpect(status().isNoContent());
}
}