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..f6904908 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt @@ -0,0 +1,40 @@ +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 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/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/apis/src/main/resources/application.yml b/apis/src/main/resources/application.yml index dcd73de6..aaa824b0 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: @@ -85,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/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/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 } } /** 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/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 입니다.") + } +} 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?, +)