diff --git a/src/main/java/com/devpick/domain/user/controller/AuthController.java b/src/main/java/com/devpick/domain/user/controller/AuthController.java index d9ab065..e7f6bec 100644 --- a/src/main/java/com/devpick/domain/user/controller/AuthController.java +++ b/src/main/java/com/devpick/domain/user/controller/AuthController.java @@ -46,7 +46,9 @@ public class AuthController { static final String REFRESH_TOKEN_COOKIE = "refreshToken"; + static final String HAS_SESSION_COOKIE = "hasSession"; static final int REFRESH_TOKEN_MAX_AGE = 7 * 24 * 60 * 60; // 7일 (초 단위) + private static final String SET_COOKIE_HEADER = "Set-Cookie"; private final AuthService authService; private final EmailVerificationService emailVerificationService; @@ -76,6 +78,7 @@ public ApiResponse login(@RequestBody @Valid LoginRequest request HttpServletResponse response) { LoginResponse loginResponse = authService.login(request); setRefreshTokenCookie(response, loginResponse.refreshTokenValue()); + setHasSessionCookie(response); return ApiResponse.ok(loginResponse); } @@ -90,6 +93,7 @@ public ApiResponse logout(Authentication authentication, HttpServletRespon UUID userId = (UUID) authentication.getPrincipal(); tokenService.logout(userId); clearRefreshTokenCookie(response); + clearHasSessionCookie(response); return ApiResponse.ok(null); } @@ -120,6 +124,7 @@ public ApiResponse recover(@RequestBody @Valid RecoverRequest req HttpServletResponse response) { LoginResponse loginResponse = authService.recover(request); setRefreshTokenCookie(response, loginResponse.refreshTokenValue()); + setHasSessionCookie(response); return ApiResponse.ok(loginResponse); } @@ -134,6 +139,7 @@ public ApiResponse socialRecover(@RequestBody @Valid Social HttpServletResponse response) { SocialLoginResponse loginResponse = socialAuthService.recoverWithToken(request.recoveryToken()); setRefreshTokenCookie(response, loginResponse.refreshTokenValue()); + setHasSessionCookie(response); return ApiResponse.ok(loginResponse); } @@ -195,6 +201,7 @@ public ApiResponse githubCallback( HttpServletResponse response) { SocialLoginResponse loginResponse = socialAuthService.login("github", code, state); setRefreshTokenCookie(response, loginResponse.refreshTokenValue()); + setHasSessionCookie(response); return ApiResponse.ok(loginResponse); } @@ -212,11 +219,34 @@ public ApiResponse googleCallback( HttpServletResponse response) { SocialLoginResponse loginResponse = socialAuthService.login("google", code, state); setRefreshTokenCookie(response, loginResponse.refreshTokenValue()); + setHasSessionCookie(response); return ApiResponse.ok(loginResponse); } // ── Cookie 헬퍼 ────────────────────────────────────────────── + private void setHasSessionCookie(HttpServletResponse response) { + ResponseCookie cookie = ResponseCookie.from(HAS_SESSION_COOKIE, "true") + .httpOnly(false) // NOSONAR java:S2092 — 프론트 JS에서 세션 힌트로 읽어야 하므로 의도적으로 비활성화 + .secure(true) + .path("/") + .maxAge(REFRESH_TOKEN_MAX_AGE) + .sameSite("Lax") + .build(); + response.addHeader(SET_COOKIE_HEADER, cookie.toString()); + } + + private void clearHasSessionCookie(HttpServletResponse response) { + ResponseCookie cookie = ResponseCookie.from(HAS_SESSION_COOKIE, "") + .httpOnly(false) // NOSONAR java:S2092 — 프론트 JS에서 세션 힌트로 읽어야 하므로 의도적으로 비활성화 + .secure(true) + .path("/") + .maxAge(0) + .sameSite("Lax") + .build(); + response.addHeader(SET_COOKIE_HEADER, cookie.toString()); + } + private void setRefreshTokenCookie(HttpServletResponse response, String refreshToken) { ResponseCookie cookie = ResponseCookie.from(REFRESH_TOKEN_COOKIE, refreshToken) .httpOnly(true) @@ -225,7 +255,7 @@ private void setRefreshTokenCookie(HttpServletResponse response, String refreshT .maxAge(REFRESH_TOKEN_MAX_AGE) .sameSite("None") .build(); - response.addHeader("Set-Cookie", cookie.toString()); + response.addHeader(SET_COOKIE_HEADER, cookie.toString()); } private void clearRefreshTokenCookie(HttpServletResponse response) { @@ -236,6 +266,6 @@ private void clearRefreshTokenCookie(HttpServletResponse response) { .maxAge(0) .sameSite("None") .build(); - response.addHeader("Set-Cookie", cookie.toString()); + response.addHeader(SET_COOKIE_HEADER, cookie.toString()); } } diff --git a/src/test/java/com/devpick/domain/user/controller/AuthControllerTest.java b/src/test/java/com/devpick/domain/user/controller/AuthControllerTest.java index af86323..91d4159 100644 --- a/src/test/java/com/devpick/domain/user/controller/AuthControllerTest.java +++ b/src/test/java/com/devpick/domain/user/controller/AuthControllerTest.java @@ -31,6 +31,10 @@ import java.util.List; import java.util.UUID; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItem; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; @@ -38,7 +42,6 @@ import static org.mockito.Mockito.doThrow; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -159,7 +162,10 @@ void login_success() throws Exception { .andExpect(jsonPath("$.data.email").value("test@devpick.kr")) .andExpect(header().string("Set-Cookie", containsString("HttpOnly"))) .andExpect(header().string("Set-Cookie", containsString("SameSite=None"))) - .andExpect(header().string("Set-Cookie", containsString("refreshToken=refresh-token"))); + .andExpect(header().string("Set-Cookie", containsString("refreshToken=refresh-token"))) + .andExpect(result -> assertThat( + result.getResponse().getHeaders("Set-Cookie"), + hasItem(allOf(containsString("hasSession=true"), containsString("SameSite=Lax"))))); } @Test @@ -216,7 +222,12 @@ void logout_success() throws Exception { testUserId, null, List.of()))) .andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(true)) - .andExpect(header().string("Set-Cookie", containsString("Max-Age=0"))); + .andExpect(result -> assertThat( + result.getResponse().getHeaders("Set-Cookie"), + hasItem(allOf(containsString("refreshToken="), containsString("Max-Age=0"))))) + .andExpect(result -> assertThat( + result.getResponse().getHeaders("Set-Cookie"), + hasItem(allOf(containsString("hasSession="), containsString("Max-Age=0"))))); } @Test @@ -360,7 +371,10 @@ void githubCallback_success() throws Exception { .andExpect(jsonPath("$.data.email").value("hayoung@test.com")) .andExpect(header().string("Set-Cookie", containsString("HttpOnly"))) .andExpect(header().string("Set-Cookie", containsString("SameSite=None"))) - .andExpect(header().string("Set-Cookie", containsString("refreshToken=refresh-token"))); + .andExpect(header().string("Set-Cookie", containsString("refreshToken=refresh-token"))) + .andExpect(result -> assertThat( + result.getResponse().getHeaders("Set-Cookie"), + hasItem(allOf(containsString("hasSession=true"), containsString("SameSite=Lax"))))); } @Test @@ -420,7 +434,10 @@ void googleCallback_success() throws Exception { .andExpect(jsonPath("$.data.email").value("hayoung@gmail.com")) .andExpect(header().string("Set-Cookie", containsString("HttpOnly"))) .andExpect(header().string("Set-Cookie", containsString("SameSite=None"))) - .andExpect(header().string("Set-Cookie", containsString("refreshToken=refresh-token"))); + .andExpect(header().string("Set-Cookie", containsString("refreshToken=refresh-token"))) + .andExpect(result -> assertThat( + result.getResponse().getHeaders("Set-Cookie"), + hasItem(allOf(containsString("hasSession=true"), containsString("SameSite=Lax"))))); } @Test @@ -450,7 +467,10 @@ void recover_success() throws Exception { .content(objectMapper.writeValueAsString(request))) .andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data.accessToken").value("access-token")); + .andExpect(jsonPath("$.data.accessToken").value("access-token")) + .andExpect(result -> assertThat( + result.getResponse().getHeaders("Set-Cookie"), + hasItem(allOf(containsString("hasSession=true"), containsString("SameSite=Lax"))))); } @Test @@ -484,7 +504,10 @@ void socialRecover_success() throws Exception { .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.data.accessToken").value("access-token")) .andExpect(header().string("Set-Cookie", containsString("HttpOnly"))) - .andExpect(header().string("Set-Cookie", containsString("SameSite=None"))); + .andExpect(header().string("Set-Cookie", containsString("SameSite=None"))) + .andExpect(result -> assertThat( + result.getResponse().getHeaders("Set-Cookie"), + hasItem(allOf(containsString("hasSession=true"), containsString("SameSite=Lax"))))); } @Test