diff --git a/backend/src/main/java/com/recipeapp/auth/AuthController.java b/backend/src/main/java/com/recipeapp/auth/AuthController.java index 3e5e9ee..51f5fa7 100644 --- a/backend/src/main/java/com/recipeapp/auth/AuthController.java +++ b/backend/src/main/java/com/recipeapp/auth/AuthController.java @@ -7,9 +7,15 @@ 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") @@ -26,7 +32,7 @@ public class AuthController { @Valid @RequestBody SignupRequest request, HttpServletRequest httpRequest) { UserResponse user = authService.signup(request); - httpRequest.getSession(true); + authenticateInSession(user.email(), "user", httpRequest); return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success(user)); } @@ -35,11 +41,25 @@ public class AuthController { @Valid @RequestBody LoginRequest request, HttpServletRequest httpRequest) { UserResponse user = authService.login(request); - HttpSession session = httpRequest.getSession(true); - session.setAttribute("user_email", user.email()); + // 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); return ResponseEntity.ok(ApiResponse.success(user)); } + 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/SecurityConfig.java b/backend/src/main/java/com/recipeapp/auth/SecurityConfig.java index c084836..7c1a6bd 100644 --- a/backend/src/main/java/com/recipeapp/auth/SecurityConfig.java +++ b/backend/src/main/java/com/recipeapp/auth/SecurityConfig.java @@ -10,8 +10,6 @@ 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.HttpStatusEntryPoint; -import org.springframework.security.web.csrf.CookieCsrfTokenRepository; -import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; @Configuration @EnableWebSecurity @@ -20,9 +18,7 @@ public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - .csrf(csrf -> csrf - .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) - .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())) + .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/v1/auth/signup", "/v1/auth/login").permitAll() .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts index df32645..9fbd150 100644 --- a/frontend/src/hooks.server.ts +++ b/frontend/src/hooks.server.ts @@ -15,7 +15,7 @@ function isPublicRoute(pathname: string): boolean { function loginRedirect(pathname: string): never { const target = '/login?redirect=' + encodeURIComponent(pathname); - redirect(302, target); + throw redirect(302, target); } export const handle: Handle = async ({ event, resolve }) => { diff --git a/frontend/src/routes/(public)/login/+page.server.ts b/frontend/src/routes/(public)/login/+page.server.ts index dd6912b..5b4a45b 100644 --- a/frontend/src/routes/(public)/login/+page.server.ts +++ b/frontend/src/routes/(public)/login/+page.server.ts @@ -3,7 +3,7 @@ import { apiClient } from '$lib/server/api'; import type { Actions } from './$types'; export const actions = { - default: async ({ request, url, fetch }) => { + default: async ({ request, url, fetch, cookies }) => { const formData = await request.formData(); const email = (formData.get('email') ?? '').toString().trim(); const password = (formData.get('password') ?? '').toString(); @@ -24,7 +24,7 @@ export const actions = { } const api = apiClient(fetch); - const { error } = await api.POST('/v1/auth/login', { + const { error, response } = await api.POST('/v1/auth/login', { body: { email, password } }); @@ -35,7 +35,12 @@ export const actions = { }); } + const sessionId = response.headers.get('set-cookie')?.match(/JSESSIONID=([^;]+)/i)?.[1]; + if (sessionId) { + cookies.set('JSESSIONID', sessionId, { path: '/', httpOnly: true, sameSite: 'lax' }); + } + const redirectTo = url.searchParams.get('redirect') || '/planner'; - redirect(303, redirectTo); + throw redirect(303, redirectTo); } } satisfies Actions; diff --git a/frontend/src/routes/(public)/signup/+page.server.ts b/frontend/src/routes/(public)/signup/+page.server.ts index 8fa6ff5..f158209 100644 --- a/frontend/src/routes/(public)/signup/+page.server.ts +++ b/frontend/src/routes/(public)/signup/+page.server.ts @@ -3,7 +3,7 @@ import { apiClient } from '$lib/server/api'; import type { Actions } from './$types'; export const actions = { - default: async ({ request, fetch }) => { + default: async ({ request, fetch, cookies }) => { const formData = await request.formData(); const displayName = (formData.get('displayName') ?? '').toString().trim(); const email = (formData.get('email') ?? '').toString().trim(); @@ -29,7 +29,7 @@ export const actions = { } const api = apiClient(fetch); - const { error } = await api.POST('/v1/auth/signup', { + const { error, response } = await api.POST('/v1/auth/signup', { body: { displayName, email, password } }); @@ -41,6 +41,11 @@ export const actions = { }); } - redirect(303, '/household/setup'); + const sessionId = response.headers.get('set-cookie')?.match(/JSESSIONID=([^;]+)/i)?.[1]; + if (sessionId) { + cookies.set('JSESSIONID', sessionId, { path: '/', httpOnly: true, sameSite: 'lax' }); + } + + throw redirect(303, '/household/setup'); } } satisfies Actions; diff --git a/frontend/src/routes/(public)/signup/+page.svelte b/frontend/src/routes/(public)/signup/+page.svelte index 8140c3a..dd9dda0 100644 --- a/frontend/src/routes/(public)/signup/+page.svelte +++ b/frontend/src/routes/(public)/signup/+page.svelte @@ -12,7 +12,7 @@
-
+