From c7782d554f9fbef7ea847b5fd6e6ae4812c23a69 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 17 May 2026 22:40:41 +0200 Subject: [PATCH] test(auth): login response never leaks the password field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pin the @JsonProperty(WRITE_ONLY) invariant on AppUser.password. If the annotation is ever dropped — or a new field aliases the hash — the CI run that ships the regression flags it the next morning rather than waiting for a security review. Addresses PR #612 / Nora concern (regression test). Co-Authored-By: Claude Opus 4.7 --- .../auth/AuthSessionControllerTest.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/backend/src/test/java/org/raddatz/familienarchiv/auth/AuthSessionControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/auth/AuthSessionControllerTest.java index 45b96d11..7ace8c45 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/auth/AuthSessionControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/auth/AuthSessionControllerTest.java @@ -100,6 +100,31 @@ class AuthSessionControllerTest { verify(sessionAuthenticationStrategy).onAuthentication(eq(auth), any(), any()); } + @Test + void login_response_body_does_not_contain_password_field() throws Exception { + // Regression guard: AppUser.password is @JsonProperty(WRITE_ONLY). If anyone + // ever drops that annotation, this assertion catches the credential leak on + // the very next CI run. + UUID userId = UUID.randomUUID(); + AppUser appUser = AppUser.builder() + .id(userId) + .email("leak@test.de") + .password("$2a$10$shouldnotappearinresponse") + .build(); + Authentication auth = mock(Authentication.class); + when(authService.login(anyString(), anyString(), anyString(), anyString())) + .thenReturn(new LoginResult(appUser, auth)); + + mockMvc.perform(post("/api/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"email\":\"leak@test.de\",\"password\":\"pass\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.password").doesNotExist()) + .andExpect(jsonPath("$.pwd").doesNotExist()) + .andExpect(content().string(org.hamcrest.Matchers.not( + org.hamcrest.Matchers.containsString("$2a$10$shouldnotappearinresponse")))); + } + @Test void login_does_not_set_cookie_on_failure() throws Exception { when(authService.login(anyString(), anyString(), anyString(), anyString()))