refactor(auth): extract authenticateInSession to AuthService

Remove duplicated private authenticateInSession from AuthController and
HouseholdController. Add a single public implementation on AuthService
with session fixation protection built in. HouseholdController now
injects AuthService and passes role "user" for invite-accepted accounts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-10 22:24:58 +02:00
parent 73af11e84b
commit 0b182a33fd
4 changed files with 37 additions and 49 deletions

View File

@@ -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<Void> logout(HttpServletRequest httpRequest) {
HttpSession session = httpRequest.getSession(false);

View File

@@ -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(

View File

@@ -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);
}
}

View File

@@ -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;