fix(auth): rotate session ID on login to prevent session fixation (CWE-384)
Inject Spring Security's SessionAuthenticationStrategy (ChangeSessionIdAuthenticationStrategy) into AuthSessionController and invoke onAuthentication at the credential boundary. The strategy calls HttpServletRequest.changeSessionId() to invalidate any pre-auth session ID an attacker may have planted and mint a fresh ID before the SecurityContext is attached. Addresses PR #612 / Nora B1. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -20,23 +21,31 @@ import org.springframework.web.bind.annotation.*;
|
||||
public class AuthSessionController {
|
||||
|
||||
private final AuthService authService;
|
||||
private final SessionAuthenticationStrategy sessionAuthenticationStrategy;
|
||||
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<AppUser> login(
|
||||
@RequestBody LoginRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
HttpServletRequest httpRequest,
|
||||
HttpServletResponse httpResponse) {
|
||||
|
||||
String ip = resolveClientIp(httpRequest);
|
||||
String ua = resolveUserAgent(httpRequest);
|
||||
|
||||
AuthService.LoginResult result = authService.login(request.email(), request.password(), ip, ua);
|
||||
|
||||
// Establish server-side session. Spring Session JDBC intercepts getSession().setAttribute()
|
||||
// and persists the record; the Set-Cookie: fa_session=<opaque-id> is added automatically.
|
||||
// Session-fixation defense (CWE-384): rotate the session ID at the authentication
|
||||
// boundary. ChangeSessionIdAuthenticationStrategy invalidates any pre-auth session ID
|
||||
// an attacker may have planted and mints a fresh one before we attach the SecurityContext.
|
||||
httpRequest.getSession(true);
|
||||
sessionAuthenticationStrategy.onAuthentication(result.authentication(), httpRequest, httpResponse);
|
||||
|
||||
// Spring Session JDBC intercepts setAttribute() and persists the record under the
|
||||
// (now rotated) opaque ID; the Set-Cookie: fa_session=<opaque-id> is added automatically.
|
||||
SecurityContext context = SecurityContextHolder.createEmptyContext();
|
||||
context.setAuthentication(result.authentication());
|
||||
SecurityContextHolder.setContext(context);
|
||||
httpRequest.getSession(true)
|
||||
httpRequest.getSession()
|
||||
.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, context);
|
||||
|
||||
return ResponseEntity.ok(result.user());
|
||||
|
||||
@@ -17,6 +17,8 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.session.ChangeSessionIdAuthenticationStrategy;
|
||||
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@@ -43,6 +45,14 @@ public class SecurityConfig {
|
||||
return config.getAuthenticationManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SessionAuthenticationStrategy sessionAuthenticationStrategy() {
|
||||
// ChangeSessionIdAuthenticationStrategy rotates the session ID via the Servlet 3.1+
|
||||
// HttpServletRequest.changeSessionId() — preserves attributes, mints a fresh ID.
|
||||
// Used by AuthSessionController.login to defend against session fixation (CWE-384).
|
||||
return new ChangeSessionIdAuthenticationStrategy();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
public SecurityFilterChain managementFilterChain(HttpSecurity http) throws Exception {
|
||||
|
||||
@@ -14,12 +14,14 @@ import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
@@ -33,6 +35,7 @@ class AuthSessionControllerTest {
|
||||
|
||||
@MockitoBean AuthService authService;
|
||||
@MockitoBean CustomUserDetailsService customUserDetailsService;
|
||||
@MockitoBean SessionAuthenticationStrategy sessionAuthenticationStrategy;
|
||||
|
||||
// ─── POST /api/auth/login ──────────────────────────────────────────────────
|
||||
|
||||
@@ -79,6 +82,24 @@ class AuthSessionControllerTest {
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
void login_delegates_to_SessionAuthenticationStrategy_for_fixation_protection() throws Exception {
|
||||
UUID userId = UUID.randomUUID();
|
||||
AppUser appUser = AppUser.builder().id(userId).email("fix@test.de").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\":\"fix@test.de\",\"password\":\"pass\"}"))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
// Session-fixation defense (CWE-384): the controller must hand the new
|
||||
// Authentication to Spring Security's strategy, which rotates the session ID.
|
||||
verify(sessionAuthenticationStrategy).onAuthentication(eq(auth), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void login_does_not_set_cookie_on_failure() throws Exception {
|
||||
when(authService.login(anyString(), anyString(), anyString(), anyString()))
|
||||
|
||||
Reference in New Issue
Block a user