diff --git a/src/docs/asciidoc/auth-api.adoc b/src/docs/asciidoc/auth-api.adoc index fd7634b..6e5a4ff 100644 --- a/src/docs/asciidoc/auth-api.adoc +++ b/src/docs/asciidoc/auth-api.adoc @@ -45,10 +45,14 @@ include::{snippetsDir}/kakaoLogin/2/response-headers.adoc[] include::{snippetsDir}/kakaoLogin/1/response-fields.adoc[] ==== 실패 Response -실패 1. +실패 1. 유효하지 않은 인가 코드일 경우 예외가 발생한다. include::{snippetsDir}/kakaoLogin/3/http-response.adoc[] -실패 2. +실패 2. 유효하지 않은 리다이렉트 키일 경우 에외가 발생한다. (허용 키 : test, local, dev) include::{snippetsDir}/kakaoLogin/4/http-response.adoc[] +실패 3. 카카오 서버로부터 토큰 조회에 실패할 경우 예외가 발생한다. +include::{snippetsDir}/kakaoLogin/5/http-response.adoc[] +실패 4. 카카오 서버로부터 유저 정보 조회에 실패할 경우 예외가 발생한다. +include::{snippetsDir}/kakaoLogin/6/http-response.adoc[] --- diff --git a/src/main/java/com/ftm/server/adapter/in/web/auth/controller/KakaoLoginController.java b/src/main/java/com/ftm/server/adapter/in/web/auth/controller/KakaoLoginController.java index cfa551f..e381a8f 100644 --- a/src/main/java/com/ftm/server/adapter/in/web/auth/controller/KakaoLoginController.java +++ b/src/main/java/com/ftm/server/adapter/in/web/auth/controller/KakaoLoginController.java @@ -17,6 +17,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -34,7 +35,7 @@ public class KakaoLoginController { @PostMapping("/api/auth/login/kakao") public ResponseEntity> kakaoLogin( - @RequestBody KakaoLoginRequest request, + @RequestBody @Valid KakaoLoginRequest request, HttpServletRequest req, HttpServletResponse res) { SocialLoginOutcomeVo result = kakaoLoginUseCase.execute(KakaoLoginCommand.from(request)); diff --git a/src/main/java/com/ftm/server/adapter/in/web/auth/dto/request/KakaoLoginRequest.java b/src/main/java/com/ftm/server/adapter/in/web/auth/dto/request/KakaoLoginRequest.java index 927c079..a05821b 100644 --- a/src/main/java/com/ftm/server/adapter/in/web/auth/dto/request/KakaoLoginRequest.java +++ b/src/main/java/com/ftm/server/adapter/in/web/auth/dto/request/KakaoLoginRequest.java @@ -1,5 +1,7 @@ package com.ftm.server.adapter.in.web.auth.dto.request; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; import lombok.AllArgsConstructor; import lombok.Getter; @@ -7,5 +9,10 @@ @AllArgsConstructor public class KakaoLoginRequest { + @NotBlank(message = "인가 코드는 필수 요청 정보입니다.") private final String authorizationCode; + + @NotBlank(message = "리다이렉트 키는 필수 요청 정보입니다.") + @Pattern(regexp = "^(test|local|dev)$", message = "리다이렉트 키는 test, local, dev만 허용됩니다.") + private final String redirectKey; } diff --git a/src/main/java/com/ftm/server/adapter/out/oauth/kakao/KakaoOAuthClientAdapter.java b/src/main/java/com/ftm/server/adapter/out/oauth/kakao/KakaoOAuthClientAdapter.java index 11ffe2e..13a967f 100644 --- a/src/main/java/com/ftm/server/adapter/out/oauth/kakao/KakaoOAuthClientAdapter.java +++ b/src/main/java/com/ftm/server/adapter/out/oauth/kakao/KakaoOAuthClientAdapter.java @@ -30,7 +30,8 @@ public class KakaoOAuthClientAdapter @Override public KakaoAuthUser authenticate(KakaoLoginCommand command) { // Access Token 요청 - KakaoTokenResponse token = getKakaoToken(command.getAuthorizationCode()); + String redirectUri = kakaoProperties.getRedirectUriByEnv(command.getRedirectEnv()); + KakaoTokenResponse token = getKakaoToken(command.getAuthorizationCode(), redirectUri); // 카카오 유저 정보 요청 KakaoUserInfoResponse userInfo = getKakaoUserInfo(token.getAccessToken()); @@ -38,22 +39,22 @@ public KakaoAuthUser authenticate(KakaoLoginCommand command) { return KakaoAuthUser.from(userInfo.getId()); } - private KakaoTokenResponse getKakaoToken(String authorizationCode) { - return requestToken(authorizationCode).block(); + private KakaoTokenResponse getKakaoToken(String authorizationCode, String redirectUri) { + return requestToken(authorizationCode, redirectUri).block(); } private KakaoUserInfoResponse getKakaoUserInfo(String accessToken) { return requestUserInfo(accessToken).block(); } - private Mono requestToken(String authorizationCode) { + private Mono requestToken(String authorizationCode, String redirectUri) { return webClient .post() .uri(kakaoProperties.getTokenUri()) .body( BodyInserters.fromFormData("grant_type", AUTHORIZATION_GRANT_TYPE) .with("client_id", kakaoProperties.getClientId()) - .with("redirect_uri", kakaoProperties.getRedirectUri()) + .with("redirect_uri", redirectUri) .with("code", authorizationCode) .with("client_secret", kakaoProperties.getClientSecret())) .retrieve() diff --git a/src/main/java/com/ftm/server/application/command/auth/KakaoLoginCommand.java b/src/main/java/com/ftm/server/application/command/auth/KakaoLoginCommand.java index 72e4f13..a10922f 100644 --- a/src/main/java/com/ftm/server/application/command/auth/KakaoLoginCommand.java +++ b/src/main/java/com/ftm/server/application/command/auth/KakaoLoginCommand.java @@ -1,16 +1,20 @@ package com.ftm.server.application.command.auth; import com.ftm.server.adapter.in.web.auth.dto.request.KakaoLoginRequest; +import com.ftm.server.domain.enums.RedirectEnv; import lombok.Getter; @Getter public class KakaoLoginCommand extends SocialLoginCommand { - private KakaoLoginCommand(String authorizationCode) { + private final RedirectEnv redirectEnv; + + private KakaoLoginCommand(String authorizationCode, String redirectKey) { super(authorizationCode); + this.redirectEnv = RedirectEnv.from(redirectKey); } public static KakaoLoginCommand from(KakaoLoginRequest request) { - return new KakaoLoginCommand(request.getAuthorizationCode()); + return new KakaoLoginCommand(request.getAuthorizationCode(), request.getRedirectKey()); } } diff --git a/src/main/java/com/ftm/server/common/response/enums/ErrorResponseCode.java b/src/main/java/com/ftm/server/common/response/enums/ErrorResponseCode.java index f13ac22..6ffff75 100644 --- a/src/main/java/com/ftm/server/common/response/enums/ErrorResponseCode.java +++ b/src/main/java/com/ftm/server/common/response/enums/ErrorResponseCode.java @@ -30,6 +30,7 @@ public enum ErrorResponseCode { BAD_REQUEST_PAGING_INDEX_RANGE(HttpStatus.BAD_REQUEST, "E400_011", "페이지 번호는 최소 0 이상이여야 합니다."), BAD_REQUEST_PAGING_SIZE_RANGE( HttpStatus.BAD_REQUEST, "E400_012", "페이지당 개수는 최소 1 이상, 10 이하여야 합니다."), + INVALID_REDIRECT_ENV(HttpStatus.BAD_REQUEST, "E400_013", "유효하지 않은 리다이렉트 환경입니다."), // 401번 NOT_AUTHENTICATED(HttpStatus.UNAUTHORIZED, "E401_001", "인증되지 않은 사용자입니다."), diff --git a/src/main/java/com/ftm/server/domain/enums/RedirectEnv.java b/src/main/java/com/ftm/server/domain/enums/RedirectEnv.java new file mode 100644 index 0000000..17d5cac --- /dev/null +++ b/src/main/java/com/ftm/server/domain/enums/RedirectEnv.java @@ -0,0 +1,28 @@ +package com.ftm.server.domain.enums; + +import com.ftm.server.common.exception.CustomException; +import com.ftm.server.common.response.enums.ErrorResponseCode; +import java.util.Locale; + +public enum RedirectEnv { + TEST, + LOCAL, + DEV; + + public static RedirectEnv from(String value) { + if (value == null || value.isBlank()) { + return LOCAL; + } + + return switch (value.trim().toLowerCase(Locale.ROOT)) { + case "test" -> TEST; + case "local" -> LOCAL; + case "dev" -> DEV; + default -> throw new CustomException(ErrorResponseCode.INVALID_REDIRECT_ENV); + }; + } + + public String key() { + return name().toLowerCase(); + } +} diff --git a/src/main/java/com/ftm/server/infrastructure/oauth/kakao/KakaoProperties.java b/src/main/java/com/ftm/server/infrastructure/oauth/kakao/KakaoProperties.java index e212e10..e7268ca 100644 --- a/src/main/java/com/ftm/server/infrastructure/oauth/kakao/KakaoProperties.java +++ b/src/main/java/com/ftm/server/infrastructure/oauth/kakao/KakaoProperties.java @@ -1,5 +1,7 @@ package com.ftm.server.infrastructure.oauth.kakao; +import com.ftm.server.domain.enums.RedirectEnv; +import java.util.Map; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -13,7 +15,16 @@ public class KakaoProperties { private String clientId; private String clientSecret; - private String redirectUri; private String tokenUri; private String userInfoUri; + private Map redirectUris; + + public String getRedirectUriByEnv(RedirectEnv env) { + String redirectUri = redirectUris.get(env.key()); + if (redirectUri != null && !redirectUri.isBlank()) { + return redirectUri; + } + + return redirectUris.get("local"); + } } diff --git a/src/main/resources/application-security.yml b/src/main/resources/application-security.yml index 8381424..2163992 100644 --- a/src/main/resources/application-security.yml +++ b/src/main/resources/application-security.yml @@ -6,6 +6,9 @@ spring: kakao: clinet-id: ${KAKAO_CLIENT_ID} client-secret: ${KAKAO_CLIENT_SECRET} - redirect-uri: "${BASE_URL}/api/auth/kakao/callback" token-uri: "https://kauth.kakao.com/oauth/token" - user-info-uri: "https://kapi.kakao.com/v2/user/me" \ No newline at end of file + user-info-uri: "https://kapi.kakao.com/v2/user/me" + redirect-uris: + test: "http://localhost:8080/api/auth/kakao/callback" + local: "http://localhost:3000/api/auth/kakao/callback" + dev: "https://ftm-client.vercel.app/api/auth/kakao/callback" \ No newline at end of file diff --git a/src/test/java/com/ftm/server/auth/KakaoLoginTest.java b/src/test/java/com/ftm/server/auth/KakaoLoginTest.java index b6b17e3..db70985 100644 --- a/src/test/java/com/ftm/server/auth/KakaoLoginTest.java +++ b/src/test/java/com/ftm/server/auth/KakaoLoginTest.java @@ -47,7 +47,11 @@ public class KakaoLoginTest extends BaseTest { @Autowired private SaveUserImagePort saveUserImagePort; private final List requestFieldKakaoLogin = - List.of(fieldWithPath("authorizationCode").type(STRING).description("카카오 인증 코드")); + List.of( + fieldWithPath("authorizationCode").type(STRING).description("카카오 인증 코드"), + fieldWithPath("redirectKey") + .type(STRING) + .description("리다이렉트 키 (test, local, dev)")); private final List responseFieldKakaoLogin = List.of( @@ -105,7 +109,7 @@ private RestDocumentationResultHandler getDocument( UserImage userImage = UserImage.createUserImage(testUser.getId()); saveUserImagePort.saveUserDefaultImage(userImage); - KakaoLoginRequest request = new KakaoLoginRequest("test_code"); + KakaoLoginRequest request = new KakaoLoginRequest("test_code", "test"); KakaoAuthUser testKakaoUser = KakaoAuthUser.from("test_kakao_id"); given(kakaoOAuthClientPort.authenticate(any(KakaoLoginCommand.class))) .willReturn(testKakaoUser); @@ -132,7 +136,7 @@ private RestDocumentationResultHandler getDocument( @Transactional void 카카오_로그인_성공2() throws Exception { // given - KakaoLoginRequest request = new KakaoLoginRequest("test_code"); + KakaoLoginRequest request = new KakaoLoginRequest("test_code", "test"); KakaoAuthUser testKakaoUser = KakaoAuthUser.from("test_kakao_id"); given(kakaoOAuthClientPort.authenticate(any(KakaoLoginCommand.class))) .willReturn(testKakaoUser); @@ -160,7 +164,57 @@ private RestDocumentationResultHandler getDocument( @Transactional void 카카오_로그인_실패1() throws Exception { // given - KakaoLoginRequest request = new KakaoLoginRequest("test_code"); + KakaoLoginRequest request = new KakaoLoginRequest("", "test"); + + // when + ResultActions resultActions = getResultActions(request); + + // then + resultActions + .andExpect( + status().is( + ErrorResponseCode.INVALID_REQUEST_ARGUMENT + .getHttpStatus() + .value())) + .andExpect( + jsonPath("code") + .value(ErrorResponseCode.INVALID_REQUEST_ARGUMENT.getCode())) + .andDo(print()); + + // documentation + resultActions.andDo(getDocument(3, "")); + } + + @Test + @Transactional + void 카카오_로그인_실패2() throws Exception { + // given + KakaoLoginRequest request = new KakaoLoginRequest("test_code", "abc"); + + // when + ResultActions resultActions = getResultActions(request); + + // then + resultActions + .andExpect( + status().is( + ErrorResponseCode.INVALID_REQUEST_ARGUMENT + .getHttpStatus() + .value())) + .andExpect( + jsonPath("code") + .value(ErrorResponseCode.INVALID_REQUEST_ARGUMENT.getCode())) + .andDo(print()); + + // documentation + resultActions.andDo(getDocument(4, "")); + } + + @Test + @Transactional + void 카카오_로그인_실패3() throws Exception { + // given + KakaoLoginRequest request = new KakaoLoginRequest("test_code", "test"); doThrow(new CustomException(ErrorResponseCode.KAKAO_AUTH_TOKEN_EXCHANGE_FAILED)) .when(kakaoOAuthClientPort) .authenticate(any(KakaoLoginCommand.class)); @@ -183,14 +237,14 @@ private RestDocumentationResultHandler getDocument( .andDo(print()); // documentation - resultActions.andDo(getDocument(3, "")); + resultActions.andDo(getDocument(5, "")); } @Test @Transactional - void 카카오_로그인_실패2() throws Exception { + void 카카오_로그인_실패4() throws Exception { // given - KakaoLoginRequest request = new KakaoLoginRequest("test_code"); + KakaoLoginRequest request = new KakaoLoginRequest("test_code", "test"); doThrow(new CustomException(ErrorResponseCode.KAKAO_USER_PROFILE_FETCH_FAILED)) .when(kakaoOAuthClientPort) .authenticate(any(KakaoLoginCommand.class)); @@ -211,6 +265,6 @@ private RestDocumentationResultHandler getDocument( .andDo(print()); // documentation - resultActions.andDo(getDocument(4, "")); + resultActions.andDo(getDocument(6, "")); } }