From 9591b8092a418bb6a15a7c7ae445ebc75e9752f4 Mon Sep 17 00:00:00 2001 From: Sim-km Date: Sun, 4 May 2025 19:06:20 +0900 Subject: [PATCH 1/4] =?UTF-8?q?ASAP-445=20=EB=84=A4=EC=9D=B4=EB=B2=84=20ac?= =?UTF-8?q?cess=20token=20=EC=A1=B0=ED=9A=8C=20api=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/port/in/SocialLoginUsecase.kt | 10 ++- .../user/port/out/AuthInfoRetrievePort.kt | 13 +++- .../asap/bootstrap/web/auth/api/AuthApi.kt | 37 +++++++++-- .../web/auth/controller/AuthController.kt | 21 ++++-- .../web/auth/dto/OAuthAccessTokenRequest.kt | 5 ++ .../web/auth/dto/OAuthAccessTokenResponse.kt | 5 ++ .../src/main/resources/application.yml | 3 + .../auth/controller/AuthControllerTest.kt | 36 +++++++++++ .../kotlin/com/asap/client/ClientConfig.kt | 5 +- .../com/asap/client/ClientProperties.kt | 17 +++++ .../client/oauth/OAuthInfoRetrieveAdapter.kt | 11 ++++ .../asap/client/oauth/OAuthRetrieveHandler.kt | 12 ++++ .../platform/AbstractOAuthRetrieveHandler.kt | 26 ++++---- .../platform/NaverOAuthRetrieveHandler.kt | 64 +++++++++++++++++-- .../resources/application-client-local.yml | 5 ++ .../src/main/resources/application-client.yml | 5 ++ 16 files changed, 237 insertions(+), 38 deletions(-) create mode 100644 Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/dto/OAuthAccessTokenRequest.kt create mode 100644 Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/dto/OAuthAccessTokenResponse.kt create mode 100644 Infrastructure-Module/Client/src/main/kotlin/com/asap/client/ClientProperties.kt create mode 100644 Infrastructure-Module/Client/src/main/resources/application-client-local.yml create mode 100644 Infrastructure-Module/Client/src/main/resources/application-client.yml diff --git a/Application-Module/src/main/kotlin/com/asap/application/user/port/in/SocialLoginUsecase.kt b/Application-Module/src/main/kotlin/com/asap/application/user/port/in/SocialLoginUsecase.kt index f0748daf..ea3de7e4 100644 --- a/Application-Module/src/main/kotlin/com/asap/application/user/port/in/SocialLoginUsecase.kt +++ b/Application-Module/src/main/kotlin/com/asap/application/user/port/in/SocialLoginUsecase.kt @@ -1,7 +1,6 @@ package com.asap.application.user.port.`in` interface SocialLoginUsecase { - fun login(command: Command): Response data class Command( @@ -9,16 +8,15 @@ interface SocialLoginUsecase { val accessToken: String, ) - sealed class Response { - } + sealed class Response data class Success( val accessToken: String, val refreshToken: String, - val isProcessedOnboarding: Boolean + val isProcessedOnboarding: Boolean, ) : Response() data class NonRegistered( - val registerToken: String + val registerToken: String, ) : Response() -} \ No newline at end of file +} diff --git a/Application-Module/src/main/kotlin/com/asap/application/user/port/out/AuthInfoRetrievePort.kt b/Application-Module/src/main/kotlin/com/asap/application/user/port/out/AuthInfoRetrievePort.kt index 98255536..ddceb62e 100644 --- a/Application-Module/src/main/kotlin/com/asap/application/user/port/out/AuthInfoRetrievePort.kt +++ b/Application-Module/src/main/kotlin/com/asap/application/user/port/out/AuthInfoRetrievePort.kt @@ -5,7 +5,14 @@ import com.asap.application.user.vo.AuthInfo import com.asap.domain.user.enums.SocialLoginProvider interface AuthInfoRetrievePort { - @Throws(UserException.UserAuthNotFoundException::class) - fun getAuthInfo(provider: SocialLoginProvider, accessToken: String): AuthInfo -} \ No newline at end of file + fun getAuthInfo( + provider: SocialLoginProvider, + accessToken: String, + ): AuthInfo + + fun getAccessToken( + provider: SocialLoginProvider, + code: String, + ): String +} diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/api/AuthApi.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/api/AuthApi.kt index cd126725..fc7c2746 100644 --- a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/api/AuthApi.kt +++ b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/api/AuthApi.kt @@ -1,10 +1,7 @@ package com.asap.bootstrap.web.auth.api import com.asap.bootstrap.common.exception.ExceptionResponse -import com.asap.bootstrap.web.auth.dto.ReissueRequest -import com.asap.bootstrap.web.auth.dto.ReissueResponse -import com.asap.bootstrap.web.auth.dto.SocialLoginRequest -import com.asap.bootstrap.web.auth.dto.SocialLoginResponse +import com.asap.bootstrap.web.auth.dto.* import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Schema @@ -62,6 +59,38 @@ interface AuthApi { @RequestBody request: SocialLoginRequest, ): ResponseEntity + @Operation(summary = "OAuth 액세스 토큰 획득") + @PostMapping("/token/{provider}") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "액세스 토큰 획득 성공", + content = [ + Content( + mediaType = "application/json", + schema = Schema(implementation = OAuthAccessTokenResponse::class), + ), + ], + ), + ApiResponse( + responseCode = "4XX", + description = "액세스 토큰 획득 실패", + content = [ + Content( + mediaType = "application/json", + schema = Schema(implementation = ExceptionResponse::class), + ), + ], + ), + ], + ) + fun getAccessToken( + @Schema(description = "소셜 로그인 플랫폼, ex) KAKAO, GOOGLE, NAVER") + @PathVariable provider: String, + @RequestBody request: OAuthAccessTokenRequest, + ): OAuthAccessTokenResponse + @Operation(summary = "토큰 재발급") @PostMapping("/reissue") @ApiResponses( diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/controller/AuthController.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/controller/AuthController.kt index 408411e5..e86c0029 100644 --- a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/controller/AuthController.kt +++ b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/controller/AuthController.kt @@ -2,11 +2,10 @@ package com.asap.bootstrap.web.auth.controller import com.asap.application.user.port.`in`.ReissueTokenUsecase import com.asap.application.user.port.`in`.SocialLoginUsecase +import com.asap.application.user.port.out.AuthInfoRetrievePort import com.asap.bootstrap.web.auth.api.AuthApi -import com.asap.bootstrap.web.auth.dto.ReissueRequest -import com.asap.bootstrap.web.auth.dto.ReissueResponse -import com.asap.bootstrap.web.auth.dto.SocialLoginRequest -import com.asap.bootstrap.web.auth.dto.SocialLoginResponse +import com.asap.bootstrap.web.auth.dto.* +import com.asap.domain.user.enums.SocialLoginProvider import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.RestController @@ -15,6 +14,7 @@ import org.springframework.web.bind.annotation.RestController class AuthController( private val socialLoginUsecase: SocialLoginUsecase, private val reissueTokenUsecase: ReissueTokenUsecase, + private val authInfoRetrievePort: AuthInfoRetrievePort, ) : AuthApi { override fun socialLogin( provider: String, @@ -54,4 +54,17 @@ class AuthController( refreshToken = response.refreshToken, ) } + + override fun getAccessToken( + provider: String, + request: OAuthAccessTokenRequest, + ): OAuthAccessTokenResponse { + val accessToken = authInfoRetrievePort.getAccessToken( + provider = SocialLoginProvider.valueOf(provider), + code = request.code, + ) + return OAuthAccessTokenResponse( + accessToken = accessToken, + ) + } } diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/dto/OAuthAccessTokenRequest.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/dto/OAuthAccessTokenRequest.kt new file mode 100644 index 00000000..486f2d31 --- /dev/null +++ b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/dto/OAuthAccessTokenRequest.kt @@ -0,0 +1,5 @@ +package com.asap.bootstrap.web.auth.dto + +data class OAuthAccessTokenRequest( + val code: String, +) diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/dto/OAuthAccessTokenResponse.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/dto/OAuthAccessTokenResponse.kt new file mode 100644 index 00000000..c0682ec6 --- /dev/null +++ b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/dto/OAuthAccessTokenResponse.kt @@ -0,0 +1,5 @@ +package com.asap.bootstrap.web.auth.dto + +data class OAuthAccessTokenResponse( + val accessToken: String, +) diff --git a/Bootstrap-Module/src/main/resources/application.yml b/Bootstrap-Module/src/main/resources/application.yml index 15e69ef3..5321b4e8 100644 --- a/Bootstrap-Module/src/main/resources/application.yml +++ b/Bootstrap-Module/src/main/resources/application.yml @@ -5,14 +5,17 @@ spring: - security - aws - persistence + - client local: - security-local - aws-local - persistence-local + - client-local test: - security-local - aws-local - persistence-test + - client-local active: local diff --git a/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/acceptance/auth/controller/AuthControllerTest.kt b/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/acceptance/auth/controller/AuthControllerTest.kt index e2054f42..86eb87f7 100644 --- a/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/acceptance/auth/controller/AuthControllerTest.kt +++ b/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/acceptance/auth/controller/AuthControllerTest.kt @@ -4,9 +4,12 @@ import com.asap.application.user.port.`in`.LogoutUsecase import com.asap.application.user.port.`in`.ReissueTokenUsecase import com.asap.application.user.port.`in`.SocialLoginUsecase import com.asap.application.user.port.`in`.TokenResolveUsecase +import com.asap.application.user.port.out.AuthInfoRetrievePort import com.asap.bootstrap.AcceptanceSupporter +import com.asap.bootstrap.web.auth.dto.OAuthAccessTokenRequest import com.asap.bootstrap.web.auth.dto.ReissueRequest import com.asap.bootstrap.web.auth.dto.SocialLoginRequest +import com.asap.domain.user.enums.SocialLoginProvider import org.junit.jupiter.api.Test import org.mockito.BDDMockito import org.springframework.boot.test.mock.mockito.MockBean @@ -26,6 +29,9 @@ class AuthControllerTest : AcceptanceSupporter() { @MockBean private lateinit var logoutUsecase: LogoutUsecase + @MockBean + private lateinit var authInfoRetrievePort: AuthInfoRetrievePort + @Test fun socialLoginSuccessTest() { // given @@ -112,4 +118,34 @@ class AuthControllerTest : AcceptanceSupporter() { } } } + + @Test + fun getAccessTokenTest() { + // given + val provider = "KAKAO" + val code = "authorization_code" + val request = OAuthAccessTokenRequest(code) + val expectedAccessToken = "access_token" + + BDDMockito + .given(authInfoRetrievePort.getAccessToken(SocialLoginProvider.valueOf(provider), code)) + .willReturn(expectedAccessToken) + + // when + val response = + mockMvc.post("/api/v1/auth/token/{provider}", provider) { + contentType = MediaType.APPLICATION_JSON + content = objectMapper.writeValueAsString(request) + } + + // then + response.andExpect { + status { isOk() } + jsonPath("$.accessToken") { + exists() + isString() + value(expectedAccessToken) + } + } + } } diff --git a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/ClientConfig.kt b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/ClientConfig.kt index 22f49955..2495455b 100644 --- a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/ClientConfig.kt +++ b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/ClientConfig.kt @@ -1,9 +1,10 @@ package com.asap.client +import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.ComponentScan import org.springframework.context.annotation.Configuration @Configuration @ComponentScan(basePackages = ["com.asap.client"]) -class ClientConfig { -} \ No newline at end of file +@EnableConfigurationProperties(ClientProperties::class) +class ClientConfig diff --git a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/ClientProperties.kt b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/ClientProperties.kt new file mode 100644 index 00000000..1f99b216 --- /dev/null +++ b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/ClientProperties.kt @@ -0,0 +1,17 @@ +package com.asap.client + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties(prefix = "client") +class ClientProperties { + var oauth: OAuthProperties = OAuthProperties() +} + +class OAuthProperties { + var naver: NaverOAuthProperties = NaverOAuthProperties() +} + +class NaverOAuthProperties { + var clientId: String = "" + var clientSecret: String = "" +} diff --git a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/OAuthInfoRetrieveAdapter.kt b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/OAuthInfoRetrieveAdapter.kt index 762003cf..f9c3876d 100644 --- a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/OAuthInfoRetrieveAdapter.kt +++ b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/OAuthInfoRetrieveAdapter.kt @@ -30,4 +30,15 @@ class OAuthInfoRetrieveAdapter( throw UserException.UserAuthNotFoundException("OAuth 정보를 가져오는데 실패했습니다. 에러 메시지: ${e.message}") } } + + override fun getAccessToken( + provider: SocialLoginProvider, + code: String, + ): String { + val accessTokenResponse = + oAuthRetrieveHandlers[provider]?.getAccessToken(OAuthRetrieveHandler.OAuthGetAccessTokenRequest(code)) + ?: throw OAuthException.OAuthRetrieveFailedException("OAuth Access Token을 가져오는 핸들러가 존재하지 않습니다.") + + return accessTokenResponse.accessToken + } } diff --git a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/OAuthRetrieveHandler.kt b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/OAuthRetrieveHandler.kt index fb197b6d..1a64a43a 100644 --- a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/OAuthRetrieveHandler.kt +++ b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/OAuthRetrieveHandler.kt @@ -3,6 +3,9 @@ package com.asap.client.oauth interface OAuthRetrieveHandler { fun getOAuthInfo(request: OAuthRequest): OAuthResponse + fun getAccessToken(request: OAuthGetAccessTokenRequest): OAuthAccessTokenResponse = + throw UnsupportedOperationException("This operation is not supported yet.") + data class OAuthRequest( val accessToken: String, ) @@ -13,4 +16,13 @@ interface OAuthRetrieveHandler { val email: String, val profileImage: String, ) + + data class OAuthGetAccessTokenRequest( + val code: String, + ) + + data class OAuthAccessTokenResponse( + val accessToken: String, + val refreshToken: String?, + ) } diff --git a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/platform/AbstractOAuthRetrieveHandler.kt b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/platform/AbstractOAuthRetrieveHandler.kt index 9091768b..955ccbd0 100644 --- a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/platform/AbstractOAuthRetrieveHandler.kt +++ b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/platform/AbstractOAuthRetrieveHandler.kt @@ -5,20 +5,20 @@ import com.asap.client.oauth.exception.OAuthException import org.springframework.web.reactive.function.client.WebClient abstract class AbstractOAuthRetrieveHandler( - private val webClient: WebClient, + protected val webClient: WebClient, ) : OAuthRetrieveHandler { - override fun getOAuthInfo(request: OAuthRetrieveHandler.OAuthRequest): OAuthRetrieveHandler.OAuthResponse { - val response = webClient - .get() - .uri(getApiEndpoint()) - .header("Authorization", "Bearer ${request.accessToken}") - .retrieve() - .onStatus({ it.isError }, { - throw OAuthException.OAuthRetrieveFailedException(getErrorMessage()) - }) - .bodyToMono(getResponseType()) - .block() + val response = + webClient + .get() + .uri(getApiEndpoint()) + .header("Authorization", "Bearer ${request.accessToken}") + .retrieve() + .onStatus({ it.isError }, { + throw OAuthException.OAuthRetrieveFailedException(getErrorMessage()) + }) + .bodyToMono(getResponseType()) + .block() if (response == null) { throw OAuthException.OAuthRetrieveFailedException(getErrorMessage()) @@ -46,4 +46,4 @@ abstract class AbstractOAuthRetrieveHandler( * Maps the provider-specific response to a common OAuthResponse */ protected abstract fun mapToOAuthResponse(response: T): OAuthRetrieveHandler.OAuthResponse -} \ No newline at end of file +} diff --git a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/platform/NaverOAuthRetrieveHandler.kt b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/platform/NaverOAuthRetrieveHandler.kt index 1724e787..794052ec 100644 --- a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/platform/NaverOAuthRetrieveHandler.kt +++ b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/platform/NaverOAuthRetrieveHandler.kt @@ -1,31 +1,57 @@ package com.asap.client.oauth.platform +import com.asap.client.ClientProperties import com.asap.client.oauth.OAuthRetrieveHandler +import com.asap.client.oauth.exception.OAuthException +import com.fasterxml.jackson.annotation.JsonProperty import org.springframework.beans.factory.annotation.Qualifier import org.springframework.stereotype.Component import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.util.UriComponentsBuilder @Component class NaverOAuthRetrieveHandler( @Qualifier("naverWebClient") naverWebClient: WebClient, -) : AbstractOAuthRetrieveHandler(naverWebClient) { + private val clientProperties: ClientProperties, +) : AbstractOAuthRetrieveHandler(naverWebClient) { + private val naverOAuthConfig by lazy { clientProperties.oauth.naver } + + override fun getAccessToken(request: OAuthRetrieveHandler.OAuthGetAccessTokenRequest): OAuthRetrieveHandler.OAuthAccessTokenResponse { + val accessTokenUri = + NaverAccessTokenRequest( + grantType = "authorization_code", + clientId = naverOAuthConfig.clientId, + clientSecret = naverOAuthConfig.clientSecret, + code = request.code, + ).toUriComponents(getApiEndpoint()) + + val response = + webClient + .get() + .uri(accessTokenUri) + .retrieve() + .bodyToMono(NaverAccessTokenResponse::class.java) + .block() + ?: throw OAuthException.OAuthRetrieveFailedException(getErrorMessage()) + + return OAuthRetrieveHandler.OAuthAccessTokenResponse(response.accessToken, response.tokenType) + } override fun getApiEndpoint(): String = "/v1/nid/me" override fun getErrorMessage(): String = "네이버 사용자 정보를 가져오는데 실패했습니다." - override fun getResponseType(): Class = NaverApiResponse::class.java + override fun getResponseType(): Class = NaverOAuthUserInfoResponse::class.java - override fun mapToOAuthResponse(response: NaverApiResponse): OAuthRetrieveHandler.OAuthResponse { - return OAuthRetrieveHandler.OAuthResponse( + override fun mapToOAuthResponse(response: NaverOAuthUserInfoResponse): OAuthRetrieveHandler.OAuthResponse = + OAuthRetrieveHandler.OAuthResponse( username = response.response.nickname, socialId = response.response.id, email = response.response.email, profileImage = response.response.profile_image, ) - } - data class NaverApiResponse( + data class NaverOAuthUserInfoResponse( val resultcode: String, val message: String, val response: NaverUserResponse, @@ -43,4 +69,30 @@ class NaverOAuthRetrieveHandler( val birthyear: String, val mobile: String, ) + + data class NaverAccessTokenRequest( + @JsonProperty("grant_type") + val grantType: String, + @JsonProperty("client_id") + val clientId: String, + @JsonProperty("client_secret") + val clientSecret: String, + val code: String, + ) { + fun toUriComponents(baseUrl: String): String = + UriComponentsBuilder + .fromHttpUrl(baseUrl) + .queryParam("grant_type", grantType) + .queryParam("client_id", clientId) + .queryParam("client_secret", clientSecret) + .queryParam("code", code) + .toUriString() + } + + data class NaverAccessTokenResponse( + val accessToken: String, + val refreshToken: String, + val tokenType: String, + val expiresIn: Int, + ) } diff --git a/Infrastructure-Module/Client/src/main/resources/application-client-local.yml b/Infrastructure-Module/Client/src/main/resources/application-client-local.yml new file mode 100644 index 00000000..4be1bd97 --- /dev/null +++ b/Infrastructure-Module/Client/src/main/resources/application-client-local.yml @@ -0,0 +1,5 @@ +client: + oauth: + naver: + client_id: not-config-client-id + client_secret: not-config-client-secret \ No newline at end of file diff --git a/Infrastructure-Module/Client/src/main/resources/application-client.yml b/Infrastructure-Module/Client/src/main/resources/application-client.yml new file mode 100644 index 00000000..5468572e --- /dev/null +++ b/Infrastructure-Module/Client/src/main/resources/application-client.yml @@ -0,0 +1,5 @@ +client: + oauth: + naver: + client_id: ${NAVER_OAUTH_CLIENT_ID} + client_secret: ${NAVER_OAUTH_CLIENT_SECRET} \ No newline at end of file From 51b6b1acac1f67162968e412b879a08648b074c9 Mon Sep 17 00:00:00 2001 From: Sim-km Date: Sun, 4 May 2025 19:17:22 +0900 Subject: [PATCH 2/4] =?UTF-8?q?ASAP-445=20access=20token=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20api=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/asap/client/oauth/platform/NaverOAuthRetrieveHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/platform/NaverOAuthRetrieveHandler.kt b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/platform/NaverOAuthRetrieveHandler.kt index 794052ec..b35697c8 100644 --- a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/platform/NaverOAuthRetrieveHandler.kt +++ b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/platform/NaverOAuthRetrieveHandler.kt @@ -23,7 +23,7 @@ class NaverOAuthRetrieveHandler( clientId = naverOAuthConfig.clientId, clientSecret = naverOAuthConfig.clientSecret, code = request.code, - ).toUriComponents(getApiEndpoint()) + ).toUriComponents("/oauth2/token") val response = webClient From 4bd4c975da1c64c5d19e0b92f5d7719d56cf59a1 Mon Sep 17 00:00:00 2001 From: Sim-km Date: Sun, 4 May 2025 20:13:33 +0900 Subject: [PATCH 3/4] =?UTF-8?q?ASAP-445=20=EB=B3=80=EC=88=98=EB=AA=85=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20URI=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95:=20`baseUrl`=EC=9D=84?= =?UTF-8?q?=20`baseUri`=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=98=EA=B3=A0=20?= =?UTF-8?q?`fromHttpUrl`=EC=9D=84=20`fromPath`=EB=A1=9C=20=EA=B5=90?= =?UTF-8?q?=EC=B2=B4=ED=95=98=EC=97=AC=20URI=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../asap/client/oauth/platform/NaverOAuthRetrieveHandler.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/platform/NaverOAuthRetrieveHandler.kt b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/platform/NaverOAuthRetrieveHandler.kt index b35697c8..4631f041 100644 --- a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/platform/NaverOAuthRetrieveHandler.kt +++ b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/platform/NaverOAuthRetrieveHandler.kt @@ -79,9 +79,9 @@ class NaverOAuthRetrieveHandler( val clientSecret: String, val code: String, ) { - fun toUriComponents(baseUrl: String): String = + fun toUriComponents(baseUri: String): String = UriComponentsBuilder - .fromHttpUrl(baseUrl) + .fromPath(baseUri) .queryParam("grant_type", grantType) .queryParam("client_id", clientId) .queryParam("client_secret", clientSecret) From 7f01df54c6e877662c2e226dcf1aebb71ce2378b Mon Sep 17 00:00:00 2001 From: Sim-km Date: Sun, 4 May 2025 22:16:57 +0900 Subject: [PATCH 4/4] =?UTF-8?q?ASAP-445=20=EB=84=A4=EC=9D=B4=EB=B2=84=20OA?= =?UTF-8?q?uth=20=EC=83=81=ED=83=9C(state)=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=20=EC=9A=94=EC=B2=AD=20=EB=A1=9C=EC=A7=81=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/port/out/AuthInfoRetrievePort.kt | 1 + .../web/auth/controller/AuthController.kt | 10 +++-- .../web/auth/dto/OAuthAccessTokenRequest.kt | 1 + .../auth/controller/AuthControllerTest.kt | 5 ++- .../com/asap/client/ClientProperties.kt | 20 +++++----- .../client/oauth/OAuthInfoRetrieveAdapter.kt | 3 +- .../asap/client/oauth/OAuthRetrieveHandler.kt | 1 + .../asap/client/oauth/OAuthWebClientConfig.kt | 9 +++++ .../platform/NaverOAuthRetrieveHandler.kt | 37 +++++++++---------- .../platform/NaverOAuthRetrieveHandlerTest.kt | 28 ++++++++++---- 10 files changed, 71 insertions(+), 44 deletions(-) diff --git a/Application-Module/src/main/kotlin/com/asap/application/user/port/out/AuthInfoRetrievePort.kt b/Application-Module/src/main/kotlin/com/asap/application/user/port/out/AuthInfoRetrievePort.kt index ddceb62e..99238be9 100644 --- a/Application-Module/src/main/kotlin/com/asap/application/user/port/out/AuthInfoRetrievePort.kt +++ b/Application-Module/src/main/kotlin/com/asap/application/user/port/out/AuthInfoRetrievePort.kt @@ -14,5 +14,6 @@ interface AuthInfoRetrievePort { fun getAccessToken( provider: SocialLoginProvider, code: String, + state: String, ): String } diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/controller/AuthController.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/controller/AuthController.kt index e86c0029..bcd737e6 100644 --- a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/controller/AuthController.kt +++ b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/controller/AuthController.kt @@ -59,10 +59,12 @@ class AuthController( provider: String, request: OAuthAccessTokenRequest, ): OAuthAccessTokenResponse { - val accessToken = authInfoRetrievePort.getAccessToken( - provider = SocialLoginProvider.valueOf(provider), - code = request.code, - ) + val accessToken = + authInfoRetrievePort.getAccessToken( + provider = SocialLoginProvider.valueOf(provider), + code = request.code, + state = request.state, + ) return OAuthAccessTokenResponse( accessToken = accessToken, ) diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/dto/OAuthAccessTokenRequest.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/dto/OAuthAccessTokenRequest.kt index 486f2d31..dddf04af 100644 --- a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/dto/OAuthAccessTokenRequest.kt +++ b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/auth/dto/OAuthAccessTokenRequest.kt @@ -2,4 +2,5 @@ package com.asap.bootstrap.web.auth.dto data class OAuthAccessTokenRequest( val code: String, + val state: String, ) diff --git a/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/acceptance/auth/controller/AuthControllerTest.kt b/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/acceptance/auth/controller/AuthControllerTest.kt index 86eb87f7..175e1622 100644 --- a/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/acceptance/auth/controller/AuthControllerTest.kt +++ b/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/acceptance/auth/controller/AuthControllerTest.kt @@ -124,11 +124,12 @@ class AuthControllerTest : AcceptanceSupporter() { // given val provider = "KAKAO" val code = "authorization_code" - val request = OAuthAccessTokenRequest(code) + val state = "state" + val request = OAuthAccessTokenRequest(code, state) val expectedAccessToken = "access_token" BDDMockito - .given(authInfoRetrievePort.getAccessToken(SocialLoginProvider.valueOf(provider), code)) + .given(authInfoRetrievePort.getAccessToken(SocialLoginProvider.valueOf(provider), code, state)) .willReturn(expectedAccessToken) // when diff --git a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/ClientProperties.kt b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/ClientProperties.kt index 1f99b216..782535a0 100644 --- a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/ClientProperties.kt +++ b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/ClientProperties.kt @@ -3,15 +3,15 @@ package com.asap.client import org.springframework.boot.context.properties.ConfigurationProperties @ConfigurationProperties(prefix = "client") -class ClientProperties { - var oauth: OAuthProperties = OAuthProperties() -} +class ClientProperties( + var oauth: OAuthProperties = OAuthProperties(), +) -class OAuthProperties { - var naver: NaverOAuthProperties = NaverOAuthProperties() -} +class OAuthProperties( + var naver: NaverOAuthProperties = NaverOAuthProperties(), +) -class NaverOAuthProperties { - var clientId: String = "" - var clientSecret: String = "" -} +class NaverOAuthProperties( + var clientId: String = "", + var clientSecret: String = "", +) diff --git a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/OAuthInfoRetrieveAdapter.kt b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/OAuthInfoRetrieveAdapter.kt index f9c3876d..af01e081 100644 --- a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/OAuthInfoRetrieveAdapter.kt +++ b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/OAuthInfoRetrieveAdapter.kt @@ -34,9 +34,10 @@ class OAuthInfoRetrieveAdapter( override fun getAccessToken( provider: SocialLoginProvider, code: String, + state: String, ): String { val accessTokenResponse = - oAuthRetrieveHandlers[provider]?.getAccessToken(OAuthRetrieveHandler.OAuthGetAccessTokenRequest(code)) + oAuthRetrieveHandlers[provider]?.getAccessToken(OAuthRetrieveHandler.OAuthGetAccessTokenRequest(code, state)) ?: throw OAuthException.OAuthRetrieveFailedException("OAuth Access Token을 가져오는 핸들러가 존재하지 않습니다.") return accessTokenResponse.accessToken diff --git a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/OAuthRetrieveHandler.kt b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/OAuthRetrieveHandler.kt index 1a64a43a..6a2fe55d 100644 --- a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/OAuthRetrieveHandler.kt +++ b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/OAuthRetrieveHandler.kt @@ -19,6 +19,7 @@ interface OAuthRetrieveHandler { data class OAuthGetAccessTokenRequest( val code: String, + val state: String, ) data class OAuthAccessTokenResponse( diff --git a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/OAuthWebClientConfig.kt b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/OAuthWebClientConfig.kt index 980cfd35..3400f3f8 100644 --- a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/OAuthWebClientConfig.kt +++ b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/OAuthWebClientConfig.kt @@ -34,4 +34,13 @@ class OAuthWebClientConfig { .baseUrl("https://openapi.naver.com") .defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) .build() + + @Bean + @Qualifier("getNaverAccessTokenWebClient") + fun getNaverAccessTokenWebClient(): WebClient = + WebClient + .builder() + .baseUrl("https://nid.naver.com") + .defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) + .build() } diff --git a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/platform/NaverOAuthRetrieveHandler.kt b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/platform/NaverOAuthRetrieveHandler.kt index 4631f041..07426bb2 100644 --- a/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/platform/NaverOAuthRetrieveHandler.kt +++ b/Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/platform/NaverOAuthRetrieveHandler.kt @@ -12,29 +12,33 @@ import org.springframework.web.util.UriComponentsBuilder @Component class NaverOAuthRetrieveHandler( @Qualifier("naverWebClient") naverWebClient: WebClient, + @Qualifier("getNaverAccessTokenWebClient") val getNaverAccessTokenWebClient: WebClient, private val clientProperties: ClientProperties, ) : AbstractOAuthRetrieveHandler(naverWebClient) { private val naverOAuthConfig by lazy { clientProperties.oauth.naver } override fun getAccessToken(request: OAuthRetrieveHandler.OAuthGetAccessTokenRequest): OAuthRetrieveHandler.OAuthAccessTokenResponse { - val accessTokenUri = + val accessTokenUrl = NaverAccessTokenRequest( grantType = "authorization_code", clientId = naverOAuthConfig.clientId, clientSecret = naverOAuthConfig.clientSecret, code = request.code, - ).toUriComponents("/oauth2/token") + ).toUriComponents("/oauth2.0/token") val response = - webClient + getNaverAccessTokenWebClient .get() - .uri(accessTokenUri) + .uri(accessTokenUrl) .retrieve() .bodyToMono(NaverAccessTokenResponse::class.java) .block() ?: throw OAuthException.OAuthRetrieveFailedException(getErrorMessage()) - return OAuthRetrieveHandler.OAuthAccessTokenResponse(response.accessToken, response.tokenType) + return OAuthRetrieveHandler.OAuthAccessTokenResponse( + accessToken = response.accessToken, + refreshToken = response.tokenType, + ) } override fun getApiEndpoint(): String = "/v1/nid/me" @@ -45,10 +49,10 @@ class NaverOAuthRetrieveHandler( override fun mapToOAuthResponse(response: NaverOAuthUserInfoResponse): OAuthRetrieveHandler.OAuthResponse = OAuthRetrieveHandler.OAuthResponse( - username = response.response.nickname, + username = "", socialId = response.response.id, email = response.response.email, - profileImage = response.response.profile_image, + profileImage = "", ) data class NaverOAuthUserInfoResponse( @@ -59,29 +63,18 @@ class NaverOAuthRetrieveHandler( data class NaverUserResponse( val id: String, - val nickname: String, - val name: String, val email: String, - val gender: String, - val age: String, - val birthday: String, - val profile_image: String, - val birthyear: String, - val mobile: String, ) data class NaverAccessTokenRequest( - @JsonProperty("grant_type") val grantType: String, - @JsonProperty("client_id") val clientId: String, - @JsonProperty("client_secret") val clientSecret: String, val code: String, ) { - fun toUriComponents(baseUri: String): String = + fun toUriComponents(basePath: String): String = UriComponentsBuilder - .fromPath(baseUri) + .fromPath(basePath) .queryParam("grant_type", grantType) .queryParam("client_id", clientId) .queryParam("client_secret", clientSecret) @@ -90,9 +83,13 @@ class NaverOAuthRetrieveHandler( } data class NaverAccessTokenResponse( + @JsonProperty("access_token") val accessToken: String, + @JsonProperty("refresh_token") val refreshToken: String, + @JsonProperty("token_type") val tokenType: String, + @JsonProperty("expires_in") val expiresIn: Int, ) } diff --git a/Infrastructure-Module/Client/src/test/kotlin/com/asap/client/oauth/platform/NaverOAuthRetrieveHandlerTest.kt b/Infrastructure-Module/Client/src/test/kotlin/com/asap/client/oauth/platform/NaverOAuthRetrieveHandlerTest.kt index 2995104a..da7ad97f 100644 --- a/Infrastructure-Module/Client/src/test/kotlin/com/asap/client/oauth/platform/NaverOAuthRetrieveHandlerTest.kt +++ b/Infrastructure-Module/Client/src/test/kotlin/com/asap/client/oauth/platform/NaverOAuthRetrieveHandlerTest.kt @@ -1,5 +1,8 @@ package com.asap.client.oauth.platform +import com.asap.client.ClientProperties +import com.asap.client.NaverOAuthProperties +import com.asap.client.OAuthProperties import com.asap.client.oauth.OAuthRetrieveHandler import com.asap.client.oauth.exception.OAuthException import io.kotest.assertions.throwables.shouldThrow @@ -12,16 +15,29 @@ import org.springframework.web.reactive.function.client.WebClient class NaverOAuthRetrieveHandlerTest : BehaviorSpec({ - var mockWebServer = MockWebServer().also { - it.start() - } + var mockWebServer = + MockWebServer().also { + it.start() + } var naverWebClient: WebClient = WebClient .builder() .baseUrl(mockWebServer.url("/").toString()) .defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) .build() - var naverOAuthRetrieveHandler = NaverOAuthRetrieveHandler(naverWebClient) + + val config = + ClientProperties( + oauth = + OAuthProperties( + naver = + NaverOAuthProperties( + clientId = "test-client-id", + clientSecret = "test-client-secret", + ), + ), + ) + var naverOAuthRetrieveHandler = NaverOAuthRetrieveHandler(naverWebClient, naverWebClient, config) given("OAuth 요청이 성공적으로 처리되었을 때") { val accessToken = "test-access-token" @@ -58,10 +74,8 @@ class NaverOAuthRetrieveHandlerTest : val response = naverOAuthRetrieveHandler.getOAuthInfo(request) then("올바른 OAuthResponse를 반환해야 한다") { - response.username shouldBe "Test User" response.socialId shouldBe "12345" response.email shouldBe "test@example.com" - response.profileImage shouldBe "https://example.com/profile.jpg" // 요청 검증 val recordedRequest = mockWebServer.takeRequest() @@ -110,4 +124,4 @@ class NaverOAuthRetrieveHandlerTest : } } } - }) \ No newline at end of file + })