diff --git a/src/main/java/com/ject/studytrip/auth/application/facade/AuthFacade.java b/src/main/java/com/ject/studytrip/auth/application/facade/AuthFacade.java index ce8b7b7..03ec717 100644 --- a/src/main/java/com/ject/studytrip/auth/application/facade/AuthFacade.java +++ b/src/main/java/com/ject/studytrip/auth/application/facade/AuthFacade.java @@ -22,8 +22,8 @@ public class AuthFacade { private final TokenService tokenService; private final MemberService memberService; - public TokenResponse kakaoLogin(KakaoLoginRequest request) { - KakaoUserInfoResponse response = kakaoLoginService.getKakaoUserInfo(request.code()); + public TokenResponse kakaoLogin(KakaoLoginRequest request, String origin) { + KakaoUserInfoResponse response = kakaoLoginService.getKakaoUserInfo(request.code(), origin); Member member = memberService.getMemberBySocialProviderAndSocialId( @@ -32,8 +32,8 @@ public TokenResponse kakaoLogin(KakaoLoginRequest request) { return tokenService.getTokens(member.getId().toString(), member.getRole().name()); } - public TokenResponse kakaoSignup(KakaoSignupRequest request) { - KakaoUserInfoResponse response = kakaoLoginService.getKakaoUserInfo(request.code()); + public TokenResponse kakaoSignup(KakaoSignupRequest request, String origin) { + KakaoUserInfoResponse response = kakaoLoginService.getKakaoUserInfo(request.code(), origin); CreateMemberCommand command = CreateMemberCommand.of( response.kakaoId(), diff --git a/src/main/java/com/ject/studytrip/auth/application/service/KakaoLoginService.java b/src/main/java/com/ject/studytrip/auth/application/service/KakaoLoginService.java index 8488f4d..7109a46 100644 --- a/src/main/java/com/ject/studytrip/auth/application/service/KakaoLoginService.java +++ b/src/main/java/com/ject/studytrip/auth/application/service/KakaoLoginService.java @@ -11,8 +11,8 @@ public class KakaoLoginService { private final KakaoOauthProvider kakaoOauthProvider; - public KakaoUserInfoResponse getKakaoUserInfo(String code) { - KakaoTokenResponse response = kakaoOauthProvider.getKakaoTokens(code); + public KakaoUserInfoResponse getKakaoUserInfo(String code, String origin) { + KakaoTokenResponse response = kakaoOauthProvider.getKakaoTokens(code, origin); return kakaoOauthProvider.getKakaoUserInfo(response.accessToken()); } } diff --git a/src/main/java/com/ject/studytrip/auth/infra/provider/KakaoOauthProvider.java b/src/main/java/com/ject/studytrip/auth/infra/provider/KakaoOauthProvider.java index 049370e..6d5873e 100644 --- a/src/main/java/com/ject/studytrip/auth/infra/provider/KakaoOauthProvider.java +++ b/src/main/java/com/ject/studytrip/auth/infra/provider/KakaoOauthProvider.java @@ -20,10 +20,10 @@ public class KakaoOauthProvider { private final KakaoOauthClient kakaoOauthClient; private final KakaoOauthProperties kakaoOauthProperties; - public KakaoTokenResponse getKakaoTokens(String code) { + public KakaoTokenResponse getKakaoTokens(String code, String origin) { validateKakaoAuthorizationCode(code); return kakaoOauthClient - .fetchKakaoTokens(kakaoOauthProperties.tokenUri(), createFormData(code)) + .fetchKakaoTokens(kakaoOauthProperties.tokenUri(), createFormData(code, origin)) .block(); } @@ -34,12 +34,12 @@ public KakaoUserInfoResponse getKakaoUserInfo(String accessToken) { .block(); } - private BodyInserters.FormInserter createFormData(String code) { + private BodyInserters.FormInserter createFormData(String code, String origin) { MultiValueMap formData = new LinkedMultiValueMap<>(); formData.add("grant_type", "authorization_code"); formData.add("client_id", kakaoOauthProperties.clientId()); formData.add("client_secret", kakaoOauthProperties.clientSecret()); - formData.add("redirect_uri", kakaoOauthProperties.redirectUri()); + formData.add("redirect_uri", origin + kakaoOauthProperties.redirectUri()); formData.add("code", code); return BodyInserters.fromFormData(formData); } diff --git a/src/main/java/com/ject/studytrip/auth/presentation/controller/AuthController.java b/src/main/java/com/ject/studytrip/auth/presentation/controller/AuthController.java index 0d1eaf5..85fec52 100644 --- a/src/main/java/com/ject/studytrip/auth/presentation/controller/AuthController.java +++ b/src/main/java/com/ject/studytrip/auth/presentation/controller/AuthController.java @@ -13,23 +13,21 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @Tag(name = "Auth", description = "인증 API") @RestController @RequiredArgsConstructor -@RequestMapping("api/auth") +@RequestMapping("/api/auth") public class AuthController { private final AuthFacade authFacade; @Operation(summary = "카카오 로그인", description = "카카오 인가 코드를 이용하여, 엑세스 토큰과 리프레시 토큰을 발급합니다.") @PostMapping("/login/kakao") public ResponseEntity kakaoLogin( + @RequestAttribute(value = "origin") String origin, @Valid @RequestBody KakaoLoginRequest request) { - TokenResponse response = authFacade.kakaoLogin(request); + TokenResponse response = authFacade.kakaoLogin(request, origin); return ResponseEntity.ok(StandardResponse.success(HttpStatus.OK.value(), response)); } @@ -39,8 +37,9 @@ public ResponseEntity kakaoLogin( description = "카카오 인가 코드, 카테고리, 닉네임을 이용하여, 엑세스 토큰과 리프레시 토큰을 발급합니다.") @PostMapping("/signup/kakao") public ResponseEntity kakaoSignup( + @RequestAttribute(value = "origin") String origin, @Valid @RequestBody KakaoSignupRequest request) { - TokenResponse response = authFacade.kakaoSignup(request); + TokenResponse response = authFacade.kakaoSignup(request, origin); return ResponseEntity.ok(StandardResponse.success(HttpStatus.OK.value(), response)); } diff --git a/src/main/java/com/ject/studytrip/global/annotation/ClientOrigin.java b/src/main/java/com/ject/studytrip/global/annotation/ClientOrigin.java new file mode 100644 index 0000000..db02dee --- /dev/null +++ b/src/main/java/com/ject/studytrip/global/annotation/ClientOrigin.java @@ -0,0 +1,10 @@ +package com.ject.studytrip.global.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface ClientOrigin {} diff --git a/src/main/java/com/ject/studytrip/global/common/constants/SwaggerUrlConstants.java b/src/main/java/com/ject/studytrip/global/common/constants/SwaggerUrlConstants.java deleted file mode 100644 index 75fe8c4..0000000 --- a/src/main/java/com/ject/studytrip/global/common/constants/SwaggerUrlConstants.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.ject.studytrip.global.common.constants; - -import java.util.Arrays; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public enum SwaggerUrlConstants { - SWAGGER_RESOURCES_URL("/swagger-resources/**"), - SWAGGER_UI_URL("/swagger-ui/**"), - SWAGGER_API_DOCS_URL("/v3/api-docs/**"), - ; - - private final String value; - - public static String[] getSwaggerUrls() { - return Arrays.stream(SwaggerUrlConstants.values()) - .map(SwaggerUrlConstants::getValue) - .toArray(String[]::new); - } -} diff --git a/src/main/java/com/ject/studytrip/global/common/constants/UrlConstants.java b/src/main/java/com/ject/studytrip/global/common/constants/UrlConstants.java index b3b896f..7759771 100644 --- a/src/main/java/com/ject/studytrip/global/common/constants/UrlConstants.java +++ b/src/main/java/com/ject/studytrip/global/common/constants/UrlConstants.java @@ -1,18 +1,37 @@ package com.ject.studytrip.global.common.constants; import lombok.Getter; -import lombok.RequiredArgsConstructor; @Getter -@RequiredArgsConstructor public enum UrlConstants { - DEV_API_SERVER_URL("https://dev-api-studytrip.duckdns.org"), - LOCAL_API_SERVER_URL("http://localhost:8080"), + // CORS 허용 도메인 + CORS_DOMAINS( + "http://localhost:8080", + "https://dev-api-studytrip.duckdns.org", + "http://localhost:5173", + "https://localhost:5173", + "https://ject-4-client.vercel.app"), - PRODUCTION_CLIENT_URL("https://ject-4-client.vercel.app"), - LOCAL_CLIENT_URL("http://localhost:5173"), - LOCAL_SECURE_CLIENT_URL("https://localhost:5173"), - ; + // 정적 리소스 경로 + STATIC_RESOURCES( + "/favicon.ico", + "/firebase-messaging-sw.js", + "/v3/api-docs/**", + "/swagger-ui/**", + "/swagger-ui.html"), - private final String value; + // OAuth 콜백 경로 + CALLBACK_PATHS("/auth/callback/**"), + + // Origin 추출이 필요한 경로 + ORIGIN_EXTRACT_PATHS("/api/auth/login/kakao", "/api/auth/signup/kakao"), + + // 인증이 필요없는 API 경로 + PERMIT_ALL_API_PATHS("/api/auth/**", "/api/trips/categories"); + + private final String[] urls; + + UrlConstants(String... urls) { + this.urls = urls; + } } diff --git a/src/main/java/com/ject/studytrip/global/config/WebSecurityConfig.java b/src/main/java/com/ject/studytrip/global/config/WebSecurityConfig.java index 674cd72..67e4635 100644 --- a/src/main/java/com/ject/studytrip/global/config/WebSecurityConfig.java +++ b/src/main/java/com/ject/studytrip/global/config/WebSecurityConfig.java @@ -1,11 +1,10 @@ package com.ject.studytrip.global.config; -import com.ject.studytrip.global.common.constants.SwaggerUrlConstants; -import com.ject.studytrip.global.common.constants.UrlConstants; +import static com.ject.studytrip.global.common.constants.UrlConstants.*; + +import com.ject.studytrip.auth.application.service.TokenService; import com.ject.studytrip.global.config.properties.TokenProperties; -import com.ject.studytrip.global.security.CustomAccessDeniedHandler; -import com.ject.studytrip.global.security.CustomAuthenticationEntryPoint; -import com.ject.studytrip.global.security.JwtAuthenticationFilter; +import com.ject.studytrip.global.security.*; import java.util.Arrays; import java.util.List; import lombok.RequiredArgsConstructor; @@ -13,6 +12,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -21,6 +21,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; @Slf4j @EnableWebSecurity @@ -28,7 +29,6 @@ @RequiredArgsConstructor @EnableConfigurationProperties(TokenProperties.class) public class WebSecurityConfig { - private final JwtAuthenticationFilter jwtAuthenticationFilter; private final CustomAuthenticationEntryPoint authenticationEntryPoint; private final CustomAccessDeniedHandler accessDeniedHandler; @@ -47,23 +47,64 @@ private void defaultFilterChain(HttpSecurity http) throws Exception { .cors(cors -> cors.configurationSource(corsConfigurationSource())); } + // 퍼블릭 리소스 URL 필터 체인 + @Bean + @Order(0) + public SecurityFilterChain publicFilterChain(HttpSecurity http) throws Exception { + defaultFilterChain(http); + + http.securityMatcher(STATIC_RESOURCES.getUrls()); + http.authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll()); + + return http.build(); + } + + // 콜백 URL 필터 체인 @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + @Order(1) + public SecurityFilterChain callbackFilterChain(HttpSecurity http) throws Exception { + defaultFilterChain(http); + http.securityMatcher(CALLBACK_PATHS.getUrls()); + http.authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll()); + + return http.build(); + } + + // Origin 추출이 필요한 URL 필터체인 + @Bean + @Order(2) + public SecurityFilterChain originExtractionFilterChain( + HttpSecurity http, SecurityResponseHandler securityResponseHandler) throws Exception { + defaultFilterChain(http); + + http.securityMatcher(ORIGIN_EXTRACT_PATHS.getUrls()); + + // Origin 추출 필터 등록 : CORS 이후 OriginExtractionFilter 실행 + http.addFilterAfter(new OriginExtractionFilter(securityResponseHandler), CorsFilter.class); + + http.authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll()); + + return http.build(); + } + + // 메인 필터체인 + @Bean + @Order(3) + public SecurityFilterChain mainFilterChain(HttpSecurity http, TokenService tokenService) + throws Exception { defaultFilterChain(http); // JWT 필터 등록 : 인증 이전에 동작해야 하므로 UsernamePasswordAuthenticationFilter 앞에 삽입 - http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + http.addFilterBefore( + new JwtAuthenticationFilter(tokenService), + UsernamePasswordAuthenticationFilter.class); // 경로 인가 설정 http.authorizeHttpRequests( authorize -> authorize - .requestMatchers(SwaggerUrlConstants.getSwaggerUrls()) - .permitAll() // Swagger 경로 - .requestMatchers("/api/sample/**", "/api/auth/**") - .permitAll() // 샘플 api 경로 - .requestMatchers("/api/trips/categories") + .requestMatchers(PERMIT_ALL_API_PATHS.getUrls()) .permitAll() .anyRequest() .authenticated()); // 그 외 요청은 모두 인증 수행 @@ -92,8 +133,7 @@ public UrlBasedCorsConfigurationSource corsConfigurationSource() { // - prod: PROD_DOMAIN 만 허용 // - Spring Active Profile 기반 분기 필요 // - 서비스 도메인, 서버 운영 환경 설정 완료 시 작업 - List allowedOrigins = - Arrays.stream(UrlConstants.values()).map(UrlConstants::getValue).toList(); + List allowedOrigins = Arrays.asList(CORS_DOMAINS.getUrls()); config.setAllowedOrigins(allowedOrigins); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); diff --git a/src/main/java/com/ject/studytrip/global/exception/error/CommonErrorCode.java b/src/main/java/com/ject/studytrip/global/exception/error/CommonErrorCode.java index 3d145ea..61f0532 100644 --- a/src/main/java/com/ject/studytrip/global/exception/error/CommonErrorCode.java +++ b/src/main/java/com/ject/studytrip/global/exception/error/CommonErrorCode.java @@ -12,6 +12,9 @@ public enum CommonErrorCode implements ErrorCode { CONSTRAINT_VIOLATION(HttpStatus.BAD_REQUEST, "요청 파라미터 또는 경로 변수 유효성 검증에 실패했습니다."), METHOD_ARGUMENT_TYPE_MISMATCH(HttpStatus.BAD_REQUEST, "요청한 메서드 파마미터의 타입이 일치하지 않습니다."), INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 에러. 관리자에게 문의하세요."), + + INVALID_ORIGIN(HttpStatus.BAD_REQUEST, "잘못된 Origin입니다."), + UNSUPPORTED_ORIGIN(HttpStatus.BAD_REQUEST, "지원하지 않는 Origin입니다."), ; private final HttpStatus status; diff --git a/src/main/java/com/ject/studytrip/global/security/JwtAuthenticationFilter.java b/src/main/java/com/ject/studytrip/global/security/JwtAuthenticationFilter.java index 4951656..2d2a5a0 100644 --- a/src/main/java/com/ject/studytrip/global/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/ject/studytrip/global/security/JwtAuthenticationFilter.java @@ -9,11 +9,9 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; -import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; -@Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final TokenService tokenService; diff --git a/src/main/java/com/ject/studytrip/global/security/OriginExtractionFilter.java b/src/main/java/com/ject/studytrip/global/security/OriginExtractionFilter.java new file mode 100644 index 0000000..0348f51 --- /dev/null +++ b/src/main/java/com/ject/studytrip/global/security/OriginExtractionFilter.java @@ -0,0 +1,51 @@ +package com.ject.studytrip.global.security; + +import static com.ject.studytrip.global.common.constants.UrlConstants.*; + +import com.google.common.net.HttpHeaders; +import com.ject.studytrip.global.exception.error.CommonErrorCode; +import com.ject.studytrip.global.util.OriginArgumentUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.web.filter.OncePerRequestFilter; + +@RequiredArgsConstructor +public class OriginExtractionFilter extends OncePerRequestFilter { + private static final String ATTRIBUTE_NAME = "origin"; + + private final SecurityResponseHandler securityResponseHandler; + + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String origin = + OriginArgumentUtil.resolveOrigin( + request.getHeader(HttpHeaders.ORIGIN), + request.getScheme(), + request.getServerName(), + request.getServerPort()); + + // origin null 검증 + if (origin == null) { + securityResponseHandler.sendResponse(response, CommonErrorCode.INVALID_ORIGIN); + return; + } + + // 허용된 origin 검증 + List allowedOrigins = Arrays.asList(CORS_DOMAINS.getUrls()); + if (!allowedOrigins.contains(origin)) { + securityResponseHandler.sendResponse(response, CommonErrorCode.UNSUPPORTED_ORIGIN); + return; + } + + request.setAttribute(ATTRIBUTE_NAME, origin); + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/ject/studytrip/global/util/OriginArgumentUtil.java b/src/main/java/com/ject/studytrip/global/util/OriginArgumentUtil.java new file mode 100644 index 0000000..11a01a2 --- /dev/null +++ b/src/main/java/com/ject/studytrip/global/util/OriginArgumentUtil.java @@ -0,0 +1,80 @@ +package com.ject.studytrip.global.util; + +import static org.springframework.util.StringUtils.hasText; + +import java.net.URI; + +public final class OriginArgumentUtil { + private static final String NULL_ORIGIN = "null"; + private static final String HTTP_SCHEME = "http"; + private static final String HTTPS_SCHEME = "https"; + private static final String SCHEME_HOST_SEPARATOR = "://"; + private static final String HOST_PORT_SEPARATOR = ":"; + private static final int DEFAULT_HTTP_PORT = 80; + private static final int DEFAULT_HTTPS_PORT = 443; + private static final int MIN_PORT = 1; + private static final int MAX_PORT = 65535; + + private OriginArgumentUtil() {} + + // Origin 추출 + public static String resolveOrigin(String origin, String scheme, String host, int port) { + // Origin 헤더 정보가 있을 경우 + if (hasText(origin)) { + if (!NULL_ORIGIN.equalsIgnoreCase(origin.trim())) { + return canonicalizeOrigin(origin); + } + } + + // 서버 정보로 구성 + String serverDerivedOrigin = + scheme + SCHEME_HOST_SEPARATOR + host + HOST_PORT_SEPARATOR + port; + return canonicalizeOrigin(serverDerivedOrigin); + } + + private static String canonicalizeOrigin(String origin) { + if (!hasText(origin)) return null; + + try { + URI uri = URI.create(origin.trim()); + + String scheme = toSupportedScheme(uri.getScheme()); + if (!hasText(scheme)) return null; + + String host = uri.getHost(); + if (!hasText(host)) return null; + + int port = uri.getPort(); + if (port != -1 && !isValidPort(port)) return null; + + String result = scheme + SCHEME_HOST_SEPARATOR + host; + if (port != -1 && !isDefaultPort(scheme, port)) { + result += HOST_PORT_SEPARATOR + port; + } + + return result; + } catch (IllegalArgumentException e) { + return null; + } + } + + private static String toSupportedScheme(String scheme) { + if (!hasText(scheme)) return null; + + String lower = scheme.trim().toLowerCase(); + if (HTTP_SCHEME.equals(lower) || HTTPS_SCHEME.equals(lower)) { + return lower; + } + + return null; + } + + private static boolean isValidPort(int port) { + return port >= MIN_PORT && port <= MAX_PORT; + } + + private static boolean isDefaultPort(String scheme, int port) { + return (HTTP_SCHEME.equals(scheme) && port == DEFAULT_HTTP_PORT) + || (HTTPS_SCHEME.equals(scheme) && port == DEFAULT_HTTPS_PORT); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9f40a0c..339ca79 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,6 +5,9 @@ spring: - redis - security +server: + forward-headers-strategy: framework + swagger: version: ${SWAGGER_VERSION:1} server-url: ${API_SERVER_URL:http://localhost:8080} diff --git a/src/test/java/com/ject/studytrip/auth/application/service/KakaoLoginServiceTest.java b/src/test/java/com/ject/studytrip/auth/application/service/KakaoLoginServiceTest.java index cc9692e..0f38c07 100644 --- a/src/test/java/com/ject/studytrip/auth/application/service/KakaoLoginServiceTest.java +++ b/src/test/java/com/ject/studytrip/auth/application/service/KakaoLoginServiceTest.java @@ -24,6 +24,7 @@ class KakaoLoginServiceTest extends BaseUnitTest { private static final String EMAIL = "choi@kakao.com"; private static final String PROFILE_IMAGE = "https://kakao.com/profile.jpg"; private static final String VALID_CODE = "valid-code"; + private static final String VALID_ORIGIN = "https://test.com"; @InjectMocks private KakaoLoginService kakaoLoginService; @@ -37,11 +38,11 @@ class GetKakaoUserInfo { @DisplayName("유효하지 않은 인가 코드를 전달하면 예외가 발생한다.") void shouldThrowExceptionWhenAuthorizationCodeIsInvalid() { // given - when(kakaoOauthProvider.getKakaoTokens(" ")) + when(kakaoOauthProvider.getKakaoTokens(" ", VALID_ORIGIN)) .thenThrow(new CustomException(AuthErrorCode.INVALID_KAKAO_AUTHORIZATION_CODE)); // when & then - assertThatThrownBy(() -> kakaoLoginService.getKakaoUserInfo(" ")) + assertThatThrownBy(() -> kakaoLoginService.getKakaoUserInfo(" ", VALID_ORIGIN)) .isInstanceOf(CustomException.class) .hasMessage(AuthErrorCode.INVALID_KAKAO_AUTHORIZATION_CODE.getMessage()); } @@ -51,29 +52,32 @@ void shouldThrowExceptionWhenAuthorizationCodeIsInvalid() { void shouldThrowExceptionWhenFetchingKakaoUserInfoFails() { // given KakaoTokenResponse tokenResponse = new KakaoTokenResponseFixture().build(); - when(kakaoOauthProvider.getKakaoTokens(VALID_CODE)).thenReturn(tokenResponse); + when(kakaoOauthProvider.getKakaoTokens(VALID_CODE, VALID_ORIGIN)) + .thenReturn(tokenResponse); when(kakaoOauthProvider.getKakaoUserInfo(tokenResponse.accessToken())) .thenThrow(new CustomException(AuthErrorCode.KAKAO_USER_INFO_FETCH_FAILED)); // when & then - assertThatThrownBy(() -> kakaoLoginService.getKakaoUserInfo(VALID_CODE)) + assertThatThrownBy(() -> kakaoLoginService.getKakaoUserInfo(VALID_CODE, VALID_ORIGIN)) .isInstanceOf(CustomException.class) .hasMessage(AuthErrorCode.KAKAO_USER_INFO_FETCH_FAILED.getMessage()); } @Test - @DisplayName("유효한 인가 코드를 전달하면 사용자 정보를 반환한다.") - void shouldReturnKakaoUserInfoResponseWhenCodeIsValid() { + @DisplayName("유효한 인가 코드와 origin을 전달하면 사용자 정보를 반환한다.") + void shouldReturnKakaoUserInfoResponseWhenCodeAndOriginAreValid() { // given KakaoTokenResponse kakaoTokenResponse = new KakaoTokenResponseFixture().build(); KakaoUserInfoResponse kakaoUserInfoResponse = new KakaoUserInfoResponseFixture().build(); - when(kakaoOauthProvider.getKakaoTokens(VALID_CODE)).thenReturn(kakaoTokenResponse); + when(kakaoOauthProvider.getKakaoTokens(VALID_CODE, VALID_ORIGIN)) + .thenReturn(kakaoTokenResponse); when(kakaoOauthProvider.getKakaoUserInfo("access-token")) .thenReturn(kakaoUserInfoResponse); // when - KakaoUserInfoResponse result = kakaoLoginService.getKakaoUserInfo(VALID_CODE); + KakaoUserInfoResponse result = + kakaoLoginService.getKakaoUserInfo(VALID_CODE, VALID_ORIGIN); // then assertThat(result.kakaoId()).isEqualTo(KAKAO_ID); diff --git a/src/test/java/com/ject/studytrip/auth/helper/KakaoOauthTestHelper.java b/src/test/java/com/ject/studytrip/auth/helper/KakaoOauthTestHelper.java index 306b8c1..a7a7fd7 100644 --- a/src/test/java/com/ject/studytrip/auth/helper/KakaoOauthTestHelper.java +++ b/src/test/java/com/ject/studytrip/auth/helper/KakaoOauthTestHelper.java @@ -22,13 +22,15 @@ public KakaoOauthTestHelper(KakaoOauthProvider kakaoOauthProvider) { public void mockSuccess( KakaoTokenResponse kakaoTokenResponse, KakaoUserInfoResponse kakaoUserInfoResponse) { - given(kakaoOauthProvider.getKakaoTokens(anyString())).willReturn(kakaoTokenResponse); + given(kakaoOauthProvider.getKakaoTokens(anyString(), anyString())) + .willReturn(kakaoTokenResponse); given(kakaoOauthProvider.getKakaoUserInfo(anyString())).willReturn(kakaoUserInfoResponse); } public void mockThrowException( KakaoTokenResponse kakaoTokenResponse, MemberErrorCode memberErrorCode) { - given(kakaoOauthProvider.getKakaoTokens(anyString())).willReturn(kakaoTokenResponse); + given(kakaoOauthProvider.getKakaoTokens(anyString(), anyString())) + .willReturn(kakaoTokenResponse); given(kakaoOauthProvider.getKakaoUserInfo(anyString())) .willThrow(new CustomException(memberErrorCode)); } diff --git a/src/test/java/com/ject/studytrip/auth/presentation/controller/AuthControllerIntegrationTest.java b/src/test/java/com/ject/studytrip/auth/presentation/controller/AuthControllerIntegrationTest.java index 64a8965..84dcdbe 100644 --- a/src/test/java/com/ject/studytrip/auth/presentation/controller/AuthControllerIntegrationTest.java +++ b/src/test/java/com/ject/studytrip/auth/presentation/controller/AuthControllerIntegrationTest.java @@ -35,6 +35,7 @@ @DisplayName("AuthController 통합 테스트") class AuthControllerIntegrationTest extends BaseIntegrationTest { private static final String BASE_AUTH_URL = "/api/auth"; + private static final String TEST_ORIGIN = "http://localhost:8080"; @Autowired private MemberTestHelper memberTestHelper; @Autowired private TokenTestHelper tokenTestHelper; @@ -73,6 +74,7 @@ class KakaoLogin { private ResultActions getResultActions(KakaoLoginRequest request) throws Exception { return mockMvc.perform( post(BASE_AUTH_URL + "/login/kakao") + .header("Origin", TEST_ORIGIN) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); } @@ -168,6 +170,7 @@ class KakaoSignup { private ResultActions getResultActions(KakaoSignupRequest request) throws Exception { return mockMvc.perform( post(BASE_AUTH_URL + "/signup/kakao") + .header("Origin", TEST_ORIGIN) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); } @@ -309,6 +312,7 @@ class Logout { private ResultActions getResultActions(LogoutRequest request) throws Exception { return mockMvc.perform( post(BASE_AUTH_URL + "/logout") + .header("Origin", TEST_ORIGIN) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); }