diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index 8940c108e..2e581ab71 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -9,6 +9,7 @@ import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.auth.dto.oauth.OAuthCodeRequest; import com.example.solidconnection.auth.dto.oauth.OAuthResponse; +import com.example.solidconnection.auth.dto.oauth.OAuthSignInResponse; import com.example.solidconnection.auth.service.AuthService; import com.example.solidconnection.auth.service.CommonSignUpTokenProvider; import com.example.solidconnection.auth.service.EmailSignInService; @@ -21,6 +22,7 @@ import com.example.solidconnection.common.exception.ErrorCode; import com.example.solidconnection.common.resolver.AuthorizedUser; import com.example.solidconnection.siteuser.domain.AuthType; +import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -44,28 +46,39 @@ public class AuthController { private final EmailSignUpService emailSignUpService; private final EmailSignUpTokenProvider emailSignUpTokenProvider; private final CommonSignUpTokenProvider commonSignUpTokenProvider; + private final RefreshTokenCookieManager refreshTokenCookieManager; @PostMapping("/apple") public ResponseEntity processAppleOAuth( - @Valid @RequestBody OAuthCodeRequest oAuthCodeRequest + @Valid @RequestBody OAuthCodeRequest oAuthCodeRequest, + HttpServletResponse httpServletResponse ) { OAuthResponse oAuthResponse = appleOAuthService.processOAuth(oAuthCodeRequest); + if (oAuthResponse instanceof OAuthSignInResponse signInResponse) { + refreshTokenCookieManager.setCookie(httpServletResponse, signInResponse.refreshToken()); + } return ResponseEntity.ok(oAuthResponse); } @PostMapping("/kakao") public ResponseEntity processKakaoOAuth( - @Valid @RequestBody OAuthCodeRequest oAuthCodeRequest + @Valid @RequestBody OAuthCodeRequest oAuthCodeRequest, + HttpServletResponse httpServletResponse ) { OAuthResponse oAuthResponse = kakaoOAuthService.processOAuth(oAuthCodeRequest); + if (oAuthResponse instanceof OAuthSignInResponse signInResponse) { + refreshTokenCookieManager.setCookie(httpServletResponse, signInResponse.refreshToken()); + } return ResponseEntity.ok(oAuthResponse); } @PostMapping("/email/sign-in") public ResponseEntity signInWithEmail( - @Valid @RequestBody EmailSignInRequest signInRequest + @Valid @RequestBody EmailSignInRequest signInRequest, + HttpServletResponse httpServletResponse ) { SignInResponse signInResponse = emailSignInService.signIn(signInRequest); + refreshTokenCookieManager.setCookie(httpServletResponse, signInResponse.refreshToken()); return ResponseEntity.ok(signInResponse); } @@ -94,20 +107,24 @@ public ResponseEntity signUp( @PostMapping("/sign-out") public ResponseEntity signOut( - Authentication authentication + Authentication authentication, + HttpServletResponse httpServletResponse ) { String accessToken = getAccessToken(authentication); authService.signOut(accessToken); + refreshTokenCookieManager.deleteCookie(httpServletResponse); return ResponseEntity.ok().build(); } @DeleteMapping("/quit") public ResponseEntity quit( + @AuthorizedUser long siteUserId, Authentication authentication, - @AuthorizedUser long siteUserId + HttpServletResponse httpServletResponse ) { String accessToken = getAccessToken(authentication); authService.quit(siteUserId, accessToken); + refreshTokenCookieManager.deleteCookie(httpServletResponse); return ResponseEntity.ok().build(); } diff --git a/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java b/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java new file mode 100644 index 000000000..81bc45461 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java @@ -0,0 +1,42 @@ +package com.example.solidconnection.auth.controller; + +import com.example.solidconnection.auth.domain.TokenType; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Component; + +@Component +public class RefreshTokenCookieManager { + + private static final String COOKIE_NAME = "refreshToken"; + private static final String PATH = "/"; + private static final String SAME_SITE = "Strict"; + + public void setCookie(HttpServletResponse response, String refreshToken) { + long maxAge = convertExpireTimeToCookieMaxAge(TokenType.REFRESH.getExpireTime()); + setRefreshTokenCookie(response, refreshToken, maxAge); + } + + private long convertExpireTimeToCookieMaxAge(long milliSeconds) { + // jwt의 expireTime: millisecond, cookie의 maxAge: second + return milliSeconds / 1000; + } + + public void deleteCookie(HttpServletResponse response) { + setRefreshTokenCookie(response, "", 0); // 쿠키 삭제를 위해 maxAge를 0으로 설정 + } + + private void setRefreshTokenCookie( + HttpServletResponse response, String refreshToken, long maxAge + ) { + ResponseCookie cookie = ResponseCookie.from(COOKIE_NAME, refreshToken) + .httpOnly(true) + .secure(true) + .path(PATH) + .maxAge(maxAge) + .sameSite(SAME_SITE) + .build(); + response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); + } +} diff --git a/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java b/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java new file mode 100644 index 000000000..944be37ab --- /dev/null +++ b/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java @@ -0,0 +1,64 @@ +package com.example.solidconnection.auth.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.example.solidconnection.auth.domain.TokenType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletResponse; + +@DisplayName("리프레시 토큰 쿠키 매니저 테스트") +class RefreshTokenCookieManagerTest { + + private RefreshTokenCookieManager cookieManager; + + @BeforeEach + void setUp() { + cookieManager = new RefreshTokenCookieManager(); + } + + @Test + void 리프레시_토큰을_쿠키로_설정한다() { + // given + MockHttpServletResponse response = new MockHttpServletResponse(); + String refreshToken = "test-refresh-token"; + + // when + cookieManager.setCookie(response, refreshToken); + + // then + String header = response.getHeader("Set-Cookie"); + assertAll( + () -> assertThat(header).isNotNull(), + () -> assertThat(header).contains("refreshToken=" + refreshToken), + () -> assertThat(header).contains("HttpOnly"), + () -> assertThat(header).contains("Secure"), + () -> assertThat(header).contains("Path=/"), + () -> assertThat(header).contains("Max-Age=" + TokenType.REFRESH.getExpireTime() / 1000), + () -> assertThat(header).contains("SameSite=Strict") + ); + } + + @Test + void 쿠키에서_리프레시_토큰을_삭제한다() { + // given + MockHttpServletResponse response = new MockHttpServletResponse(); + + // when + cookieManager.deleteCookie(response); + + // then + String header = response.getHeader("Set-Cookie"); + assertAll( + () -> assertThat(header).isNotNull(), + () -> assertThat(header).contains("refreshToken="), + () -> assertThat(header).contains("HttpOnly"), + () -> assertThat(header).contains("Secure"), + () -> assertThat(header).contains("Path=/"), + () -> assertThat(header).contains("Max-Age=0"), + () -> assertThat(header).contains("SameSite=Strict") + ); + } +}