From 93ada66cea2d84fa4edc3003e2ff0774c31045bb Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Sat, 11 Apr 2026 12:37:02 +1000 Subject: [PATCH 01/22] feat: add new field in user entity --- .../smashing/domain/user/entity/User.kt | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/org/appjam/smashing/domain/user/entity/User.kt b/src/main/kotlin/org/appjam/smashing/domain/user/entity/User.kt index 79f603c0..32d385d1 100644 --- a/src/main/kotlin/org/appjam/smashing/domain/user/entity/User.kt +++ b/src/main/kotlin/org/appjam/smashing/domain/user/entity/User.kt @@ -2,6 +2,7 @@ package org.appjam.smashing.domain.user.entity import io.hypersistence.utils.hibernate.id.Tsid import jakarta.persistence.* +import org.appjam.smashing.domain.auth.enums.ProviderType import org.appjam.smashing.domain.common.entity.BaseEntity import org.appjam.smashing.domain.user.enums.Gender import org.hibernate.annotations.Comment @@ -12,7 +13,8 @@ import java.time.LocalDateTime @Entity @Table( indexes = [ - Index(name = "idx_nickname", columnList = "nickname") + Index(name = "idx_nickname", columnList = "nickname"), + Index(name = "idx_social", columnList = "social_id, provider") ] ) @Comment("유저 정보") @@ -32,8 +34,13 @@ class User( val id: String? = null, @Column(nullable = false) - @Comment("카카오 IDX") - val kakaoId: String, + @Comment("소셜 IDX") + val socialId: String, + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 10) + @Comment("소셜 제공자") + val provider: ProviderType, @Column(nullable = false, length = 50) @Comment("닉네임") @@ -85,18 +92,19 @@ class User( const val DELETED_USER_NICKNAME = "알 수 없음" fun create( - kakaoId: String, + socialId: String, + provider: ProviderType, nickname: String, gender: Gender, openchatUrl: String, region: String, ) = User( - kakaoId = kakaoId, + socialId = socialId, + provider = provider, nickname = nickname, gender = gender, openchatUrl = openchatUrl, region = region, ) } - } From 151222a914e9dbd7a224d990310384e6cc1ddc93 Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Sat, 11 Apr 2026 12:37:17 +1000 Subject: [PATCH 02/22] feat: delete unused files --- .../auth/social/kakao/KakaoApiClient.kt | 20 -------------- .../social/kakao/KakaoAuthTokenValidator.kt | 26 ------------------- .../kakao/dto/response/KakaoUserResponse.kt | 9 ------- 3 files changed, 55 deletions(-) delete mode 100644 src/main/kotlin/org/appjam/smashing/domain/auth/social/kakao/KakaoApiClient.kt delete mode 100644 src/main/kotlin/org/appjam/smashing/domain/auth/social/kakao/KakaoAuthTokenValidator.kt delete mode 100644 src/main/kotlin/org/appjam/smashing/domain/auth/social/kakao/dto/response/KakaoUserResponse.kt diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/social/kakao/KakaoApiClient.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/social/kakao/KakaoApiClient.kt deleted file mode 100644 index 72ee0382..00000000 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/social/kakao/KakaoApiClient.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.appjam.smashing.domain.auth.social.kakao - -import org.appjam.smashing.domain.auth.social.kakao.dto.response.KakaoUserResponse -import org.springframework.cloud.openfeign.FeignClient -import org.springframework.http.HttpHeaders -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RequestHeader - -/** - * [KakaoApiClient] - * - * 카카오 API 서버와 통신하여 사용자 정보를 가져오기 위한 Feign Client - */ -@FeignClient(name = "kakaoApiClient", url = "https://kapi.kakao.com") -interface KakaoApiClient { - @GetMapping("/v2/user/me") - fun getUserInfo( - @RequestHeader(HttpHeaders.AUTHORIZATION) bearerToken: String - ): KakaoUserResponse -} diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/social/kakao/KakaoAuthTokenValidator.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/social/kakao/KakaoAuthTokenValidator.kt deleted file mode 100644 index 2e575d16..00000000 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/social/kakao/KakaoAuthTokenValidator.kt +++ /dev/null @@ -1,26 +0,0 @@ -package org.appjam.smashing.domain.auth.social.kakao - -import org.appjam.smashing.global.exception.CustomException -import org.appjam.smashing.global.exception.ErrorCode -import org.springframework.stereotype.Component - -@Component -class KakaoAuthTokenValidator( - private val kakaoApiClient: KakaoApiClient -) { - fun extractKakaoId(authAccessToken: String): String = - try { - val token = if (authAccessToken.startsWith(PREFIX)) authAccessToken else "$PREFIX$authAccessToken" - - val response = kakaoApiClient.getUserInfo(token) - - response.id.toString() - - } catch (e: Exception) { - throw CustomException(ErrorCode.INVALID_KAKAO_ACCESS_TOKEN) - } - - companion object { - private const val PREFIX = "Bearer " - } -} diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/social/kakao/dto/response/KakaoUserResponse.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/social/kakao/dto/response/KakaoUserResponse.kt deleted file mode 100644 index c360d26c..00000000 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/social/kakao/dto/response/KakaoUserResponse.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.appjam.smashing.domain.auth.social.kakao.dto.response - -import com.fasterxml.jackson.databind.PropertyNamingStrategies -import com.fasterxml.jackson.databind.annotation.JsonNaming - -@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class) -data class KakaoUserResponse( - val id: Long, -) From c09842e48c7620ceeae4c78902e8a47e78bffc46 Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Sat, 11 Apr 2026 12:37:48 +1000 Subject: [PATCH 03/22] feat: change sign in dto --- .../auth/dto/command/SignInRequestCommand.kt | 5 ++++- .../domain/auth/dto/request/SignInRequest.kt | 17 +++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/dto/command/SignInRequestCommand.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/dto/command/SignInRequestCommand.kt index 789514d0..2e22f7db 100644 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/dto/command/SignInRequestCommand.kt +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/dto/command/SignInRequestCommand.kt @@ -1,5 +1,8 @@ package org.appjam.smashing.domain.auth.dto.command +import org.appjam.smashing.domain.auth.enums.ProviderType + data class SignInRequestCommand( - val accessToken: String, + val idToken: String, + val provider: ProviderType, ) diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/dto/request/SignInRequest.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/dto/request/SignInRequest.kt index 84a6eeeb..9d0b2e84 100644 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/dto/request/SignInRequest.kt +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/dto/request/SignInRequest.kt @@ -2,13 +2,18 @@ package org.appjam.smashing.domain.auth.dto.request import jakarta.validation.constraints.NotBlank import org.appjam.smashing.domain.auth.dto.command.SignInRequestCommand +import org.appjam.smashing.domain.auth.enums.ProviderType +import org.appjam.smashing.global.common.validator.annotation.ValidEnum +import org.appjam.smashing.global.extensions.ofIgnoreCase data class SignInRequest( - @field:NotBlank(message = "엑세스 토큰을 입력해주세요.") - val accessToken: String?, + @field:NotBlank(message = "idToken을 입력해주세요.") + val idToken: String?, + @field:ValidEnum(message = "잘못된 provider 값입니다.", enumClass = ProviderType::class) + val provider: String?, ) { - fun toCommand(): SignInRequestCommand = - SignInRequestCommand( - accessToken = accessToken!!, - ) + fun toCommand() = SignInRequestCommand( + idToken = idToken!!, + provider = ofIgnoreCase(provider!!), + ) } From de038fe1f247b6cc3be139e9cc1faa47bb002698 Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Sat, 11 Apr 2026 12:38:06 +1000 Subject: [PATCH 04/22] feat: change sign up dto --- .../domain/auth/dto/command/SignUpRequestCommand.kt | 4 +++- .../smashing/domain/auth/dto/request/SignUpRequest.kt | 10 +++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/dto/command/SignUpRequestCommand.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/dto/command/SignUpRequestCommand.kt index 7fdbe618..237b3e41 100644 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/dto/command/SignUpRequestCommand.kt +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/dto/command/SignUpRequestCommand.kt @@ -1,10 +1,12 @@ package org.appjam.smashing.domain.auth.dto.command +import org.appjam.smashing.domain.auth.enums.ProviderType import org.appjam.smashing.domain.sport.enums.ExperienceRange import org.appjam.smashing.domain.user.enums.Gender data class SignUpRequestCommand( - val kakaoId: String, + val socialId: String, + val provider: ProviderType, val nickname: String, val gender: Gender, val openChatUrl: String, diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/dto/request/SignUpRequest.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/dto/request/SignUpRequest.kt index 40a2bfb8..cdb7d907 100644 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/dto/request/SignUpRequest.kt +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/dto/request/SignUpRequest.kt @@ -2,14 +2,17 @@ package org.appjam.smashing.domain.auth.dto.request import jakarta.validation.constraints.NotBlank import org.appjam.smashing.domain.auth.dto.command.SignUpRequestCommand +import org.appjam.smashing.domain.auth.enums.ProviderType import org.appjam.smashing.domain.sport.enums.ExperienceRange import org.appjam.smashing.domain.user.enums.Gender import org.appjam.smashing.global.common.validator.annotation.ValidEnum import org.appjam.smashing.global.extensions.ofIgnoreCase data class SignUpRequest( - @field:NotBlank(message = "kakaoId를 입력해주세요.") - val kakaoId: String?, + @field:NotBlank(message = "socialId를 입력해주세요.") + val socialId: String?, + @field:ValidEnum(message = "잘못된 provider 값입니다.", enumClass = ProviderType::class) + val provider: String?, @field:NotBlank(message = "nickname을 입력해주세요.") val nickname: String?, @field:NotBlank(message = "gender를 입력해주세요.") @@ -26,7 +29,8 @@ data class SignUpRequest( val region: String?, ) { fun toCommand() = SignUpRequestCommand( - kakaoId = kakaoId!!, + socialId = socialId!!, + provider = ofIgnoreCase(provider!!), nickname = nickname!!, gender = ofIgnoreCase(gender!!), openChatUrl = openChatUrl!!, From 1f8c9679460ddf7ae7a639ff94c5508fed44cc39 Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Sat, 11 Apr 2026 12:38:17 +1000 Subject: [PATCH 05/22] feat: change sign in response dto --- .../smashing/domain/auth/dto/response/SignInResponse.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/dto/response/SignInResponse.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/dto/response/SignInResponse.kt index d987460b..c5e50410 100644 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/dto/response/SignInResponse.kt +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/dto/response/SignInResponse.kt @@ -3,7 +3,7 @@ package org.appjam.smashing.domain.auth.dto.response data class SignInResponse( val accessToken: String?, val refreshToken: String?, - val kakaoId: String, + val socialId: String, val userId: String?, val nickname: String?, ) { @@ -13,13 +13,13 @@ data class SignInResponse( fun from( accessToken: String? = null, refreshToken: String? = null, - kakaoId: String, + socialId: String, userId: String? = null, nickname: String? = null, ) = SignInResponse( accessToken = accessToken, refreshToken = refreshToken, - kakaoId = kakaoId, + socialId = socialId, userId = userId, nickname = nickname, ) From bb57b7d799181e11cda0bc6be49a7164a02dd830 Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Sat, 11 Apr 2026 12:39:40 +1000 Subject: [PATCH 06/22] feat: make OIDC token validator --- .../domain/auth/social/OidcTokenValidator.kt | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt new file mode 100644 index 00000000..fd4dcad6 --- /dev/null +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt @@ -0,0 +1,57 @@ +package org.appjam.smashing.domain.auth.social + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import io.jsonwebtoken.Jwts +import org.appjam.smashing.global.exception.CustomException +import org.appjam.smashing.global.exception.ErrorCode +import org.springframework.stereotype.Component +import org.springframework.web.client.RestTemplate +import java.math.BigInteger +import java.security.KeyFactory +import java.security.PublicKey +import java.security.spec.RSAPublicKeySpec +import java.util.* + +@Component +class OidcTokenValidator { + fun extractSocialId( + idToken: String, + jwksUri: String, + iss: String, + ): String = try { + val publicKey = getPublicKey(idToken, jwksUri) + + val claims = Jwts.parserBuilder() + .setSigningKey(publicKey) + .build() + .parseClaimsJws(idToken) + .body + + require(claims.issuer == iss) { "invalid iss" } + + claims.subject + } catch (e: Exception) { + throw CustomException(ErrorCode.INVALID_ACCESS_TOKEN) // todo: change + } + + private fun getPublicKey( + idToken: String, + jwksUri: String, + ): PublicKey { + val header = String(Base64.getUrlDecoder().decode(idToken.split(".")[0])) + val kid = ObjectMapper().readTree(header).get("kid").asText() + + val jwks = RestTemplate().getForObject(jwksUri, JsonNode::class.java) + ?: throw CustomException(ErrorCode.INVALID_ACCESS_TOKEN) // todo: change + + val key = jwks["keys"].find { it["kid"].asText() == kid } + ?: throw CustomException(ErrorCode.INVALID_ACCESS_TOKEN) // todo: change + + val n = BigInteger(1, Base64.getUrlDecoder().decode(key["n"].asText())) + val e = BigInteger(1, Base64.getUrlDecoder().decode(key["e"].asText())) + + return KeyFactory.getInstance("RSA") + .generatePublic(RSAPublicKeySpec(n, e)) + } +} From 254433fc69151d18ab64916a0394e8883db8d630 Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Sat, 11 Apr 2026 12:39:50 +1000 Subject: [PATCH 07/22] feat: make kakao and apple validator --- .../auth/social/apple/AppleOidcValidator.kt | 21 +++++++++++++++++++ .../auth/social/kakao/KakaoOidcValidator.kt | 21 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 src/main/kotlin/org/appjam/smashing/domain/auth/social/apple/AppleOidcValidator.kt create mode 100644 src/main/kotlin/org/appjam/smashing/domain/auth/social/kakao/KakaoOidcValidator.kt diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/social/apple/AppleOidcValidator.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/social/apple/AppleOidcValidator.kt new file mode 100644 index 00000000..4ad5afef --- /dev/null +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/social/apple/AppleOidcValidator.kt @@ -0,0 +1,21 @@ +package org.appjam.smashing.domain.auth.social.apple + +import org.appjam.smashing.domain.auth.social.OidcTokenValidator +import org.springframework.stereotype.Component + +@Component +class AppleOidcValidator( + private val oidcTokenValidator: OidcTokenValidator, +) { + fun extractAppleId(idToken: String): String = + oidcTokenValidator.extractSocialId( + idToken = idToken, + jwksUri = JWKS_URI, + iss = ISS, + ) + + companion object { + private const val JWKS_URI = "https://appleid.apple.com/auth/keys" + private const val ISS = "https://appleid.apple.com" + } +} diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/social/kakao/KakaoOidcValidator.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/social/kakao/KakaoOidcValidator.kt new file mode 100644 index 00000000..a46ed85e --- /dev/null +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/social/kakao/KakaoOidcValidator.kt @@ -0,0 +1,21 @@ +package org.appjam.smashing.domain.auth.social.kakao + +import org.appjam.smashing.domain.auth.social.OidcTokenValidator +import org.springframework.stereotype.Component + +@Component +class KakaoOidcValidator( + private val oidcTokenValidator: OidcTokenValidator, +) { + fun extractKakaoId(idToken: String): String = + oidcTokenValidator.extractSocialId( + idToken = idToken, + jwksUri = JWKS_URI, + iss = ISS, + ) + + companion object { + private const val JWKS_URI = "https://kauth.kakao.com/.well-known/jwks.json" + private const val ISS = "https://kauth.kakao.com" + } +} From 38cf547c114dbb0c4fa31cae437281bdf7b04b32 Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Sat, 11 Apr 2026 12:40:05 +1000 Subject: [PATCH 08/22] feat: change login endpoint --- .../smashing/domain/auth/controller/AuthController.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/controller/AuthController.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/controller/AuthController.kt index 4c79d953..445ad846 100644 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/controller/AuthController.kt +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/controller/AuthController.kt @@ -24,10 +24,10 @@ class AuthController( private val authService: AuthService, ) { @Operation( - summary = "카카오 로그인 API", - description = "카카오 로그인을 진행합니다." + summary = "소셜 로그인 API", + description = "소셜 로그인을 진행합니다." ) - @PostMapping("/login/kakao") + @PostMapping("/login") fun signIn( @Valid @RequestBody signInRequest: SignInRequest ): ResponseEntity> { From 0ee47979fa5ad38657fda3b2ba2923c33fba8a45 Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Sat, 11 Apr 2026 12:40:21 +1000 Subject: [PATCH 09/22] feat: change sign in service parameter --- .../smashing/domain/auth/service/AuthService.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/service/AuthService.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/service/AuthService.kt index 2494cdfa..b42048ba 100644 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/service/AuthService.kt +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/service/AuthService.kt @@ -42,11 +42,11 @@ class AuthService( fun signIn( requestCommand: SignInRequestCommand, ): SignInResponse { - val kakaoId = socialAuthServiceManager.getKakaoId(requestCommand.accessToken) + val (provider, socialId) = socialAuthServiceManager.getSocialId(requestCommand) - val user = userRepository.findByKakaoId(kakaoId) + val user = userRepository.findBySocialIdAndProvider(socialId, provider.name) ?: return SignInResponse.from( - kakaoId = kakaoId, + socialId = socialId, ) val userId = user.id ?: throw CustomException(ErrorCode.USER_NOT_FOUND) @@ -56,7 +56,7 @@ class AuthService( return SignInResponse.from( accessToken = token.accessToken.token, refreshToken = token.refreshToken.token, - kakaoId = kakaoId, + socialId = socialId, userId = userId, nickname = user.nickname, ) @@ -81,7 +81,8 @@ class AuthService( val user = userRepository.save( User.create( - kakaoId = requestCommand.kakaoId, + socialId = requestCommand.socialId, + provider = requestCommand.provider, nickname = requestCommand.nickname, gender = requestCommand.gender, openchatUrl = requestCommand.openChatUrl.trim(), @@ -173,7 +174,7 @@ class AuthService( } private fun validateUser(requestCommand: SignUpRequestCommand) { - if (userRepository.existsByKakaoId(requestCommand.kakaoId)) { + if (userRepository.existsBySocialId(requestCommand.socialId)) { throw CustomException(ErrorCode.DUPLICATE_KAKAO_ID) } From 43b57c23a781741db8c83b7a08f1e7fb9e587e13 Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Sat, 11 Apr 2026 12:40:30 +1000 Subject: [PATCH 10/22] feat: make provider enum type --- .../org/appjam/smashing/domain/auth/enums/ProviderType.kt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/main/kotlin/org/appjam/smashing/domain/auth/enums/ProviderType.kt diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/enums/ProviderType.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/enums/ProviderType.kt new file mode 100644 index 00000000..cd6132ba --- /dev/null +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/enums/ProviderType.kt @@ -0,0 +1,5 @@ +package org.appjam.smashing.domain.auth.enums + +enum class ProviderType { + KAKAO, APPLE +} From 2882fbe529b4fc90272671b9bdb461996a0e7731 Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Sat, 11 Apr 2026 12:40:42 +1000 Subject: [PATCH 11/22] feat: change social service and repository --- .../auth/social/SocialAuthServiceManager.kt | 19 ++++++++++++++++--- .../domain/user/repository/UserRepository.kt | 8 ++++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/social/SocialAuthServiceManager.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/social/SocialAuthServiceManager.kt index 86b696c3..ff3990a7 100644 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/social/SocialAuthServiceManager.kt +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/social/SocialAuthServiceManager.kt @@ -1,11 +1,24 @@ package org.appjam.smashing.domain.auth.social -import org.appjam.smashing.domain.auth.social.kakao.KakaoAuthTokenValidator +import org.appjam.smashing.domain.auth.dto.command.SignInRequestCommand +import org.appjam.smashing.domain.auth.enums.ProviderType +import org.appjam.smashing.domain.auth.enums.ProviderType.APPLE +import org.appjam.smashing.domain.auth.enums.ProviderType.KAKAO +import org.appjam.smashing.domain.auth.social.apple.AppleOidcValidator +import org.appjam.smashing.domain.auth.social.kakao.KakaoOidcValidator import org.springframework.stereotype.Component @Component class SocialAuthServiceManager( - private val kakaoAuthTokenValidator: KakaoAuthTokenValidator, + private val kakaoOidcValidator: KakaoOidcValidator, + private val appleOidcValidator: AppleOidcValidator, ) { - fun getKakaoId(authAccessToken: String): String = kakaoAuthTokenValidator.extractKakaoId(authAccessToken) + fun getSocialId(command: SignInRequestCommand): Pair { + val idToken = command.idToken + + return when (command.provider) { + KAKAO -> Pair(KAKAO, kakaoOidcValidator.extractKakaoId(idToken)) + APPLE -> Pair(APPLE, appleOidcValidator.extractAppleId(idToken)) + } + } } diff --git a/src/main/kotlin/org/appjam/smashing/domain/user/repository/UserRepository.kt b/src/main/kotlin/org/appjam/smashing/domain/user/repository/UserRepository.kt index 9d795803..3542294f 100644 --- a/src/main/kotlin/org/appjam/smashing/domain/user/repository/UserRepository.kt +++ b/src/main/kotlin/org/appjam/smashing/domain/user/repository/UserRepository.kt @@ -5,8 +5,7 @@ import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query interface UserRepository : JpaRepository { - fun findByKakaoId(kakaoId: String): User? - fun existsByKakaoId(kakaoId: String): Boolean + fun existsBySocialId(socialId: String): Boolean fun existsByNickname(nickname: String): Boolean fun existsByOpenchatUrl(openChatUrl: String): Boolean @@ -21,4 +20,9 @@ interface UserRepository : JpaRepository { fun findByIdIncludingDeleted( id: String, ): User? + + fun findBySocialIdAndProvider( + socialId: String, + provider: String, + ): User? } From d29fea2c78543e9859f44a031a89db35c49adb61 Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Sat, 11 Apr 2026 12:59:38 +1000 Subject: [PATCH 12/22] feat: make SocialType response --- .../domain/auth/dto/response/SocialType.kt | 8 ++++++++ .../auth/social/SocialAuthServiceManager.kt | 15 +++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/org/appjam/smashing/domain/auth/dto/response/SocialType.kt diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/dto/response/SocialType.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/dto/response/SocialType.kt new file mode 100644 index 00000000..7ab8913f --- /dev/null +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/dto/response/SocialType.kt @@ -0,0 +1,8 @@ +package org.appjam.smashing.domain.auth.dto.response + +import org.appjam.smashing.domain.auth.enums.ProviderType + +data class SocialType( + val provider: ProviderType, + val socialId: String, +) diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/social/SocialAuthServiceManager.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/social/SocialAuthServiceManager.kt index ff3990a7..c067a244 100644 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/social/SocialAuthServiceManager.kt +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/social/SocialAuthServiceManager.kt @@ -1,7 +1,7 @@ package org.appjam.smashing.domain.auth.social import org.appjam.smashing.domain.auth.dto.command.SignInRequestCommand -import org.appjam.smashing.domain.auth.enums.ProviderType +import org.appjam.smashing.domain.auth.dto.response.SocialType import org.appjam.smashing.domain.auth.enums.ProviderType.APPLE import org.appjam.smashing.domain.auth.enums.ProviderType.KAKAO import org.appjam.smashing.domain.auth.social.apple.AppleOidcValidator @@ -13,12 +13,19 @@ class SocialAuthServiceManager( private val kakaoOidcValidator: KakaoOidcValidator, private val appleOidcValidator: AppleOidcValidator, ) { - fun getSocialId(command: SignInRequestCommand): Pair { + fun getSocialId(command: SignInRequestCommand): SocialType { val idToken = command.idToken return when (command.provider) { - KAKAO -> Pair(KAKAO, kakaoOidcValidator.extractKakaoId(idToken)) - APPLE -> Pair(APPLE, appleOidcValidator.extractAppleId(idToken)) + KAKAO -> SocialType( + provider = KAKAO, + socialId = kakaoOidcValidator.extractKakaoId(idToken), + ) + + APPLE -> SocialType( + provider = APPLE, + socialId = appleOidcValidator.extractAppleId(idToken), + ) } } } From 1703ce77ab595e204ec29ebc9c5f0d7a97ac4333 Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Sat, 11 Apr 2026 12:59:51 +1000 Subject: [PATCH 13/22] feat: match the line convention --- .../appjam/smashing/domain/auth/service/AuthService.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/service/AuthService.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/service/AuthService.kt index b42048ba..8ff22487 100644 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/service/AuthService.kt +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/service/AuthService.kt @@ -44,10 +44,10 @@ class AuthService( ): SignInResponse { val (provider, socialId) = socialAuthServiceManager.getSocialId(requestCommand) - val user = userRepository.findBySocialIdAndProvider(socialId, provider.name) - ?: return SignInResponse.from( - socialId = socialId, - ) + val user = userRepository.findBySocialIdAndProvider( + socialId = socialId, + provider = provider, + ) ?: return SignInResponse.from(socialId = socialId) val userId = user.id ?: throw CustomException(ErrorCode.USER_NOT_FOUND) From 01aebac8bf1baf9288cb10eee0bbbf67ec1c93a7 Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Sat, 11 Apr 2026 13:00:02 +1000 Subject: [PATCH 14/22] feat: fix repository function parameter type --- .../appjam/smashing/domain/user/repository/UserRepository.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/org/appjam/smashing/domain/user/repository/UserRepository.kt b/src/main/kotlin/org/appjam/smashing/domain/user/repository/UserRepository.kt index 3542294f..243cc4b3 100644 --- a/src/main/kotlin/org/appjam/smashing/domain/user/repository/UserRepository.kt +++ b/src/main/kotlin/org/appjam/smashing/domain/user/repository/UserRepository.kt @@ -1,5 +1,6 @@ package org.appjam.smashing.domain.user.repository +import org.appjam.smashing.domain.auth.enums.ProviderType import org.appjam.smashing.domain.user.entity.User import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query @@ -23,6 +24,6 @@ interface UserRepository : JpaRepository { fun findBySocialIdAndProvider( socialId: String, - provider: String, + provider: ProviderType, ): User? } From 4b4937bf838ef08916600fae68c56e500f8deeea Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Sat, 11 Apr 2026 13:13:18 +1000 Subject: [PATCH 15/22] feat: fix social login error type --- .../smashing/domain/auth/service/AuthService.kt | 2 +- .../domain/auth/social/OidcTokenValidator.kt | 13 ++++++++----- .../appjam/smashing/global/exception/ErrorCode.kt | 9 +++++---- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/service/AuthService.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/service/AuthService.kt index 8ff22487..0e1111e2 100644 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/service/AuthService.kt +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/service/AuthService.kt @@ -175,7 +175,7 @@ class AuthService( private fun validateUser(requestCommand: SignUpRequestCommand) { if (userRepository.existsBySocialId(requestCommand.socialId)) { - throw CustomException(ErrorCode.DUPLICATE_KAKAO_ID) + throw CustomException(ErrorCode.DUPLICATE_SOCIAL_ID) } if (userRepository.existsByNickname(requestCommand.nickname)) { diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt index fd4dcad6..2b863cc0 100644 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt @@ -20,7 +20,10 @@ class OidcTokenValidator { jwksUri: String, iss: String, ): String = try { - val publicKey = getPublicKey(idToken, jwksUri) + val publicKey = getPublicKey( + idToken = idToken, + jwksUri = jwksUri, + ) val claims = Jwts.parserBuilder() .setSigningKey(publicKey) @@ -28,11 +31,11 @@ class OidcTokenValidator { .parseClaimsJws(idToken) .body - require(claims.issuer == iss) { "invalid iss" } + if (claims.issuer != iss) throw CustomException(ErrorCode.INVALID_ISS) claims.subject } catch (e: Exception) { - throw CustomException(ErrorCode.INVALID_ACCESS_TOKEN) // todo: change + throw CustomException(ErrorCode.INVALID_ID_TOKEN) } private fun getPublicKey( @@ -43,10 +46,10 @@ class OidcTokenValidator { val kid = ObjectMapper().readTree(header).get("kid").asText() val jwks = RestTemplate().getForObject(jwksUri, JsonNode::class.java) - ?: throw CustomException(ErrorCode.INVALID_ACCESS_TOKEN) // todo: change + ?: throw CustomException(ErrorCode.INVALID_ID_TOKEN) val key = jwks["keys"].find { it["kid"].asText() == kid } - ?: throw CustomException(ErrorCode.INVALID_ACCESS_TOKEN) // todo: change + ?: throw CustomException(ErrorCode.INVALID_ID_TOKEN) val n = BigInteger(1, Base64.getUrlDecoder().decode(key["n"].asText())) val e = BigInteger(1, Base64.getUrlDecoder().decode(key["e"].asText())) diff --git a/src/main/kotlin/org/appjam/smashing/global/exception/ErrorCode.kt b/src/main/kotlin/org/appjam/smashing/global/exception/ErrorCode.kt index 9e5b32e1..83192fbc 100644 --- a/src/main/kotlin/org/appjam/smashing/global/exception/ErrorCode.kt +++ b/src/main/kotlin/org/appjam/smashing/global/exception/ErrorCode.kt @@ -41,10 +41,11 @@ enum class ErrorCode( INVALID_REFRESH_TOKEN_SUBJECT(HttpStatus.UNAUTHORIZED, "AUTH-018", "리프레시 토큰의 유저 정보가 올바르지 않습니다."), INVALID_REFRESH_TOKEN_TYPE(HttpStatus.UNAUTHORIZED, "AUTH-019", "리프레시 토큰의 타입이 올바르지 않습니다."), - // Auth - Kakao Token - INVALID_KAKAO_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH-020", "유효하지 않은 카카오 액세스 토큰입니다."), - DUPLICATE_KAKAO_ID(HttpStatus.CONFLICT, "AUTH-021", "이미 존재하는 유저입니다."), + // Auth - SocialId Token + INVALID_ISS(HttpStatus.UNAUTHORIZED, "AUTH-020", "유효하지 않은 ID 토큰 발급자입니다."), + DUPLICATE_SOCIAL_ID(HttpStatus.CONFLICT, "AUTH-021", "이미 존재하는 유저입니다."), DUPLICATE_NICKNAME(HttpStatus.CONFLICT, "AUTH-022", "이미 사용 중인 닉네임입니다."), + INVALID_ID_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH-023", "유효하지 않은 ID 토큰입니다."), // Domain - User / Profile USER_SPORT_PROFILE_NOT_FOUND(HttpStatus.NOT_FOUND, "USER-001", "유저 스포츠 프로필을 찾을 수 없습니다."), @@ -58,7 +59,7 @@ enum class ErrorCode( INVALID_OPENCHAT_FORMAT(HttpStatus.BAD_REQUEST, "USER-007", "잘못된 오픈채팅 링크 형식입니다."), ALREADY_EXIST_SPORT_PROFILE(HttpStatus.CONFLICT, "USER-008", "이미 존재하는 스포츠 프로필입니다."), INVALID_REGION(HttpStatus.BAD_REQUEST, "USER-009", "잘못된 지역구 형식입니다."), - WITHDRAWN_USER(HttpStatus.FORBIDDEN, "USER-010", "탈퇴한 유저입니다."), + WITHDRAWN_USER(HttpStatus.FORBIDDEN, "UINVALID_ID_TOKENSER-010", "탈퇴한 유저입니다."), // Domain - Matching MATCHING_REQUESTER_NOT_FOUND(HttpStatus.NOT_FOUND, "MATCH-001", "요청자 유저를 찾을 수 없습니다."), From ea996906e915bd4fc0c72467c5d6872154b95207 Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Sat, 11 Apr 2026 13:24:59 +1000 Subject: [PATCH 16/22] feat: make value const --- .../domain/auth/social/OidcTokenValidator.kt | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt index 2b863cc0..8118e225 100644 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt @@ -43,18 +43,26 @@ class OidcTokenValidator { jwksUri: String, ): PublicKey { val header = String(Base64.getUrlDecoder().decode(idToken.split(".")[0])) - val kid = ObjectMapper().readTree(header).get("kid").asText() + val kid = ObjectMapper().readTree(header).get(KID).asText() val jwks = RestTemplate().getForObject(jwksUri, JsonNode::class.java) ?: throw CustomException(ErrorCode.INVALID_ID_TOKEN) - val key = jwks["keys"].find { it["kid"].asText() == kid } + val key = jwks[KEYS].find { it[KID].asText() == kid } ?: throw CustomException(ErrorCode.INVALID_ID_TOKEN) - val n = BigInteger(1, Base64.getUrlDecoder().decode(key["n"].asText())) - val e = BigInteger(1, Base64.getUrlDecoder().decode(key["e"].asText())) + val n = BigInteger(1, Base64.getUrlDecoder().decode(key[N].asText())) + val e = BigInteger(1, Base64.getUrlDecoder().decode(key[E].asText())) - return KeyFactory.getInstance("RSA") + return KeyFactory.getInstance(RSA) .generatePublic(RSAPublicKeySpec(n, e)) } + + companion object { + private const val KID = "kid" + private const val KEYS = "keys" + private const val N = "n" + private const val E = "e" + private const val RSA = "RSA" + } } From c9133a683de955c4bc88149a451ac40b81e6d9ca Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Sat, 11 Apr 2026 13:26:49 +1000 Subject: [PATCH 17/22] feat: fix wrong errorCode --- .../kotlin/org/appjam/smashing/global/exception/ErrorCode.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/org/appjam/smashing/global/exception/ErrorCode.kt b/src/main/kotlin/org/appjam/smashing/global/exception/ErrorCode.kt index 83192fbc..1b7cb3c7 100644 --- a/src/main/kotlin/org/appjam/smashing/global/exception/ErrorCode.kt +++ b/src/main/kotlin/org/appjam/smashing/global/exception/ErrorCode.kt @@ -59,7 +59,7 @@ enum class ErrorCode( INVALID_OPENCHAT_FORMAT(HttpStatus.BAD_REQUEST, "USER-007", "잘못된 오픈채팅 링크 형식입니다."), ALREADY_EXIST_SPORT_PROFILE(HttpStatus.CONFLICT, "USER-008", "이미 존재하는 스포츠 프로필입니다."), INVALID_REGION(HttpStatus.BAD_REQUEST, "USER-009", "잘못된 지역구 형식입니다."), - WITHDRAWN_USER(HttpStatus.FORBIDDEN, "UINVALID_ID_TOKENSER-010", "탈퇴한 유저입니다."), + WITHDRAWN_USER(HttpStatus.FORBIDDEN, "USER-010", "탈퇴한 유저입니다."), // Domain - Matching MATCHING_REQUESTER_NOT_FOUND(HttpStatus.NOT_FOUND, "MATCH-001", "요청자 유저를 찾을 수 없습니다."), From 848ecc926f4bcbf024794a52c08e087cccbfb57d Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Mon, 20 Apr 2026 23:22:49 +1000 Subject: [PATCH 18/22] fix: delete unused code --- .../auth/social/apple/AppleOidcValidator.kt | 21 ------------------- .../auth/social/kakao/KakaoOidcValidator.kt | 21 ------------------- 2 files changed, 42 deletions(-) delete mode 100644 src/main/kotlin/org/appjam/smashing/domain/auth/social/apple/AppleOidcValidator.kt delete mode 100644 src/main/kotlin/org/appjam/smashing/domain/auth/social/kakao/KakaoOidcValidator.kt diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/social/apple/AppleOidcValidator.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/social/apple/AppleOidcValidator.kt deleted file mode 100644 index 4ad5afef..00000000 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/social/apple/AppleOidcValidator.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.appjam.smashing.domain.auth.social.apple - -import org.appjam.smashing.domain.auth.social.OidcTokenValidator -import org.springframework.stereotype.Component - -@Component -class AppleOidcValidator( - private val oidcTokenValidator: OidcTokenValidator, -) { - fun extractAppleId(idToken: String): String = - oidcTokenValidator.extractSocialId( - idToken = idToken, - jwksUri = JWKS_URI, - iss = ISS, - ) - - companion object { - private const val JWKS_URI = "https://appleid.apple.com/auth/keys" - private const val ISS = "https://appleid.apple.com" - } -} diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/social/kakao/KakaoOidcValidator.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/social/kakao/KakaoOidcValidator.kt deleted file mode 100644 index a46ed85e..00000000 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/social/kakao/KakaoOidcValidator.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.appjam.smashing.domain.auth.social.kakao - -import org.appjam.smashing.domain.auth.social.OidcTokenValidator -import org.springframework.stereotype.Component - -@Component -class KakaoOidcValidator( - private val oidcTokenValidator: OidcTokenValidator, -) { - fun extractKakaoId(idToken: String): String = - oidcTokenValidator.extractSocialId( - idToken = idToken, - jwksUri = JWKS_URI, - iss = ISS, - ) - - companion object { - private const val JWKS_URI = "https://kauth.kakao.com/.well-known/jwks.json" - private const val ISS = "https://kauth.kakao.com" - } -} From 588609970c253b6d126bfb128e27ef88addc0fe6 Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Mon, 20 Apr 2026 23:27:33 +1000 Subject: [PATCH 19/22] fix: fix the logic of oidc --- .../domain/auth/social/OidcProperties.kt | 15 ++++++ .../domain/auth/social/OidcTokenValidator.kt | 51 ++++++++++++------- .../auth/social/SocialAuthServiceManager.kt | 27 ++++------ .../smashing/global/exception/ErrorCode.kt | 7 +-- src/main/resources/application.yml | 4 ++ 5 files changed, 66 insertions(+), 38 deletions(-) create mode 100644 src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcProperties.kt diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcProperties.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcProperties.kt new file mode 100644 index 00000000..886fc7d6 --- /dev/null +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcProperties.kt @@ -0,0 +1,15 @@ +package org.appjam.smashing.domain.auth.social + +import org.appjam.smashing.domain.auth.enums.ProviderType +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties(prefix = "oidc") +data class OidcProperties( + val kakaoClientId: String, + val appleClientId: String, +) { + fun getClientId(providerType: ProviderType): String = when (providerType) { + ProviderType.KAKAO -> kakaoClientId + ProviderType.APPLE -> appleClientId + } +} diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt index 8118e225..17bd33b6 100644 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt @@ -3,6 +3,7 @@ package org.appjam.smashing.domain.auth.social import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import io.jsonwebtoken.Jwts +import org.appjam.smashing.domain.auth.enums.ProviderType import org.appjam.smashing.global.exception.CustomException import org.appjam.smashing.global.exception.ErrorCode import org.springframework.stereotype.Component @@ -14,28 +15,40 @@ import java.security.spec.RSAPublicKeySpec import java.util.* @Component -class OidcTokenValidator { +class OidcTokenValidator( + private val oidcProperties: OidcProperties, +) { fun extractSocialId( idToken: String, - jwksUri: String, - iss: String, - ): String = try { - val publicKey = getPublicKey( - idToken = idToken, - jwksUri = jwksUri, - ) + providerType: ProviderType, + ): String { + // 회웑 정보 가져오기 + val (iss, jwksUri) = when (providerType) { + ProviderType.KAKAO -> KAKAO_ISS to KAKAO_JWKS_URI + ProviderType.APPLE -> APPLE_ISS to APPLE_JWKS_URI + } + val clientId = oidcProperties.getClientId(providerType) + + return try { + val publicKey = getPublicKey( + idToken = idToken, + jwksUri = jwksUri, + ) - val claims = Jwts.parserBuilder() - .setSigningKey(publicKey) - .build() - .parseClaimsJws(idToken) - .body + val claims = Jwts.parserBuilder() + .setSigningKey(publicKey) + .build() + .parseClaimsJws(idToken) + .body - if (claims.issuer != iss) throw CustomException(ErrorCode.INVALID_ISS) + // 회원 검증 + if (claims.issuer != iss) throw CustomException(ErrorCode.INVALID_ISS) + if (claims.audience != clientId) throw CustomException(ErrorCode.INVALID_AUD) - claims.subject - } catch (e: Exception) { - throw CustomException(ErrorCode.INVALID_ID_TOKEN) + claims.subject + } catch (e: Exception) { + throw CustomException(ErrorCode.INVALID_ID_TOKEN) + } } private fun getPublicKey( @@ -64,5 +77,9 @@ class OidcTokenValidator { private const val N = "n" private const val E = "e" private const val RSA = "RSA" + private const val KAKAO_JWKS_URI = "https://kauth.kakao.com/.well-known/jwks.json" + private const val KAKAO_ISS = "https://kauth.kakao.com" + private const val APPLE_JWKS_URI = "https://appleid.apple.com/auth/keys" + private const val APPLE_ISS = "https://appleid.apple.com" } } diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/social/SocialAuthServiceManager.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/social/SocialAuthServiceManager.kt index c067a244..35300425 100644 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/social/SocialAuthServiceManager.kt +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/social/SocialAuthServiceManager.kt @@ -2,30 +2,21 @@ package org.appjam.smashing.domain.auth.social import org.appjam.smashing.domain.auth.dto.command.SignInRequestCommand import org.appjam.smashing.domain.auth.dto.response.SocialType -import org.appjam.smashing.domain.auth.enums.ProviderType.APPLE -import org.appjam.smashing.domain.auth.enums.ProviderType.KAKAO -import org.appjam.smashing.domain.auth.social.apple.AppleOidcValidator -import org.appjam.smashing.domain.auth.social.kakao.KakaoOidcValidator import org.springframework.stereotype.Component @Component class SocialAuthServiceManager( - private val kakaoOidcValidator: KakaoOidcValidator, - private val appleOidcValidator: AppleOidcValidator, + private val oidcTokenValidator: OidcTokenValidator, ) { fun getSocialId(command: SignInRequestCommand): SocialType { - val idToken = command.idToken + val socialId = oidcTokenValidator.extractSocialId( + idToken = command.idToken, + providerType = command.provider + ) - return when (command.provider) { - KAKAO -> SocialType( - provider = KAKAO, - socialId = kakaoOidcValidator.extractKakaoId(idToken), - ) - - APPLE -> SocialType( - provider = APPLE, - socialId = appleOidcValidator.extractAppleId(idToken), - ) - } + return SocialType( + provider = command.provider, + socialId = socialId, + ) } } diff --git a/src/main/kotlin/org/appjam/smashing/global/exception/ErrorCode.kt b/src/main/kotlin/org/appjam/smashing/global/exception/ErrorCode.kt index 1b7cb3c7..777029b6 100644 --- a/src/main/kotlin/org/appjam/smashing/global/exception/ErrorCode.kt +++ b/src/main/kotlin/org/appjam/smashing/global/exception/ErrorCode.kt @@ -43,9 +43,10 @@ enum class ErrorCode( // Auth - SocialId Token INVALID_ISS(HttpStatus.UNAUTHORIZED, "AUTH-020", "유효하지 않은 ID 토큰 발급자입니다."), - DUPLICATE_SOCIAL_ID(HttpStatus.CONFLICT, "AUTH-021", "이미 존재하는 유저입니다."), - DUPLICATE_NICKNAME(HttpStatus.CONFLICT, "AUTH-022", "이미 사용 중인 닉네임입니다."), - INVALID_ID_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH-023", "유효하지 않은 ID 토큰입니다."), + INVALID_AUD(HttpStatus.UNAUTHORIZED, "AUTH-021", "유효하지 않은 audience입니다."), + DUPLICATE_SOCIAL_ID(HttpStatus.CONFLICT, "AUTH-022", "이미 존재하는 유저입니다."), + DUPLICATE_NICKNAME(HttpStatus.CONFLICT, "AUTH-023", "이미 사용 중인 닉네임입니다."), + INVALID_ID_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH-024", "유효하지 않은 ID 토큰입니다."), // Domain - User / Profile USER_SPORT_PROFILE_NOT_FOUND(HttpStatus.NOT_FOUND, "USER-001", "유저 스포츠 프로필을 찾을 수 없습니다."), diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 32bb00e1..65028e13 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,3 +4,7 @@ spring: timezone: default-time-zone: Asia/Seoul + +oidc: + kakao-client-id: ${KAKAO_CLIENT_ID} + apple-client-id: ${APPLE_CLIENT_ID:test_apple_id} \ No newline at end of file From f90397275de9d31d9aa5e7e522827be18e124eff Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Tue, 21 Apr 2026 23:54:43 +1000 Subject: [PATCH 20/22] fix: add null case in provider enum --- .../org/appjam/smashing/domain/auth/dto/request/SignInRequest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/dto/request/SignInRequest.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/dto/request/SignInRequest.kt index 9d0b2e84..cc30ac30 100644 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/dto/request/SignInRequest.kt +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/dto/request/SignInRequest.kt @@ -9,6 +9,7 @@ import org.appjam.smashing.global.extensions.ofIgnoreCase data class SignInRequest( @field:NotBlank(message = "idToken을 입력해주세요.") val idToken: String?, + @field:NotBlank(message = "provider를 입력해주세요.") @field:ValidEnum(message = "잘못된 provider 값입니다.", enumClass = ProviderType::class) val provider: String?, ) { From 11dd186e858df9542a55d7dec1ddd73c432516ac Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Wed, 22 Apr 2026 23:48:29 +1000 Subject: [PATCH 21/22] fix: make caffeine cache --- build.gradle.kts | 3 ++ .../domain/auth/social/OidcJwksClient.kt | 28 +++++++++++++++++++ .../domain/auth/social/OidcTokenValidator.kt | 8 ++---- .../global/config/RestTemplateConfig.kt | 18 ++++++++++++ 4 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcJwksClient.kt create mode 100644 src/main/kotlin/org/appjam/smashing/global/config/RestTemplateConfig.kt diff --git a/build.gradle.kts b/build.gradle.kts index 93507174..32fac553 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -74,6 +74,9 @@ dependencies { // Redis implementation("org.springframework.boot:spring-boot-starter-data-redis") + + // Caffeine + implementation("com.github.ben-manes.caffeine:caffeine:3.1.8") } kotlin { diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcJwksClient.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcJwksClient.kt new file mode 100644 index 00000000..7b19f059 --- /dev/null +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcJwksClient.kt @@ -0,0 +1,28 @@ +package org.appjam.smashing.domain.auth.social + +import com.fasterxml.jackson.databind.JsonNode +import com.github.benmanes.caffeine.cache.Cache +import com.github.benmanes.caffeine.cache.Caffeine +import org.appjam.smashing.global.exception.CustomException +import org.appjam.smashing.global.exception.ErrorCode +import org.springframework.stereotype.Component +import org.springframework.web.client.RestTemplate +import java.util.concurrent.TimeUnit + +@Component +class OidcJwksClient( + private val restTemplate: RestTemplate, +) { + private val cache: Cache = Caffeine.newBuilder() + .expireAfterWrite(1, TimeUnit.HOURS) + .maximumSize(10) + .build() + + fun getKeys( + jwksUri: String, + ): JsonNode = cache.get(jwksUri) { + restTemplate.getForObject(jwksUri, JsonNode::class.java) + ?: throw CustomException(ErrorCode.INVALID_ID_TOKEN) + } ?: throw CustomException(ErrorCode.INVALID_ID_TOKEN) + +} diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt index 17bd33b6..30227ed1 100644 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt @@ -1,13 +1,11 @@ package org.appjam.smashing.domain.auth.social -import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import io.jsonwebtoken.Jwts import org.appjam.smashing.domain.auth.enums.ProviderType import org.appjam.smashing.global.exception.CustomException import org.appjam.smashing.global.exception.ErrorCode import org.springframework.stereotype.Component -import org.springframework.web.client.RestTemplate import java.math.BigInteger import java.security.KeyFactory import java.security.PublicKey @@ -17,6 +15,7 @@ import java.util.* @Component class OidcTokenValidator( private val oidcProperties: OidcProperties, + private val jwksClient: OidcJwksClient, ) { fun extractSocialId( idToken: String, @@ -58,10 +57,9 @@ class OidcTokenValidator( val header = String(Base64.getUrlDecoder().decode(idToken.split(".")[0])) val kid = ObjectMapper().readTree(header).get(KID).asText() - val jwks = RestTemplate().getForObject(jwksUri, JsonNode::class.java) - ?: throw CustomException(ErrorCode.INVALID_ID_TOKEN) + val keys = jwksClient.getKeys(jwksUri) - val key = jwks[KEYS].find { it[KID].asText() == kid } + val key = keys[KEYS].find { it[KID].asText() == kid } ?: throw CustomException(ErrorCode.INVALID_ID_TOKEN) val n = BigInteger(1, Base64.getUrlDecoder().decode(key[N].asText())) diff --git a/src/main/kotlin/org/appjam/smashing/global/config/RestTemplateConfig.kt b/src/main/kotlin/org/appjam/smashing/global/config/RestTemplateConfig.kt new file mode 100644 index 00000000..e6626c1e --- /dev/null +++ b/src/main/kotlin/org/appjam/smashing/global/config/RestTemplateConfig.kt @@ -0,0 +1,18 @@ +package org.appjam.smashing.global.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.client.SimpleClientHttpRequestFactory +import org.springframework.web.client.RestTemplate + +@Configuration +class RestTemplateConfig { + @Bean + fun restTemplate(): RestTemplate { + val factory = SimpleClientHttpRequestFactory().apply { + setConnectTimeout(3_000) + setReadTimeout(3_000) + } + return RestTemplate(factory) + } +} From fa3621669525e636905acbd111c50016d0415c5a Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Wed, 22 Apr 2026 23:53:03 +1000 Subject: [PATCH 22/22] fix: delete try~catch code --- .../domain/auth/social/OidcTokenValidator.kt | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt b/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt index 30227ed1..8dd46855 100644 --- a/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt +++ b/src/main/kotlin/org/appjam/smashing/domain/auth/social/OidcTokenValidator.kt @@ -28,26 +28,22 @@ class OidcTokenValidator( } val clientId = oidcProperties.getClientId(providerType) - return try { - val publicKey = getPublicKey( - idToken = idToken, - jwksUri = jwksUri, - ) + val publicKey = getPublicKey( + idToken = idToken, + jwksUri = jwksUri, + ) - val claims = Jwts.parserBuilder() - .setSigningKey(publicKey) - .build() - .parseClaimsJws(idToken) - .body + val claims = Jwts.parserBuilder() + .setSigningKey(publicKey) + .build() + .parseClaimsJws(idToken) + .body - // 회원 검증 - if (claims.issuer != iss) throw CustomException(ErrorCode.INVALID_ISS) - if (claims.audience != clientId) throw CustomException(ErrorCode.INVALID_AUD) + // 회원 검증 + if (claims.issuer != iss) throw CustomException(ErrorCode.INVALID_ISS) + if (claims.audience != clientId) throw CustomException(ErrorCode.INVALID_AUD) - claims.subject - } catch (e: Exception) { - throw CustomException(ErrorCode.INVALID_ID_TOKEN) - } + return claims.subject } private fun getPublicKey(