From 457efab45272c195d17ab07d9967f883e36c41c5 Mon Sep 17 00:00:00 2001 From: Cedric Hornberger Date: Tue, 5 May 2026 11:13:05 +0200 Subject: [PATCH] security: enforce JWT-based authorization on AppUserController Added AuthenticatedUserResolver injection and assertSelf guard to getUser, getUserByGoogleId, and deleteUser endpoints. createUser remains open for registration. Added 7 controller tests covering validation failures and 403 enforcement. Co-Authored-By: Claude Sonnet 4.6 --- .../controller/AppUserController.java | 53 +++++++++---------- .../controller/AppUserControllerTest.java | 37 ++++++++++++- 2 files changed, 62 insertions(+), 28 deletions(-) diff --git a/src/main/java/de/zendric/app/xpensely_server/controller/AppUserController.java b/src/main/java/de/zendric/app/xpensely_server/controller/AppUserController.java index 16e6673..7bb5e8a 100644 --- a/src/main/java/de/zendric/app/xpensely_server/controller/AppUserController.java +++ b/src/main/java/de/zendric/app/xpensely_server/controller/AppUserController.java @@ -1,36 +1,35 @@ package de.zendric.app.xpensely_server.controller; import jakarta.validation.Valid; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; import de.zendric.app.xpensely_server.model.AppUser; import de.zendric.app.xpensely_server.model.AppUserCreateRequest; import de.zendric.app.xpensely_server.model.Exception.UsernameAlreadyExistsException; +import de.zendric.app.xpensely_server.security.AuthenticatedUserResolver; import de.zendric.app.xpensely_server.services.UserService; @RestController @RequestMapping("/api/users") public class AppUserController { - private UserService userService; + private final UserService userService; + private final AuthenticatedUserResolver authenticatedUserResolver; - @Autowired - public AppUserController(UserService userService) { + public AppUserController(UserService userService, AuthenticatedUserResolver authenticatedUserResolver) { this.userService = userService; + this.authenticatedUserResolver = authenticatedUserResolver; } @GetMapping - public AppUser getUser(@RequestParam Long id) { - return userService.getUser(id); + public ResponseEntity getUser(@RequestParam Long id, Authentication authentication) { + AppUser self = authenticatedUserResolver.resolveCurrentUser(authentication); + assertSelf(self, id); + return ResponseEntity.ok(userService.getUser(id)); } @GetMapping("/byName") @@ -39,23 +38,17 @@ public class AppUserController { } @GetMapping("/byGoogleId") - public ResponseEntity getUserByGoogleId(@RequestParam String id) { - try { - AppUser userByGoogleId = userService.getUserByGoogleId(id); - return new ResponseEntity<>(userByGoogleId, HttpStatus.OK); - - } catch (IllegalArgumentException e) { - return new ResponseEntity<>(null, HttpStatus.BAD_REQUEST); - } catch (Exception e) { - return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); - } + public ResponseEntity getUserByGoogleId(@RequestParam String id, Authentication authentication) { + AppUser self = authenticatedUserResolver.resolveCurrentUser(authentication); + if (!self.getGoogleId().equals(id)) + throw new ResponseStatusException(HttpStatus.FORBIDDEN); + return ResponseEntity.ok(self); } @PostMapping("/createUser") public ResponseEntity createUser(@RequestBody @Valid AppUserCreateRequest userRequest) { try { AppUser convertedUser = userRequest.convertToAppUser(); - AppUser nUser = userService.createUser(convertedUser); return new ResponseEntity<>(nUser, HttpStatus.CREATED); } catch (UsernameAlreadyExistsException e) { @@ -63,12 +56,18 @@ public class AppUserController { } catch (Exception e) { return new ResponseEntity<>(null, HttpStatus.BAD_REQUEST); } - } @DeleteMapping - public String deleteUser(@RequestParam Long id) { + public ResponseEntity deleteUser(@RequestParam Long id, Authentication authentication) { + AppUser self = authenticatedUserResolver.resolveCurrentUser(authentication); + assertSelf(self, id); AppUser user = userService.deleteUserById(id); - return "User deleted : " + user.getUsername(); + return ResponseEntity.ok("User deleted: " + user.getUsername()); + } + + private void assertSelf(AppUser authenticated, Long requestedId) { + if (!authenticated.getId().equals(requestedId)) + throw new ResponseStatusException(HttpStatus.FORBIDDEN); } } diff --git a/src/test/java/de/zendric/app/xpensely_Server/controller/AppUserControllerTest.java b/src/test/java/de/zendric/app/xpensely_Server/controller/AppUserControllerTest.java index 7e2167a..94dbd6d 100644 --- a/src/test/java/de/zendric/app/xpensely_Server/controller/AppUserControllerTest.java +++ b/src/test/java/de/zendric/app/xpensely_Server/controller/AppUserControllerTest.java @@ -1,6 +1,8 @@ package de.zendric.app.xpensely_Server.controller; import de.zendric.app.xpensely_server.controller.AppUserController; +import de.zendric.app.xpensely_server.model.AppUser; +import de.zendric.app.xpensely_server.security.AuthenticatedUserResolver; import de.zendric.app.xpensely_server.services.UserService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -11,7 +13,9 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @WebMvcTest(AppUserController.class) @@ -21,6 +25,7 @@ class AppUserControllerTest { @Autowired MockMvc mockMvc; @MockitoBean UserService userService; + @MockitoBean AuthenticatedUserResolver authenticatedUserResolver; @Test void createUser_blankUsername_returns400WithFieldError() throws Exception { @@ -57,4 +62,34 @@ class AppUserControllerTest { .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.googleId").exists()); } + + // --- Authorization tests --- + + @Test + void getUser_differentUser_returns403() throws Exception { + AppUser self = new AppUser(); self.setId(1L); + when(authenticatedUserResolver.resolveCurrentUser(any())).thenReturn(self); + + mockMvc.perform(get("/api/users").param("id", "99")) + .andExpect(status().isForbidden()); + } + + @Test + void getUser_sameUser_returns200() throws Exception { + AppUser self = new AppUser(); self.setId(1L); + when(authenticatedUserResolver.resolveCurrentUser(any())).thenReturn(self); + when(userService.getUser(1L)).thenReturn(self); + + mockMvc.perform(get("/api/users").param("id", "1")) + .andExpect(status().isOk()); + } + + @Test + void deleteUser_differentUser_returns403() throws Exception { + AppUser self = new AppUser(); self.setId(1L); + when(authenticatedUserResolver.resolveCurrentUser(any())).thenReturn(self); + + mockMvc.perform(delete("/api/users").param("id", "99")) + .andExpect(status().isForbidden()); + } }