From 1fff6546df1c687eeed9a965cc18351d1eed71e1 Mon Sep 17 00:00:00 2001 From: KIM MIN WOO <79193811+minwoo1999@users.noreply.github.com> Date: Sun, 30 Nov 2025 14:53:46 +0900 Subject: [PATCH 1/5] [BOOK-469] feat: apis,infra google social login (#141) --- .../auth/dto/request/SocialLoginRequest.kt | 3 + .../apis/auth/manager/GoogleApiManager.kt | 20 +++ .../strategy/signin/GoogleAuthCredentials.kt | 11 ++ .../strategy/signin/GoogleSignInStrategy.kt | 53 ++++++++ apis/src/main/resources/application.yml | 5 + .../main/resources/static/kakao-login.html | 115 +++++++++++++----- .../org/yapp/domain/user/ProviderType.kt | 2 +- infra/build.gradle.kts | 3 + .../oauth/google/response/GoogleUserInfo.kt | 12 ++ 9 files changed, 193 insertions(+), 31 deletions(-) create mode 100644 apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt create mode 100644 apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/GoogleAuthCredentials.kt create mode 100644 apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/GoogleSignInStrategy.kt create mode 100644 infra/src/main/kotlin/org/yapp/infra/external/oauth/google/response/GoogleUserInfo.kt diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt index 83e2c3ff..9b17c05c 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt @@ -5,6 +5,7 @@ import jakarta.validation.constraints.NotBlank import org.yapp.apis.auth.exception.AuthErrorCode import org.yapp.apis.auth.exception.AuthException import org.yapp.apis.auth.strategy.signin.AppleAuthCredentials +import org.yapp.apis.auth.strategy.signin.GoogleAuthCredentials import org.yapp.apis.auth.strategy.signin.KakaoAuthCredentials import org.yapp.apis.auth.strategy.signin.SignInCredentials import org.yapp.domain.user.ProviderType @@ -61,6 +62,8 @@ data class SocialLoginRequest private constructor( ) AppleAuthCredentials(request.validOauthToken(), authCode) } + + ProviderType.GOOGLE -> GoogleAuthCredentials(request.validOauthToken()) } } } diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt b/apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt new file mode 100644 index 00000000..09f2ade1 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt @@ -0,0 +1,20 @@ +package org.yapp.apis.auth.manager + +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import org.springframework.web.client.RestClient +import org.yapp.infra.external.oauth.google.response.GoogleUserInfo + +@Component +class GoogleApiManager( + @Value("\${oauth.google.url.user-info}") private val userInfoUrl: String, + private val restClient: RestClient, +) { + fun getUserInfo(accessToken: String): GoogleUserInfo { + return restClient.get() + .uri(userInfoUrl) + .headers { it.setBearerAuth(accessToken) } + .retrieve() + .body(GoogleUserInfo::class.java)!! + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/GoogleAuthCredentials.kt b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/GoogleAuthCredentials.kt new file mode 100644 index 00000000..075a50e0 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/GoogleAuthCredentials.kt @@ -0,0 +1,11 @@ +package org.yapp.apis.auth.strategy.signin + +import org.yapp.domain.user.ProviderType + +data class GoogleAuthCredentials( + val accessToken: String, +) : SignInCredentials() { + override fun getProviderType(): ProviderType { + return ProviderType.GOOGLE + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/GoogleSignInStrategy.kt b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/GoogleSignInStrategy.kt new file mode 100644 index 00000000..f91f956b --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/GoogleSignInStrategy.kt @@ -0,0 +1,53 @@ +package org.yapp.apis.auth.strategy.signin + +import mu.KotlinLogging +import org.springframework.stereotype.Component +import org.yapp.apis.auth.dto.response.UserCreateInfoResponse +import org.yapp.apis.auth.exception.AuthErrorCode +import org.yapp.apis.auth.exception.AuthException +import org.yapp.apis.auth.manager.GoogleApiManager +import org.yapp.apis.auth.util.NicknameGenerator +import org.yapp.domain.user.ProviderType +import org.yapp.infra.external.oauth.google.response.GoogleUserInfo + +@Component +class GoogleSignInStrategy( + private val googleApiManager: GoogleApiManager +) : SignInStrategy { + + private val log = KotlinLogging.logger {} + + override fun getProviderType(): ProviderType = ProviderType.GOOGLE + + override fun authenticate(credentials: SignInCredentials): UserCreateInfoResponse { + return try { + val googleCredentials = validateCredentials(credentials) + val googleUser = googleApiManager.getUserInfo(googleCredentials.accessToken) + createUserInfo(googleUser) + } catch (exception: Exception) { + log.error("Google authentication failed", exception) + when (exception) { + is AuthException -> throw exception + else -> throw AuthException(AuthErrorCode.FAILED_TO_GET_USER_INFO, exception.message) + } + } + } + + private fun validateCredentials(credentials: SignInCredentials): GoogleAuthCredentials { + return credentials as? GoogleAuthCredentials + ?: throw AuthException( + AuthErrorCode.INVALID_CREDENTIALS, + "Credentials must be GoogleAuthCredentials" + ) + } + + private fun createUserInfo(googleUser: GoogleUserInfo): UserCreateInfoResponse { + return UserCreateInfoResponse.of( + email = googleUser.email ?: ("google_${googleUser.id}@google.com"), + nickname = NicknameGenerator.generate(), + profileImageUrl = googleUser.picture, + providerType = ProviderType.GOOGLE, + providerId = googleUser.id + ) + } +} diff --git a/apis/src/main/resources/application.yml b/apis/src/main/resources/application.yml index dcd73de6..cf00b4db 100644 --- a/apis/src/main/resources/application.yml +++ b/apis/src/main/resources/application.yml @@ -61,6 +61,11 @@ swagger: description: YAPP API Documentation for Development version: v1.0.0-dev +oauth: + google: + url: + user-info: https://www.googleapis.com/oauth2/v2/userinfo + --- spring: config: diff --git a/apis/src/main/resources/static/kakao-login.html b/apis/src/main/resources/static/kakao-login.html index 1c63c4d9..c5994710 100644 --- a/apis/src/main/resources/static/kakao-login.html +++ b/apis/src/main/resources/static/kakao-login.html @@ -53,38 +53,50 @@ color: #000; } - .apple-btn { - background-color: #000; - color: #fff; - } - - - - -

소셜 로그인 테스트

- -
-
카카오 로그인
-
애플 로그인
-
- -
-

카카오 계정으로 로그인하려면 아래 버튼을 클릭하세요.

-
- -
-
- -
-

애플 계정으로 로그인하려면 아래 버튼을 클릭하세요.

-
- -
-
- + .apple-btn { + background-color: #000; + color: #fff; + } + .google-btn { + background-color: #4285F4; + color: #fff; + } + + + + +

소셜 로그인 테스트

+ +
+
카카오 로그인
+
애플 로그인
+
구글 로그인
+
+ +
+

카카오 계정으로 로그인하려면 아래 버튼을 클릭하세요.

+
+ +
+
+ +
+

애플 계정으로 로그인하려면 아래 버튼을 클릭하세요.

+
+ +
+
+ +
+

구글 계정으로 로그인하려면 아래 버튼을 클릭하세요.

+
+ +
+

 
+
 
 
 
@@ -93,6 +105,26 @@ 

소셜 로그인 테스트

src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"> + // Google Sign-In Initialization + var GoogleAuth; // GoogleAuth object + + function initGoogleAuth() { + gapi.client.init({ + clientId: document.querySelector('meta[name="google-signin-client_id"]').content, + scope: 'profile email' + }).then(function () { + GoogleAuth = gapi.auth2.getAuthInstance(); + // Attach the click listener to the Google login button + document.getElementById('google-login-btn').addEventListener('click', () => { + GoogleAuth.signIn().then(onSignIn, (error) => { + console.error('Google Sign-In failed:', error); + document.getElementById('result').textContent = 'Google 로그인 실패: ' + JSON.stringify(error); + }); + }); + }); + } + + function handleGoogleClientLoad() { + gapi.load('client:auth2', initGoogleAuth); + } + + diff --git a/domain/src/main/kotlin/org/yapp/domain/user/ProviderType.kt b/domain/src/main/kotlin/org/yapp/domain/user/ProviderType.kt index 4a8aad81..ba085c91 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/ProviderType.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/ProviderType.kt @@ -4,5 +4,5 @@ package org.yapp.domain.user * Enum representing different authentication providers. */ enum class ProviderType { - KAKAO, APPLE + KAKAO, APPLE, GOOGLE } diff --git a/infra/build.gradle.kts b/infra/build.gradle.kts index 68634557..bdc24c85 100644 --- a/infra/build.gradle.kts +++ b/infra/build.gradle.kts @@ -8,6 +8,9 @@ dependencies { implementation(Dependencies.Spring.BOOT_STARTER_DATA_REDIS) implementation(Dependencies.Spring.KOTLIN_REFLECT) + + implementation(Dependencies.Spring.BOOT_STARTER_OAUTH2_CLIENT) + implementation(Dependencies.RestClient.HTTP_CLIENT5) implementation(Dependencies.RestClient.HTTP_CORE5) diff --git a/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/response/GoogleUserInfo.kt b/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/response/GoogleUserInfo.kt new file mode 100644 index 00000000..7f938491 --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/response/GoogleUserInfo.kt @@ -0,0 +1,12 @@ +package org.yapp.infra.external.oauth.google.response + +import com.fasterxml.jackson.annotation.JsonProperty + +data class GoogleUserInfo( + @JsonProperty("id") + val id: String, + @JsonProperty("email") + val email: String?, + @JsonProperty("picture") + val picture: String?, +) From ae6b5cae5d273a3b7c61c8f15d2975c5b0c36e81 Mon Sep 17 00:00:00 2001 From: KIM MIN WOO <79193811+minwoo1999@users.noreply.github.com> Date: Thu, 11 Dec 2025 23:21:40 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[BOOK-469]=20fix(ci):=20ci=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테스트 프로필에 oauth 설정 추가 - 로컬에서 sonar task 스킵되도록 수정 --- apis/src/main/resources/application.yml | 5 +++++ build.gradle.kts | 1 + 2 files changed, 6 insertions(+) diff --git a/apis/src/main/resources/application.yml b/apis/src/main/resources/application.yml index cf00b4db..aaa824b0 100644 --- a/apis/src/main/resources/application.yml +++ b/apis/src/main/resources/application.yml @@ -90,3 +90,8 @@ springdoc: enabled: false api-docs: enabled: false + +oauth: + google: + url: + user-info: https://www.googleapis.com/oauth2/v2/userinfo diff --git a/build.gradle.kts b/build.gradle.kts index d6f4ce65..6ca01291 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -192,6 +192,7 @@ sonar { // SonarQube 태스크가 통합 JaCoCo 리포트에 의존하도록 설정 tasks.named("sonar") { dependsOn("jacocoRootReport") + onlyIf { System.getenv("SONAR_TOKEN") != null } } /** From bed59baef9fbc49a41a121836ca9452c5e203486 Mon Sep 17 00:00:00 2001 From: KIM MIN WOO <79193811+minwoo1999@users.noreply.github.com> Date: Sat, 13 Dec 2025 21:50:10 +0900 Subject: [PATCH 3/5] =?UTF-8?q?[BOOK-469]=20fix:=20apis,infra=20-=20?= =?UTF-8?q?=EA=B5=AC=EA=B8=80=20=EC=86=8C=EC=85=9C=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81(#142)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apis/auth/manager/GoogleApiManager.kt | 38 ++++++++++++++----- .../strategy/signin/GoogleAuthCredentials.kt | 11 ------ .../auth/strategy/signin/SignInCredentials.kt | 13 ++++++- .../yapp/apis/config/GoogleOauthProperties.kt | 12 ++++++ .../org/yapp/apis/config/PropertiesConfig.kt | 8 ++++ .../infra/external/oauth/google/GoogleApi.kt | 22 +++++++++++ .../external/oauth/google/GoogleRestClient.kt | 24 ++++++++++++ 7 files changed, 106 insertions(+), 22 deletions(-) delete mode 100644 apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/GoogleAuthCredentials.kt create mode 100644 apis/src/main/kotlin/org/yapp/apis/config/GoogleOauthProperties.kt create mode 100644 apis/src/main/kotlin/org/yapp/apis/config/PropertiesConfig.kt create mode 100644 infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleApi.kt create mode 100644 infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleRestClient.kt diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt b/apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt index 09f2ade1..f6904908 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt @@ -1,20 +1,40 @@ package org.yapp.apis.auth.manager -import org.springframework.beans.factory.annotation.Value +import mu.KotlinLogging import org.springframework.stereotype.Component -import org.springframework.web.client.RestClient +import org.springframework.web.client.HttpClientErrorException +import org.yapp.apis.auth.exception.AuthErrorCode +import org.yapp.apis.auth.exception.AuthException +import org.yapp.apis.config.GoogleOauthProperties +import org.yapp.infra.external.oauth.google.GoogleApi import org.yapp.infra.external.oauth.google.response.GoogleUserInfo @Component class GoogleApiManager( - @Value("\${oauth.google.url.user-info}") private val userInfoUrl: String, - private val restClient: RestClient, + private val googleApi: GoogleApi, + private val googleOauthProperties: GoogleOauthProperties, ) { + private val log = KotlinLogging.logger {} + fun getUserInfo(accessToken: String): GoogleUserInfo { - return restClient.get() - .uri(userInfoUrl) - .headers { it.setBearerAuth(accessToken) } - .retrieve() - .body(GoogleUserInfo::class.java)!! + return googleApi.fetchUserInfo(accessToken, googleOauthProperties.url.userInfo) + .onSuccess { userInfo -> + log.info { "Successfully fetched Google user info for userId: ${userInfo.id}" } + } + .getOrElse { exception -> + log.error(exception) { "Failed to fetch Google user info" } + + when (exception) { + is HttpClientErrorException -> throw AuthException( + AuthErrorCode.INVALID_OAUTH_TOKEN, + "Invalid Google Access Token.", + ) + + else -> throw AuthException( + AuthErrorCode.OAUTH_SERVER_ERROR, + "Failed to communicate with Google server.", + ) + } + } } } diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/GoogleAuthCredentials.kt b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/GoogleAuthCredentials.kt deleted file mode 100644 index 075a50e0..00000000 --- a/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/GoogleAuthCredentials.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.yapp.apis.auth.strategy.signin - -import org.yapp.domain.user.ProviderType - -data class GoogleAuthCredentials( - val accessToken: String, -) : SignInCredentials() { - override fun getProviderType(): ProviderType { - return ProviderType.GOOGLE - } -} diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInCredentials.kt b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInCredentials.kt index a274481c..ba921e8f 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInCredentials.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInCredentials.kt @@ -7,14 +7,23 @@ sealed class SignInCredentials { } data class KakaoAuthCredentials( - val accessToken: String + val accessToken: String, ) : SignInCredentials() { override fun getProviderType(): ProviderType = ProviderType.KAKAO } data class AppleAuthCredentials( val idToken: String, - val authorizationCode: String + val authorizationCode: String, ) : SignInCredentials() { override fun getProviderType(): ProviderType = ProviderType.APPLE } + +data class GoogleAuthCredentials( + val accessToken: String, +) : SignInCredentials() { + override fun getProviderType(): ProviderType { + return ProviderType.GOOGLE + } +} + diff --git a/apis/src/main/kotlin/org/yapp/apis/config/GoogleOauthProperties.kt b/apis/src/main/kotlin/org/yapp/apis/config/GoogleOauthProperties.kt new file mode 100644 index 00000000..4e59c83b --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/config/GoogleOauthProperties.kt @@ -0,0 +1,12 @@ +package org.yapp.apis.config + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties(prefix = "oauth.google") +data class GoogleOauthProperties( + val url: Url +) + +data class Url( + val userInfo: String +) diff --git a/apis/src/main/kotlin/org/yapp/apis/config/PropertiesConfig.kt b/apis/src/main/kotlin/org/yapp/apis/config/PropertiesConfig.kt new file mode 100644 index 00000000..13bbe364 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/config/PropertiesConfig.kt @@ -0,0 +1,8 @@ +package org.yapp.apis.config + +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.annotation.Configuration + +@Configuration +@EnableConfigurationProperties(GoogleOauthProperties::class) +class PropertiesConfig diff --git a/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleApi.kt b/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleApi.kt new file mode 100644 index 00000000..c1dc5726 --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleApi.kt @@ -0,0 +1,22 @@ +package org.yapp.infra.external.oauth.google + +import org.springframework.stereotype.Component +import org.yapp.infra.external.oauth.google.response.GoogleUserInfo + +@Component +class GoogleApi( + private val googleRestClient: GoogleRestClient +) { + companion object { + private const val BEARER_PREFIX = "Bearer " + } + + fun fetchUserInfo( + accessToken: String, + userInfoUrl: String, + ): Result { + return runCatching { + googleRestClient.getUserInfo(BEARER_PREFIX + accessToken, userInfoUrl) + } + } +} diff --git a/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleRestClient.kt b/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleRestClient.kt new file mode 100644 index 00000000..773b8e23 --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleRestClient.kt @@ -0,0 +1,24 @@ +package org.yapp.infra.external.oauth.google + +import org.springframework.stereotype.Component +import org.springframework.web.client.RestClient +import org.yapp.infra.external.oauth.google.response.GoogleUserInfo + +@Component +class GoogleRestClient( + builder: RestClient.Builder +) { + private val client = builder.build() + + fun getUserInfo( + bearerToken: String, + url: String, + ): GoogleUserInfo { + return client.get() + .uri(url) + .header("Authorization", bearerToken) + .retrieve() + .body(GoogleUserInfo::class.java) + ?: throw IllegalStateException("Google API 응답이 null 입니다.") + } +} From 7aa722bc3eff2b2a7ef11a1e02e4e6684b91e563 Mon Sep 17 00:00:00 2001 From: KIM MIN WOO <79193811+minwoo1999@users.noreply.github.com> Date: Mon, 26 Jan 2026 21:41:45 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[BOOK-469]=20feat:=20apis,infra=20=EA=B5=AC?= =?UTF-8?q?=EA=B8=80=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=88=98=EC=A0=95(#141)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apis/build.gradle.kts | 4 ++ .../yapp/apis/auth/exception/AuthErrorCode.kt | 1 + .../helper/google/GoogleIdTokenProcessor.kt | 34 ++++++++++++++++ .../apis/auth/manager/GoogleApiManager.kt | 40 ------------------- .../strategy/signin/GoogleSignInStrategy.kt | 18 ++++----- .../auth/strategy/signin/SignInCredentials.kt | 2 +- .../yapp/apis/config/GoogleOauthProperties.kt | 3 +- apis/src/main/resources/application.yml | 1 + buildSrc/src/main/kotlin/Dependencies.kt | 6 +++ .../infra/external/oauth/google/GoogleApi.kt | 22 ---------- .../external/oauth/google/GoogleRestClient.kt | 24 ----------- .../oauth/google/response/GoogleUserInfo.kt | 12 ------ 12 files changed, 58 insertions(+), 109 deletions(-) create mode 100644 apis/src/main/kotlin/org/yapp/apis/auth/helper/google/GoogleIdTokenProcessor.kt delete mode 100644 apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt delete mode 100644 infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleApi.kt delete mode 100644 infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleRestClient.kt delete mode 100644 infra/src/main/kotlin/org/yapp/infra/external/oauth/google/response/GoogleUserInfo.kt diff --git a/apis/build.gradle.kts b/apis/build.gradle.kts index c6ab8ff1..12b005c7 100644 --- a/apis/build.gradle.kts +++ b/apis/build.gradle.kts @@ -22,6 +22,10 @@ dependencies { implementation(Dependencies.BouncyCastle.BC_PROV) implementation(Dependencies.BouncyCastle.BC_PKIX) + implementation(Dependencies.Google.API_CLIENT) + implementation(Dependencies.Google.HTTP_CLIENT_APACHE) + implementation(Dependencies.Google.HTTP_CLIENT_GSON) + kapt(Dependencies.Spring.CONFIGURATION_PROCESSOR) testImplementation(Dependencies.Spring.BOOT_STARTER_TEST) diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/exception/AuthErrorCode.kt b/apis/src/main/kotlin/org/yapp/apis/auth/exception/AuthErrorCode.kt index 3072939c..33a0c9dd 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/exception/AuthErrorCode.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/exception/AuthErrorCode.kt @@ -20,6 +20,7 @@ enum class AuthErrorCode( USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "AUTH_400_08", "사용자를 찾을 수 없습니다."), EMAIL_NOT_FOUND(HttpStatus.BAD_REQUEST, "AUTH_400_09", "이메일을 찾을 수 없습니다."), INVALID_APPLE_ID_TOKEN(HttpStatus.BAD_REQUEST, "AUTH_400_10", "유효하지 않은 Apple ID 토큰입니다."), + INVALID_GOOGLE_ID_TOKEN(HttpStatus.BAD_REQUEST, "AUTH_400_13", "유효하지 않은 Google ID 토큰입니다."), PROVIDER_TYPE_MISMATCH(HttpStatus.BAD_REQUEST, "AUTH_400_11", "요청된 공급자 타입이 실제 사용자의 공급자 타입과 일치하지 않습니다."), APPLE_REFRESH_TOKEN_MISSING(HttpStatus.BAD_REQUEST, "AUTH_400_12", "Apple 사용자 탈퇴 시 리프레시 토큰이 누락되었습니다."), KAKAO_UNLINK_FAILED(HttpStatus.BAD_REQUEST, "AUTH_400_15", "카카오 회원탈퇴 처리에 실패했습니다."), diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/helper/google/GoogleIdTokenProcessor.kt b/apis/src/main/kotlin/org/yapp/apis/auth/helper/google/GoogleIdTokenProcessor.kt new file mode 100644 index 00000000..7531bea6 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/auth/helper/google/GoogleIdTokenProcessor.kt @@ -0,0 +1,34 @@ +package org.yapp.apis.auth.helper.google + +import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken +import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier +import com.google.api.client.http.apache.v2.ApacheHttpTransport +import com.google.api.client.json.gson.GsonFactory +import org.yapp.apis.auth.exception.AuthErrorCode +import org.yapp.apis.auth.exception.AuthException +import org.yapp.apis.config.GoogleOauthProperties +import org.yapp.globalutils.annotation.Helper +import java.util.Collections + +@Helper +class GoogleIdTokenProcessor( + private val googleOauthProperties: GoogleOauthProperties, +) { + private val verifier: GoogleIdTokenVerifier = GoogleIdTokenVerifier.Builder( + ApacheHttpTransport(), + GsonFactory.getDefaultInstance() + ) + .setAudience(Collections.singletonList(googleOauthProperties.clientId)) + .build() + + fun parseAndValidate(idToken: String): GoogleIdToken.Payload { + try { + val googleIdToken = verifier.verify(idToken) + ?: throw AuthException(AuthErrorCode.INVALID_GOOGLE_ID_TOKEN, "Invalid ID token") + + return googleIdToken.payload + } catch (e: Exception) { + throw AuthException(AuthErrorCode.INVALID_GOOGLE_ID_TOKEN, e.message) + } + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt b/apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt deleted file mode 100644 index f6904908..00000000 --- a/apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt +++ /dev/null @@ -1,40 +0,0 @@ -package org.yapp.apis.auth.manager - -import mu.KotlinLogging -import org.springframework.stereotype.Component -import org.springframework.web.client.HttpClientErrorException -import org.yapp.apis.auth.exception.AuthErrorCode -import org.yapp.apis.auth.exception.AuthException -import org.yapp.apis.config.GoogleOauthProperties -import org.yapp.infra.external.oauth.google.GoogleApi -import org.yapp.infra.external.oauth.google.response.GoogleUserInfo - -@Component -class GoogleApiManager( - private val googleApi: GoogleApi, - private val googleOauthProperties: GoogleOauthProperties, -) { - private val log = KotlinLogging.logger {} - - fun getUserInfo(accessToken: String): GoogleUserInfo { - return googleApi.fetchUserInfo(accessToken, googleOauthProperties.url.userInfo) - .onSuccess { userInfo -> - log.info { "Successfully fetched Google user info for userId: ${userInfo.id}" } - } - .getOrElse { exception -> - log.error(exception) { "Failed to fetch Google user info" } - - when (exception) { - is HttpClientErrorException -> throw AuthException( - AuthErrorCode.INVALID_OAUTH_TOKEN, - "Invalid Google Access Token.", - ) - - else -> throw AuthException( - AuthErrorCode.OAUTH_SERVER_ERROR, - "Failed to communicate with Google server.", - ) - } - } - } -} diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/GoogleSignInStrategy.kt b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/GoogleSignInStrategy.kt index f91f956b..7d673b8a 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/GoogleSignInStrategy.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/GoogleSignInStrategy.kt @@ -1,18 +1,18 @@ package org.yapp.apis.auth.strategy.signin +import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken import mu.KotlinLogging import org.springframework.stereotype.Component import org.yapp.apis.auth.dto.response.UserCreateInfoResponse import org.yapp.apis.auth.exception.AuthErrorCode import org.yapp.apis.auth.exception.AuthException -import org.yapp.apis.auth.manager.GoogleApiManager +import org.yapp.apis.auth.helper.google.GoogleIdTokenProcessor import org.yapp.apis.auth.util.NicknameGenerator import org.yapp.domain.user.ProviderType -import org.yapp.infra.external.oauth.google.response.GoogleUserInfo @Component class GoogleSignInStrategy( - private val googleApiManager: GoogleApiManager + private val googleIdTokenProcessor: GoogleIdTokenProcessor, ) : SignInStrategy { private val log = KotlinLogging.logger {} @@ -22,8 +22,8 @@ class GoogleSignInStrategy( override fun authenticate(credentials: SignInCredentials): UserCreateInfoResponse { return try { val googleCredentials = validateCredentials(credentials) - val googleUser = googleApiManager.getUserInfo(googleCredentials.accessToken) - createUserInfo(googleUser) + val googleUserPayload = googleIdTokenProcessor.parseAndValidate(googleCredentials.idToken) + createUserInfo(googleUserPayload) } catch (exception: Exception) { log.error("Google authentication failed", exception) when (exception) { @@ -41,13 +41,13 @@ class GoogleSignInStrategy( ) } - private fun createUserInfo(googleUser: GoogleUserInfo): UserCreateInfoResponse { + private fun createUserInfo(googleUser: GoogleIdToken.Payload): UserCreateInfoResponse { return UserCreateInfoResponse.of( - email = googleUser.email ?: ("google_${googleUser.id}@google.com"), + email = googleUser.email ?: ("google_${googleUser.subject}@google.com"), nickname = NicknameGenerator.generate(), - profileImageUrl = googleUser.picture, + profileImageUrl = googleUser["picture"] as? String, providerType = ProviderType.GOOGLE, - providerId = googleUser.id + providerId = googleUser.subject ) } } diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInCredentials.kt b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInCredentials.kt index ba921e8f..3374e0c3 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInCredentials.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInCredentials.kt @@ -20,7 +20,7 @@ data class AppleAuthCredentials( } data class GoogleAuthCredentials( - val accessToken: String, + val idToken: String, ) : SignInCredentials() { override fun getProviderType(): ProviderType { return ProviderType.GOOGLE diff --git a/apis/src/main/kotlin/org/yapp/apis/config/GoogleOauthProperties.kt b/apis/src/main/kotlin/org/yapp/apis/config/GoogleOauthProperties.kt index 4e59c83b..d8a6c05d 100644 --- a/apis/src/main/kotlin/org/yapp/apis/config/GoogleOauthProperties.kt +++ b/apis/src/main/kotlin/org/yapp/apis/config/GoogleOauthProperties.kt @@ -4,7 +4,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties @ConfigurationProperties(prefix = "oauth.google") data class GoogleOauthProperties( - val url: Url + val url: Url, + val clientId: String ) data class Url( diff --git a/apis/src/main/resources/application.yml b/apis/src/main/resources/application.yml index aaa824b0..d9293ac7 100644 --- a/apis/src/main/resources/application.yml +++ b/apis/src/main/resources/application.yml @@ -65,6 +65,7 @@ oauth: google: url: user-info: https://www.googleapis.com/oauth2/v2/userinfo + client-id: ${GOOGLE_CLIENT_ID} --- spring: diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index e214e3bc..34827e6e 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -80,4 +80,10 @@ object Dependencies { object Firebase { const val FIREBASE_ADMIN = "com.google.firebase:firebase-admin:9.2.0" } + + object Google { + const val API_CLIENT = "com.google.api-client:google-api-client:2.2.0" + const val HTTP_CLIENT_APACHE = "com.google.http-client:google-http-client-apache-v2:1.43.3" + const val HTTP_CLIENT_GSON = "com.google.http-client:google-http-client-gson:1.43.3" + } } diff --git a/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleApi.kt b/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleApi.kt deleted file mode 100644 index c1dc5726..00000000 --- a/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleApi.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.yapp.infra.external.oauth.google - -import org.springframework.stereotype.Component -import org.yapp.infra.external.oauth.google.response.GoogleUserInfo - -@Component -class GoogleApi( - private val googleRestClient: GoogleRestClient -) { - companion object { - private const val BEARER_PREFIX = "Bearer " - } - - fun fetchUserInfo( - accessToken: String, - userInfoUrl: String, - ): Result { - return runCatching { - googleRestClient.getUserInfo(BEARER_PREFIX + accessToken, userInfoUrl) - } - } -} diff --git a/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleRestClient.kt b/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleRestClient.kt deleted file mode 100644 index 773b8e23..00000000 --- a/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleRestClient.kt +++ /dev/null @@ -1,24 +0,0 @@ -package org.yapp.infra.external.oauth.google - -import org.springframework.stereotype.Component -import org.springframework.web.client.RestClient -import org.yapp.infra.external.oauth.google.response.GoogleUserInfo - -@Component -class GoogleRestClient( - builder: RestClient.Builder -) { - private val client = builder.build() - - fun getUserInfo( - bearerToken: String, - url: String, - ): GoogleUserInfo { - return client.get() - .uri(url) - .header("Authorization", bearerToken) - .retrieve() - .body(GoogleUserInfo::class.java) - ?: throw IllegalStateException("Google API 응답이 null 입니다.") - } -} diff --git a/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/response/GoogleUserInfo.kt b/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/response/GoogleUserInfo.kt deleted file mode 100644 index 7f938491..00000000 --- a/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/response/GoogleUserInfo.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.yapp.infra.external.oauth.google.response - -import com.fasterxml.jackson.annotation.JsonProperty - -data class GoogleUserInfo( - @JsonProperty("id") - val id: String, - @JsonProperty("email") - val email: String?, - @JsonProperty("picture") - val picture: String?, -) From 71400d5904753f3572ac3374d6dfdb4a9e6e46ce Mon Sep 17 00:00:00 2001 From: KIM MIN WOO <79193811+minwoo1999@users.noreply.github.com> Date: Fri, 6 Feb 2026 22:03:23 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[BOOK-469]=20feat:=20=EA=B5=AC=EA=B8=80=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=ED=83=88=ED=87=B4=20=EA=B5=AC=ED=98=84(#141)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/WithdrawStrategyRequest.kt | 11 +++++-- .../yapp/apis/auth/exception/AuthErrorCode.kt | 4 ++- .../apis/auth/manager/GoogleApiManager.kt | 11 +++++-- .../withdraw/GoogleWithdrawStrategy.kt | 32 +++++++++++++++++++ .../yapp/apis/config/GoogleOauthProperties.kt | 10 ++++-- .../response/WithdrawTargetUserResponse.kt | 7 ++-- apis/src/main/resources/application.yml | 9 ++++-- .../main/kotlin/org/yapp/domain/user/User.kt | 12 +++++++ .../org/yapp/domain/user/UserDomainService.kt | 9 ++++++ .../domain/user/vo/WithdrawTargetUserVO.kt | 6 ++-- .../infra/external/oauth/google/GoogleApi.kt | 18 +++++++++-- .../external/oauth/google/GoogleRestClient.kt | 9 ++++++ 12 files changed, 121 insertions(+), 17 deletions(-) create mode 100644 apis/src/main/kotlin/org/yapp/apis/auth/strategy/withdraw/GoogleWithdrawStrategy.kt diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/WithdrawStrategyRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/WithdrawStrategyRequest.kt index da1591df..ff9fc72d 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/WithdrawStrategyRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/WithdrawStrategyRequest.kt @@ -35,7 +35,13 @@ data class WithdrawStrategyRequest private constructor( example = "r_abc123def456ghi789jkl0mnopqrstu", required = false ) - val appleRefreshToken: String? + val appleRefreshToken: String?, + @field:Schema( + description = "Google 로그인 시 발급받은 리프레시 토큰 (Google 로그인 회원 탈퇴 시에만 필요)", + example = "1//0g_xxxxxxxxxxxxxxxxxxxxxx", + required = false + ) + val googleRefreshToken: String? ) { companion object { fun from(response: WithdrawTargetUserResponse): WithdrawStrategyRequest { @@ -43,7 +49,8 @@ data class WithdrawStrategyRequest private constructor( userId = response.id, providerType = response.providerType, providerId = response.providerId, - appleRefreshToken = response.appleRefreshToken + appleRefreshToken = response.appleRefreshToken, + googleRefreshToken = response.googleRefreshToken ) } } diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/exception/AuthErrorCode.kt b/apis/src/main/kotlin/org/yapp/apis/auth/exception/AuthErrorCode.kt index 33a0c9dd..7295e909 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/exception/AuthErrorCode.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/exception/AuthErrorCode.kt @@ -24,6 +24,7 @@ enum class AuthErrorCode( PROVIDER_TYPE_MISMATCH(HttpStatus.BAD_REQUEST, "AUTH_400_11", "요청된 공급자 타입이 실제 사용자의 공급자 타입과 일치하지 않습니다."), APPLE_REFRESH_TOKEN_MISSING(HttpStatus.BAD_REQUEST, "AUTH_400_12", "Apple 사용자 탈퇴 시 리프레시 토큰이 누락되었습니다."), KAKAO_UNLINK_FAILED(HttpStatus.BAD_REQUEST, "AUTH_400_15", "카카오 회원탈퇴 처리에 실패했습니다."), + GOOGLE_REFRESH_TOKEN_NOT_FOUND(HttpStatus.BAD_REQUEST, "AUTH_400_16", "Google 사용자 탈퇴 시 리프레시 토큰이 누락되었습니다."), /* 401 UNAUTHORIZED */ INVALID_OAUTH_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_401_01", "잘못된 소셜 OAuth 토큰입니다."), @@ -52,7 +53,8 @@ enum class AuthErrorCode( "AUTH_500_06", "Apple에서 초기 로그인 시 리프레시 토큰을 제공하지 않았습니다." ), - KAKAO_UNLINK_RESPONSE_MISMATCH(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH_500_08", "카카오 회원탈퇴 응답이 요청과 일치하지 않습니다."); + KAKAO_UNLINK_RESPONSE_MISMATCH(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH_500_08", "카카오 회원탈퇴 응답이 요청과 일치하지 않습니다."), + FAILED_TO_REVOKE_TOKEN(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH_500_09", "토큰 철회에 실패했습니다."); override fun getHttpStatus(): HttpStatus = httpStatus override fun getCode(): String = code diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt b/apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt index f6904908..8613ac22 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt @@ -7,7 +7,7 @@ import org.yapp.apis.auth.exception.AuthErrorCode import org.yapp.apis.auth.exception.AuthException import org.yapp.apis.config.GoogleOauthProperties import org.yapp.infra.external.oauth.google.GoogleApi -import org.yapp.infra.external.oauth.google.response.GoogleUserInfo +import org.yapp.infra.external.oauth.google.response.GoogleUserInfo // Changed to GoogleUserInfo @Component class GoogleApiManager( @@ -16,7 +16,7 @@ class GoogleApiManager( ) { private val log = KotlinLogging.logger {} - fun getUserInfo(accessToken: String): GoogleUserInfo { + fun getUserInfo(accessToken: String): GoogleUserInfo { // Changed to GoogleUserInfo return googleApi.fetchUserInfo(accessToken, googleOauthProperties.url.userInfo) .onSuccess { userInfo -> log.info { "Successfully fetched Google user info for userId: ${userInfo.id}" } @@ -37,4 +37,9 @@ class GoogleApiManager( } } } -} + + fun revokeToken(token: String) { + googleApi.revokeGoogleToken(token) + .getOrThrow() + } +} \ No newline at end of file diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/strategy/withdraw/GoogleWithdrawStrategy.kt b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/withdraw/GoogleWithdrawStrategy.kt new file mode 100644 index 00000000..dedd9e28 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/withdraw/GoogleWithdrawStrategy.kt @@ -0,0 +1,32 @@ +package org.yapp.apis.auth.strategy.withdraw + +import mu.KotlinLogging +import org.springframework.stereotype.Component +import org.yapp.apis.auth.dto.request.WithdrawStrategyRequest +import org.yapp.apis.auth.exception.AuthErrorCode +import org.yapp.apis.auth.exception.AuthException +import org.yapp.apis.auth.manager.GoogleApiManager +import org.yapp.domain.user.ProviderType + +@Component +class GoogleWithdrawStrategy( + private val googleApiManager: GoogleApiManager +) : WithdrawStrategy { + + private val log = KotlinLogging.logger {} + + override fun getProviderType(): ProviderType = ProviderType.GOOGLE + + override fun withdraw(request: WithdrawStrategyRequest) { + val googleRefreshToken = request.googleRefreshToken + ?: throw AuthException(AuthErrorCode.GOOGLE_REFRESH_TOKEN_NOT_FOUND, "Google Refresh Token이 존재하지 않습니다.") + + try { + googleApiManager.revokeToken(googleRefreshToken as String) + log.info { "Google refresh token revoked successfully for user ${request.userId}" } + } catch (e: Exception) { + log.error("Failed to revoke Google token for user ${request.userId}", e) + throw AuthException(AuthErrorCode.FAILED_TO_REVOKE_TOKEN, e.message) + } + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/config/GoogleOauthProperties.kt b/apis/src/main/kotlin/org/yapp/apis/config/GoogleOauthProperties.kt index d8a6c05d..fde705c3 100644 --- a/apis/src/main/kotlin/org/yapp/apis/config/GoogleOauthProperties.kt +++ b/apis/src/main/kotlin/org/yapp/apis/config/GoogleOauthProperties.kt @@ -5,9 +5,13 @@ import org.springframework.boot.context.properties.ConfigurationProperties @ConfigurationProperties(prefix = "oauth.google") data class GoogleOauthProperties( val url: Url, - val clientId: String + val clientId: String, + val clientSecret: String? = null, + val grantType: String? = null, + val redirectUri: String? = null ) data class Url( - val userInfo: String -) + val userInfo: String, + val tokenUri: String? = null +) \ No newline at end of file diff --git a/apis/src/main/kotlin/org/yapp/apis/user/dto/response/WithdrawTargetUserResponse.kt b/apis/src/main/kotlin/org/yapp/apis/user/dto/response/WithdrawTargetUserResponse.kt index f1fd3929..9b086b4e 100644 --- a/apis/src/main/kotlin/org/yapp/apis/user/dto/response/WithdrawTargetUserResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/user/dto/response/WithdrawTargetUserResponse.kt @@ -21,7 +21,9 @@ data class WithdrawTargetUserResponse private constructor( val providerId: String, @field:Schema(description = "Apple Refresh Token (애플 회원 탈퇴 시 필요, 카카오는 null)") - val appleRefreshToken: String? = null + val appleRefreshToken: String? = null, + @field:Schema(description = "Google Refresh Token (구글 회원 탈퇴 시 필요, 카카오/애플은 null)") + val googleRefreshToken: String? = null ) { companion object { fun from(vo: WithdrawTargetUserVO): WithdrawTargetUserResponse { @@ -29,7 +31,8 @@ data class WithdrawTargetUserResponse private constructor( id = vo.id.value, providerType = vo.providerType, providerId = vo.providerId.value, - appleRefreshToken = vo.appleRefreshToken + appleRefreshToken = vo.appleRefreshToken, + googleRefreshToken = vo.googleRefreshToken ) } } diff --git a/apis/src/main/resources/application.yml b/apis/src/main/resources/application.yml index d9293ac7..652bd0f9 100644 --- a/apis/src/main/resources/application.yml +++ b/apis/src/main/resources/application.yml @@ -64,7 +64,7 @@ swagger: oauth: google: url: - user-info: https://www.googleapis.com/oauth2/v2/userinfo + user-info: ${GOOGLE_USER_INFO_URI:https://www.googleapis.com/oauth2/v2/userinfo} client-id: ${GOOGLE_CLIENT_ID} --- @@ -95,4 +95,9 @@ springdoc: oauth: google: url: - user-info: https://www.googleapis.com/oauth2/v2/userinfo + user-info: ${GOOGLE_USER_INFO_URI:https://www.googleapis.com/oauth2/v2/userinfo} + token-uri: ${GOOGLE_TOKEN_URI:test-token-uri} + client-id: ${GOOGLE_CLIENT_ID:test-client-id} + client-secret: ${GOOGLE_CLIENT_SECRET:test-client-secret} + grant-type: ${GOOGLE_GRANT_TYPE:test-grant-type} + redirect-uri: ${GOOGLE_REDIRECT_URI:test-redirect-uri} diff --git a/domain/src/main/kotlin/org/yapp/domain/user/User.kt b/domain/src/main/kotlin/org/yapp/domain/user/User.kt index d9b22605..30d41030 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/User.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/User.kt @@ -16,6 +16,7 @@ data class User private constructor( val role: Role, val termsAgreed: Boolean = false, val appleRefreshToken: String? = null, + val googleRefreshToken: String? = null, val notificationEnabled: Boolean = true, val lastActivity: LocalDateTime? = null, val createdAt: LocalDateTime? = null, @@ -42,6 +43,12 @@ data class User private constructor( ) } + fun updateGoogleRefreshToken(token: String): User { + return this.copy( + googleRefreshToken = token + ) + } + fun updateNotificationEnabled(enabled: Boolean): User { return this.copy( notificationEnabled = enabled @@ -80,6 +87,7 @@ data class User private constructor( role = Role.USER, termsAgreed = termsAgreed, appleRefreshToken = null, + googleRefreshToken = null, notificationEnabled = notificationEnabled, lastActivity = LocalDateTime.now() ) @@ -106,6 +114,7 @@ data class User private constructor( role = role, termsAgreed = termsAgreed, appleRefreshToken = null, + googleRefreshToken = null, notificationEnabled = notificationEnabled, lastActivity = LocalDateTime.now() ) @@ -121,6 +130,7 @@ data class User private constructor( role: Role, termsAgreed: Boolean = false, appleRefreshToken: String? = null, + googleRefreshToken: String? = null, notificationEnabled: Boolean = true, lastActivity: LocalDateTime? = null, createdAt: LocalDateTime? = null, @@ -137,6 +147,7 @@ data class User private constructor( role = role, termsAgreed = termsAgreed, appleRefreshToken = appleRefreshToken, + googleRefreshToken = googleRefreshToken, notificationEnabled = notificationEnabled, lastActivity = lastActivity, createdAt = createdAt, @@ -175,3 +186,4 @@ data class User private constructor( } } } + diff --git a/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt b/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt index 6b0a42fe..8aebc871 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt @@ -99,6 +99,15 @@ class UserDomainService( return UserAuthVO.newInstance(updatedUser) } + fun updateGoogleRefreshToken(userId: UUID, refreshToken: String): UserAuthVO { + val user = userRepository.findById(userId) + ?: throw UserNotFoundException(UserErrorCode.USER_NOT_FOUND) + + val updatedUser = userRepository.save(user.updateGoogleRefreshToken(refreshToken)) + + return UserAuthVO.newInstance(updatedUser) + } + fun deleteUser(userId: UUID) { val user = userRepository.findById(userId) ?: throw UserNotFoundException(UserErrorCode.USER_NOT_FOUND) diff --git a/domain/src/main/kotlin/org/yapp/domain/user/vo/WithdrawTargetUserVO.kt b/domain/src/main/kotlin/org/yapp/domain/user/vo/WithdrawTargetUserVO.kt index d14a6532..8a62bdbf 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/vo/WithdrawTargetUserVO.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/vo/WithdrawTargetUserVO.kt @@ -7,7 +7,8 @@ data class WithdrawTargetUserVO private constructor( val id: User.Id, val providerType: ProviderType, val providerId: User.ProviderId, - val appleRefreshToken: String? + val appleRefreshToken: String?, + val googleRefreshToken: String? ) { companion object { fun newInstance(user: User): WithdrawTargetUserVO { @@ -15,7 +16,8 @@ data class WithdrawTargetUserVO private constructor( id = user.id, providerType = user.providerType, providerId = user.providerId, - appleRefreshToken = user.appleRefreshToken + appleRefreshToken = user.appleRefreshToken, + googleRefreshToken = user.googleRefreshToken ) } } diff --git a/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleApi.kt b/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleApi.kt index c1dc5726..b3c10ba7 100644 --- a/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleApi.kt +++ b/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleApi.kt @@ -1,7 +1,8 @@ package org.yapp.infra.external.oauth.google import org.springframework.stereotype.Component -import org.yapp.infra.external.oauth.google.response.GoogleUserInfo +import org.springframework.util.LinkedMultiValueMap +import org.yapp.infra.external.oauth.google.response.GoogleUserInfo // Changed to GoogleUserInfo @Component class GoogleApi( @@ -9,14 +10,27 @@ class GoogleApi( ) { companion object { private const val BEARER_PREFIX = "Bearer " + private const val TOKEN = "token" } fun fetchUserInfo( accessToken: String, userInfoUrl: String, - ): Result { + ): Result { // Changed to GoogleUserInfo return runCatching { googleRestClient.getUserInfo(BEARER_PREFIX + accessToken, userInfoUrl) } } + + fun revokeGoogleToken( + token: String + ): Result { + val requestBody = LinkedMultiValueMap().apply { + add(TOKEN, token) + } + + return runCatching { + googleRestClient.revoke(requestBody) + } + } } diff --git a/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleRestClient.kt b/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleRestClient.kt index 773b8e23..42b7d085 100644 --- a/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleRestClient.kt +++ b/infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleRestClient.kt @@ -2,6 +2,7 @@ package org.yapp.infra.external.oauth.google import org.springframework.stereotype.Component import org.springframework.web.client.RestClient +import org.springframework.util.LinkedMultiValueMap import org.yapp.infra.external.oauth.google.response.GoogleUserInfo @Component @@ -21,4 +22,12 @@ class GoogleRestClient( .body(GoogleUserInfo::class.java) ?: throw IllegalStateException("Google API 응답이 null 입니다.") } + + fun revoke(requestBody: LinkedMultiValueMap) { + client.post() + .uri("https://oauth2.googleapis.com/revoke") + .body(requestBody) + .retrieve() + .body(Unit::class.java) // Google revoke API typically returns an empty body + } }