diff --git a/backend/src/main/java/com/recipeapp/auth/AuthController.java b/backend/src/main/java/com/recipeapp/auth/AuthController.java index d0f6605..93abcb1 100644 --- a/backend/src/main/java/com/recipeapp/auth/AuthController.java +++ b/backend/src/main/java/com/recipeapp/auth/AuthController.java @@ -7,15 +7,10 @@ import jakarta.servlet.http.HttpSession; import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.web.bind.annotation.*; import java.security.Principal; -import java.util.List; @RestController @RequestMapping("/v1/auth") @@ -32,7 +27,7 @@ public class AuthController { @Valid @RequestBody SignupRequest request, HttpServletRequest httpRequest) { UserResponse user = authService.signup(request); - authenticateInSession(user.email(), "user", httpRequest); + authService.authenticateInSession(user.email(), "user", httpRequest); return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success(user)); } @@ -41,30 +36,10 @@ public class AuthController { @Valid @RequestBody LoginRequest request, HttpServletRequest httpRequest) { UserResponse user = authService.login(request); - // Session fixation protection: invalidate old session before creating new one - var oldSession = httpRequest.getSession(false); - if (oldSession != null) { - oldSession.invalidate(); - } - authenticateInSession(user.email(), user.systemRole() != null ? user.systemRole() : "user", httpRequest); + authService.authenticateInSession(user.email(), user.systemRole() != null ? user.systemRole() : "user", httpRequest); return ResponseEntity.ok(ApiResponse.success(user)); } - /** - * Creates an authenticated Spring Security context and stores it in the HTTP session - * so that subsequent requests from the same session are recognised as authenticated. - * We do this manually because we are not using Spring Security's built-in form login. - */ - private void authenticateInSession(String email, String role, HttpServletRequest request) { - var auth = UsernamePasswordAuthenticationToken.authenticated( - email, null, List.of(new SimpleGrantedAuthority("ROLE_" + role.toUpperCase()))); - SecurityContext context = SecurityContextHolder.createEmptyContext(); - context.setAuthentication(auth); - SecurityContextHolder.setContext(context); - request.getSession(true).setAttribute( - HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, context); - } - @PostMapping("/logout") public ResponseEntity logout(HttpServletRequest httpRequest) { HttpSession session = httpRequest.getSession(false); diff --git a/backend/src/main/java/com/recipeapp/auth/AuthService.java b/backend/src/main/java/com/recipeapp/auth/AuthService.java index 1f007c6..feaee08 100644 --- a/backend/src/main/java/com/recipeapp/auth/AuthService.java +++ b/backend/src/main/java/com/recipeapp/auth/AuthService.java @@ -7,10 +7,18 @@ import com.recipeapp.common.ResourceNotFoundException; import com.recipeapp.common.ValidationException; import com.recipeapp.household.HouseholdMemberRepository; import com.recipeapp.household.entity.HouseholdMember; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + @Service public class AuthService { @@ -82,6 +90,24 @@ public class AuthService { return UserResponse.basic(user.getId(), user.getEmail(), user.getDisplayName()); } + /** + * Establishes an authenticated Spring Security session for the given user. + * Invalidates any existing session first (session fixation protection). + */ + public void authenticateInSession(String email, String role, HttpServletRequest request) { + var oldSession = request.getSession(false); + if (oldSession != null) { + oldSession.invalidate(); + } + var auth = UsernamePasswordAuthenticationToken.authenticated( + email, null, List.of(new SimpleGrantedAuthority("ROLE_" + role.toUpperCase()))); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(auth); + SecurityContextHolder.setContext(context); + request.getSession(true).setAttribute( + HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, context); + } + private UserResponse toUserResponse(UserAccount user) { return householdMemberRepository.findByUserEmailIgnoreCase(user.getEmail()) .map(member -> UserResponse.withHousehold( diff --git a/backend/src/main/java/com/recipeapp/household/HouseholdController.java b/backend/src/main/java/com/recipeapp/household/HouseholdController.java index d009393..c3eb59d 100644 --- a/backend/src/main/java/com/recipeapp/household/HouseholdController.java +++ b/backend/src/main/java/com/recipeapp/household/HouseholdController.java @@ -1,17 +1,12 @@ package com.recipeapp.household; -import com.recipeapp.auth.entity.UserAccount; +import com.recipeapp.auth.AuthService; import com.recipeapp.common.ApiResponse; import com.recipeapp.household.dto.*; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.web.bind.annotation.*; import java.security.Principal; @@ -24,9 +19,11 @@ import java.util.UUID; public class HouseholdController { private final HouseholdService householdService; + private final AuthService authService; - public HouseholdController(HouseholdService householdService) { + public HouseholdController(HouseholdService householdService, AuthService authService) { this.householdService = householdService; + this.authService = authService; } @PostMapping("/households") @@ -91,21 +88,7 @@ public class HouseholdController { HttpServletRequest httpRequest) { AcceptInviteResponse response = householdService.acceptInvite( code, request.name(), request.email(), request.password()); - authenticateInSession(request.email(), httpRequest); + authService.authenticateInSession(request.email(), "user", httpRequest); return ResponseEntity.ok(ApiResponse.success(response)); } - - private void authenticateInSession(String email, HttpServletRequest request) { - var oldSession = request.getSession(false); - if (oldSession != null) { - oldSession.invalidate(); - } - var auth = UsernamePasswordAuthenticationToken.authenticated( - email, null, List.of(new SimpleGrantedAuthority("ROLE_USER"))); - SecurityContext context = SecurityContextHolder.createEmptyContext(); - context.setAuthentication(auth); - SecurityContextHolder.setContext(context); - request.getSession(true).setAttribute( - HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, context); - } } diff --git a/backend/src/test/java/com/recipeapp/household/HouseholdControllerTest.java b/backend/src/test/java/com/recipeapp/household/HouseholdControllerTest.java index c889963..b7bb1c8 100644 --- a/backend/src/test/java/com/recipeapp/household/HouseholdControllerTest.java +++ b/backend/src/test/java/com/recipeapp/household/HouseholdControllerTest.java @@ -1,6 +1,7 @@ package com.recipeapp.household; import com.fasterxml.jackson.databind.ObjectMapper; +import com.recipeapp.auth.AuthService; import com.recipeapp.common.GlobalExceptionHandler; import com.recipeapp.common.ResourceNotFoundException; import com.recipeapp.common.ConflictException; @@ -36,6 +37,9 @@ class HouseholdControllerTest { @Mock private HouseholdService householdService; + @Mock + private AuthService authService; + @InjectMocks private HouseholdController householdController;