From 17f4babd55ce1c46d09a65f71162cb4266370afd Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Fri, 28 Nov 2025 23:15:17 +0900 Subject: [PATCH 01/35] =?UTF-8?q?#277=20[chore]=20openfeign=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- photi-apis/enduser/build.gradle.kts | 7 +++++++ photi-core/infra/build.gradle.kts | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/photi-apis/enduser/build.gradle.kts b/photi-apis/enduser/build.gradle.kts index 55833bb2..5de0fb30 100644 --- a/photi-apis/enduser/build.gradle.kts +++ b/photi-apis/enduser/build.gradle.kts @@ -1,3 +1,9 @@ +dependencyManagement { + imports { + mavenBom("org.springframework.cloud:spring-cloud-dependencies:2024.0.0") + } +} + dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-validation") @@ -5,6 +11,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-mail") implementation("org.springframework.boot:spring-boot-starter-thymeleaf") implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("org.springframework.cloud:spring-cloud-starter-openfeign") implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13") implementation("io.jsonwebtoken:jjwt-api:0.13.0") implementation("io.micrometer:micrometer-registry-prometheus") diff --git a/photi-core/infra/build.gradle.kts b/photi-core/infra/build.gradle.kts index 0f180006..c133b26b 100644 --- a/photi-core/infra/build.gradle.kts +++ b/photi-core/infra/build.gradle.kts @@ -6,9 +6,16 @@ val bootJar: BootJar by tasks bootJar.enabled = false jar.enabled = true +dependencyManagement { + imports { + mavenBom("org.springframework.cloud:spring-cloud-dependencies:2024.0.0") + } +} + dependencies { implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-data-redis") + implementation("org.springframework.cloud:spring-cloud-starter-openfeign") implementation("io.awspring.cloud:spring-cloud-starter-aws:2.4.4") implementation("com.querydsl:querydsl-jpa:5.1.0:jakarta") implementation(project(":photi-core:domain")) From 1636d56335893d26d3b4ba49b7fd4bfadb86f22d Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Fri, 28 Nov 2025 23:23:43 +0900 Subject: [PATCH 02/35] =?UTF-8?q?#277=20[chore]=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20configuration=20properties=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/common/properties/KakaoOAuthProperties.kt | 9 +++++++++ .../infra/properties/ConfigurationPropertiesConfig.kt | 10 ++++++++++ .../infra/src/main/resources/application-infra.yml | 10 ++++++++++ 3 files changed, 29 insertions(+) create mode 100644 photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/KakaoOAuthProperties.kt create mode 100644 photi-core/infra/src/main/kotlin/com/photi/core/infra/properties/ConfigurationPropertiesConfig.kt diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/KakaoOAuthProperties.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/KakaoOAuthProperties.kt new file mode 100644 index 00000000..960b3f70 --- /dev/null +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/KakaoOAuthProperties.kt @@ -0,0 +1,9 @@ +package com.photi.core.domain.common.properties + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("oauth.kakao") +data class KakaoOAuthProperties( + val baseUrl: String, + val restApiKey: String, +) diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/properties/ConfigurationPropertiesConfig.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/properties/ConfigurationPropertiesConfig.kt new file mode 100644 index 00000000..ad5e348c --- /dev/null +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/properties/ConfigurationPropertiesConfig.kt @@ -0,0 +1,10 @@ +package com.photi.core.infra.properties + +import com.photi.core.domain.common.properties.KakaoOAuthProperties +import com.photi.core.infra.PhotiConfig +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.annotation.Configuration + +@Configuration +@EnableConfigurationProperties(value = [KakaoOAuthProperties::class]) +class ConfigurationPropertiesConfig : PhotiConfig diff --git a/photi-core/infra/src/main/resources/application-infra.yml b/photi-core/infra/src/main/resources/application-infra.yml index 785d8ced..b0956983 100644 --- a/photi-core/infra/src/main/resources/application-infra.yml +++ b/photi-core/infra/src/main/resources/application-infra.yml @@ -17,6 +17,11 @@ cloud: bucket: ${AWS_BUCKET} stack: auto: false + +oauth: + kakao: + base-url: ${KAKAO_BASE_URL} + rest-api-key: ${KAKAO_REST_API_KEY} --- spring: config: @@ -37,6 +42,11 @@ cloud: bucket: ${AWS_BUCKET} stack: auto: false + +oauth: + kakao: + base-url: ${KAKAO_BASE_URL} + rest-api-key: ${KAKAO_REST_API_KEY} --- spring: config: From 23c4b72cecfe5816351ae8305050203c43f625e4 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Fri, 28 Nov 2025 23:26:17 +0900 Subject: [PATCH 03/35] =?UTF-8?q?#277=20[feat]=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20=EA=B3=B5=EA=B0=9C=ED=82=A4=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20feign=20client=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infra/oauth/client/KakaoOAuthClient.kt | 14 +++++++ .../infra/oauth/dto/OidcPublicKeysResponse.kt | 14 +++++++ .../core/infra/redis/RedisCacheConfig.kt | 38 +++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/KakaoOAuthClient.kt create mode 100644 photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/dto/OidcPublicKeysResponse.kt create mode 100644 photi-core/infra/src/main/kotlin/com/photi/core/infra/redis/RedisCacheConfig.kt diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/KakaoOAuthClient.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/KakaoOAuthClient.kt new file mode 100644 index 00000000..b8979109 --- /dev/null +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/KakaoOAuthClient.kt @@ -0,0 +1,14 @@ +package com.photi.core.infra.oauth.client + +import com.photi.core.infra.oauth.dto.OidcPublicKeysResponse +import org.springframework.cache.annotation.Cacheable +import org.springframework.cloud.openfeign.FeignClient +import org.springframework.web.bind.annotation.GetMapping + +@FeignClient(name = "KakaoOAuthClient", url = "https://kauth.kakao.com") +interface KakaoOAuthClient { + + @Cacheable(cacheNames = ["KakaoOidc"], cacheManager = "oidcCacheManager") + @GetMapping("/.well-known/jwks.json") + fun getOidcPublicKeys(): OidcPublicKeysResponse +} diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/dto/OidcPublicKeysResponse.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/dto/OidcPublicKeysResponse.kt new file mode 100644 index 00000000..6c0dd29a --- /dev/null +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/dto/OidcPublicKeysResponse.kt @@ -0,0 +1,14 @@ +package com.photi.core.infra.oauth.dto + +data class OidcPublicKeysResponse( + val keys: List, +) + +data class Jwk( + val kid: String, + val kty: String, + val alg: String, + val use: String, + val n: String, + val e: String, +) diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/redis/RedisCacheConfig.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/redis/RedisCacheConfig.kt new file mode 100644 index 00000000..4e24c1c4 --- /dev/null +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/redis/RedisCacheConfig.kt @@ -0,0 +1,38 @@ +package com.photi.core.infra.redis + +import com.photi.core.infra.PhotiConfig +import org.springframework.cache.CacheManager +import org.springframework.cache.annotation.EnableCaching +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.data.redis.cache.RedisCacheConfiguration +import org.springframework.data.redis.cache.RedisCacheManager +import org.springframework.data.redis.connection.RedisConnectionFactory +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer +import org.springframework.data.redis.serializer.RedisSerializationContext +import org.springframework.data.redis.serializer.StringRedisSerializer +import java.time.Duration + +@Configuration +@EnableCaching +class RedisCacheConfig : PhotiConfig { + + @Bean + fun oidcCacheManager(connectionFactory: RedisConnectionFactory): CacheManager { + val cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer( + StringRedisSerializer() + ) + ) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer( + GenericJackson2JsonRedisSerializer() + ) + ) + .entryTtl(Duration.ofDays(7L)) + return RedisCacheManager.builder(connectionFactory) + .cacheDefaults(cacheConfiguration) + .build() + } +} From 8e008782c94674b78b5910dc9bd450145de6390a Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Fri, 28 Nov 2025 23:27:19 +0900 Subject: [PATCH 04/35] =?UTF-8?q?#277=20[feat]=20feign=20config=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/photi/core/infra/feign/FeignConfig.kt | 16 ++++++++++++++++ .../oauth/client/BaseFeignClientsPackage.kt | 3 +++ 2 files changed, 19 insertions(+) create mode 100644 photi-core/infra/src/main/kotlin/com/photi/core/infra/feign/FeignConfig.kt create mode 100644 photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/BaseFeignClientsPackage.kt diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/feign/FeignConfig.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/feign/FeignConfig.kt new file mode 100644 index 00000000..d2f508aa --- /dev/null +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/feign/FeignConfig.kt @@ -0,0 +1,16 @@ +package com.photi.core.infra.feign + +import com.photi.core.infra.PhotiConfig +import com.photi.core.infra.oauth.client.BaseFeignClientsPackage +import feign.Logger +import org.springframework.cloud.openfeign.EnableFeignClients +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +@EnableFeignClients(basePackageClasses = [BaseFeignClientsPackage::class]) +class FeignConfig : PhotiConfig { + + @Bean + fun feignLoggerLevel() = Logger.Level.FULL +} diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/BaseFeignClientsPackage.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/BaseFeignClientsPackage.kt new file mode 100644 index 00000000..b04df191 --- /dev/null +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/BaseFeignClientsPackage.kt @@ -0,0 +1,3 @@ +package com.photi.core.infra.oauth.client + +interface BaseFeignClientsPackage From 4e642d276b8c1131d9650a5b307f7647a7d805a3 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Fri, 28 Nov 2025 23:29:37 +0900 Subject: [PATCH 05/35] =?UTF-8?q?#277=20[feat]=20oidc=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EB=A1=9C=EB=93=9C,=20=EC=84=9C=EB=AA=85=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../enduser/config/oidc/JwtOidcProvider.kt | 87 +++++++++++++++++++ .../core/infra/oauth/port/JwtOidcPort.kt | 15 ++++ 2 files changed, 102 insertions(+) create mode 100644 photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/oidc/JwtOidcProvider.kt create mode 100644 photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/port/JwtOidcPort.kt diff --git a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/oidc/JwtOidcProvider.kt b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/oidc/JwtOidcProvider.kt new file mode 100644 index 00000000..16f59715 --- /dev/null +++ b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/oidc/JwtOidcProvider.kt @@ -0,0 +1,87 @@ +package com.photi.apis.enduser.config.oidc + +import com.photi.core.domain.common.exception.GlobalException +import com.photi.core.domain.user.dto.OidcPayload +import com.photi.core.infra.oauth.port.JwtOidcPort +import io.jsonwebtoken.* +import org.springframework.stereotype.Component +import java.math.BigInteger +import java.security.KeyFactory +import java.security.PublicKey +import java.security.spec.RSAPublicKeySpec +import java.util.* + +@Component +class JwtOidcProvider : JwtOidcPort { + + override fun getKidFromUnsignedIdToken( + idToken: String, + iss: String, + aud: String, + nonce: String, + ) = getUnsignedIdTokenClaims(idToken, iss, aud, nonce) + .header["kid"] + .toString() + + override fun getIdTokenPayload(idToken: String, modulus: String, exponent: String): OidcPayload { + val claims = getIdTokenClaims(idToken, modulus, exponent) + return OidcPayload( + claims.issuer, + claims.audience.first(), + claims.subject, + claims["email"].toString(), + ) + } + + private fun getUnsignedIdTokenClaims( + idToken: String, + iss: String, + aud: String, + nonce: String, + ): Jwt { + return try { + Jwts.parser() + .requireIssuer(iss) + .requireAudience(aud) + .require("nonce", nonce) + .build() + .parseUnsecuredClaims(getUnsignedIdToken(idToken)) + } catch (e: ExpiredJwtException) { + throw GlobalException.ExpiredTokenException() + } catch (e: Exception) { + throw GlobalException.InvalidTokenException() + } + } + + private fun getUnsignedIdToken(idToken: String): String { + val splitToken = idToken.split("\\.") + if (splitToken.size != 3) { + throw GlobalException.InvalidTokenException() + } + return "${splitToken[0]}.${splitToken[1]}." + } + + private fun getIdTokenClaims(idToken: String, modulus: String, exponent: String): Claims { + return try { + Jwts.parser() + .verifyWith(getRsaPublicKey(modulus, exponent)) + .build() + .parseSignedClaims(idToken) + .payload + } catch (e: ExpiredJwtException) { + throw GlobalException.ExpiredTokenException() + } catch (e: Exception) { + throw GlobalException.InvalidTokenException() + } + } + + private fun getRsaPublicKey(modulus: String, exponent: String): PublicKey { + val keyFactory = KeyFactory.getInstance("RSA") + val decodeN = Base64.getUrlDecoder().decode(modulus) + val decodeE = Base64.getUrlDecoder().decode(exponent) + val n = BigInteger(1, decodeN) + val e = BigInteger(1, decodeE) + val keySpec = RSAPublicKeySpec(n, e) + return keyFactory.generatePublic(keySpec) + } +} diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/port/JwtOidcPort.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/port/JwtOidcPort.kt new file mode 100644 index 00000000..7c2fea6a --- /dev/null +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/port/JwtOidcPort.kt @@ -0,0 +1,15 @@ +package com.photi.core.infra.oauth.port + +import com.photi.core.domain.user.dto.OidcPayload + +interface JwtOidcPort { + + fun getKidFromUnsignedIdToken( + idToken: String, + iss: String, + aud: String, + nonce: String, + ): String + + fun getIdTokenPayload(idToken: String, modulus: String, exponent: String): OidcPayload +} From e95cf876762e8a1b7e0283a64948281be0b20481 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Fri, 28 Nov 2025 23:30:27 +0900 Subject: [PATCH 06/35] =?UTF-8?q?#277=20[feat]=20config=20group=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/photi/apis/enduser/config/InfraConfig.kt | 3 +++ .../main/kotlin/com/photi/core/infra/PhotiConfigGroup.kt | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/InfraConfig.kt b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/InfraConfig.kt index 796d9a00..89144275 100644 --- a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/InfraConfig.kt +++ b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/InfraConfig.kt @@ -12,6 +12,9 @@ import org.springframework.context.annotation.Configuration PhotiConfigGroup.REDIS, PhotiConfigGroup.S3, PhotiConfigGroup.ASYNC, + PhotiConfigGroup.REDIS_CACHE, + PhotiConfigGroup.FEIGN, + PhotiConfigGroup.CONFIGURATION_PROPERTIES, ] ) class InfraConfig diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/PhotiConfigGroup.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/PhotiConfigGroup.kt index 7447c871..f82f2190 100644 --- a/photi-core/infra/src/main/kotlin/com/photi/core/infra/PhotiConfigGroup.kt +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/PhotiConfigGroup.kt @@ -1,8 +1,11 @@ package com.photi.core.infra import com.photi.core.infra.async.AsyncConfig +import com.photi.core.infra.feign.FeignConfig import com.photi.core.infra.jpa.JpaConfig +import com.photi.core.infra.properties.ConfigurationPropertiesConfig import com.photi.core.infra.querydsl.QuerydslConfig +import com.photi.core.infra.redis.RedisCacheConfig import com.photi.core.infra.redis.RedisConfig import com.photi.core.infra.s3.S3Config @@ -14,4 +17,7 @@ enum class PhotiConfigGroup( REDIS(RedisConfig::class.java), S3(S3Config::class.java), ASYNC(AsyncConfig::class.java), + REDIS_CACHE(RedisCacheConfig::class.java), + FEIGN(FeignConfig::class.java), + CONFIGURATION_PROPERTIES(ConfigurationPropertiesConfig::class.java), } From 406221b4e902b769952c71813608b1b4ecd48d15 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Fri, 28 Nov 2025 23:33:33 +0900 Subject: [PATCH 07/35] =?UTF-8?q?#277=20[feat]=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20oidc=20=ED=8E=98=EC=9D=B4=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../photi/core/domain/user/dto/OidcPayload.kt | 8 ++++++ .../photi/core/domain/user/port/OAuthPort.kt | 8 ++++++ .../infra/oauth/adapter/KakaoOAuthAdapter.kt | 26 +++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OidcPayload.kt create mode 100644 photi-core/domain/src/main/kotlin/com/photi/core/domain/user/port/OAuthPort.kt create mode 100644 photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/KakaoOAuthAdapter.kt diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OidcPayload.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OidcPayload.kt new file mode 100644 index 00000000..1e61b78b --- /dev/null +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OidcPayload.kt @@ -0,0 +1,8 @@ +package com.photi.core.domain.user.dto + +data class OidcPayload( + val iss: String, + val aud: String, + val sub: String, + val email: String, +) diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/port/OAuthPort.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/port/OAuthPort.kt new file mode 100644 index 00000000..83359855 --- /dev/null +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/port/OAuthPort.kt @@ -0,0 +1,8 @@ +package com.photi.core.domain.user.port + +import com.photi.core.domain.user.dto.OidcPayload + +interface OAuthPort { + + fun getIdTokenPayload(idToken: String, iss: String, aud: String, nonce: String): OidcPayload +} diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/KakaoOAuthAdapter.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/KakaoOAuthAdapter.kt new file mode 100644 index 00000000..913118af --- /dev/null +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/KakaoOAuthAdapter.kt @@ -0,0 +1,26 @@ +package com.photi.core.infra.oauth.adapter + +import com.photi.core.domain.user.dto.OidcPayload +import com.photi.core.domain.user.port.OAuthPort +import com.photi.core.infra.oauth.client.KakaoOAuthClient +import com.photi.core.infra.oauth.port.JwtOidcPort +import org.springframework.stereotype.Component + +@Component +class KakaoOAuthAdapter( + private val kakaoOAuthClient: KakaoOAuthClient, + private val jwtOidcPort: JwtOidcPort, +) : OAuthPort { + + override fun getIdTokenPayload( + idToken: String, + iss: String, + aud: String, + nonce: String, + ): OidcPayload { + val publicKeys = kakaoOAuthClient.getOidcPublicKeys() + val kid = jwtOidcPort.getKidFromUnsignedIdToken(idToken, iss, aud, nonce) + val jwk = publicKeys.keys.first { it.kid == kid } + return jwtOidcPort.getIdTokenPayload(idToken, jwk.n, jwk.e) + } +} From 74c2edab74803ce5d91003d1e777054f4df2ab4d Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Fri, 28 Nov 2025 23:34:45 +0900 Subject: [PATCH 08/35] =?UTF-8?q?#277=20[feat]=20user=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20oauthinfo=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../photi/core/domain/user/model/OAuthInfo.kt | 23 ++++++++++++++ .../domain/user/model/OAuthProviderType.kt | 7 +++++ .../com/photi/core/domain/user/model/User.kt | 30 ++++++++++++++----- 3 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/OAuthInfo.kt create mode 100644 photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/OAuthProviderType.kt diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/OAuthInfo.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/OAuthInfo.kt new file mode 100644 index 00000000..eddd5275 --- /dev/null +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/OAuthInfo.kt @@ -0,0 +1,23 @@ +package com.photi.core.domain.user.model + +import jakarta.persistence.Column +import jakarta.persistence.Embeddable +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated + +@Embeddable +class OAuthInfo( + + @Enumerated(value = EnumType.STRING) + @Column(nullable = true, length = 15) + val provider: OAuthProviderType?, + + @Column(nullable = true, length = 255) + val sub: String?, +) { + + companion object { + + fun of(provider: OAuthProviderType, sub: String) = OAuthInfo(provider, sub) + } +} diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/OAuthProviderType.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/OAuthProviderType.kt new file mode 100644 index 00000000..c619ef4f --- /dev/null +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/OAuthProviderType.kt @@ -0,0 +1,7 @@ +package com.photi.core.domain.user.model + +enum class OAuthProviderType(private val value: String) { + KAKAO("카카오"), + GOOGLE("구글"), + APPLE("애플"), +} diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/User.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/User.kt index db383a2c..5e80b5a7 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/User.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/User.kt @@ -10,11 +10,21 @@ import com.photi.core.domain.user.validator.UserValidator import jakarta.persistence.* import java.time.LocalDateTime -@Table(name = "users") +@Table( + name = "users", + uniqueConstraints = [UniqueConstraint( + name = "uq_user_provider_sub", + columnNames = ["provider", "sub"] + )] +) @Entity class User( email: String, - authenticationCode: String, + authenticationCode: String? = null, + isAuthenticated: Boolean = false, + username: String? = null, + oAuthInfo: OAuthInfo? = null, + role: RoleType = RoleType.UNAUTHENTICATED_USER, ) : BaseTimeEntity() { @Id @@ -23,20 +33,24 @@ class User( var id: Long? = null protected set - @Column(nullable = false, unique = true, length = 100) + @Embedded + var oAuthInfo: OAuthInfo? = oAuthInfo + protected set + + @Column(nullable = false, length = 100) var email: String = email protected set - @Column(nullable = false, length = 6) - var authenticationCode: String = authenticationCode + @Column(nullable = true, length = 6) + var authenticationCode: String? = authenticationCode protected set @Column(nullable = false) - var isAuthenticated: Boolean = false + var isAuthenticated: Boolean = isAuthenticated protected set @Column(nullable = true, unique = true, length = 20) - var username: String? = null + var username: String? = username protected set @Column(nullable = true, length = 255) @@ -49,7 +63,7 @@ class User( @Enumerated(value = EnumType.STRING) @Column(nullable = false, length = 25) - var role: RoleType = RoleType.UNAUTHENTICATED_USER + var role: RoleType = role protected set @Column(nullable = true) From 8d839745c4443e4e8491862179788d89c8ad2be1 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Fri, 28 Nov 2025 23:37:21 +0900 Subject: [PATCH 09/35] =?UTF-8?q?#277=20[feat]=20user=20query,=20command?= =?UTF-8?q?=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/domain/user/dto/OAuthSignUpDto.kt | 19 +++++++++++++++++++ .../user/model/repository/UserRepository.kt | 3 +++ .../service/command/UserCommandService.kt | 5 +++++ .../user/service/query/UserQueryService.kt | 3 +++ 4 files changed, 30 insertions(+) create mode 100644 photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OAuthSignUpDto.kt diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OAuthSignUpDto.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OAuthSignUpDto.kt new file mode 100644 index 00000000..8659ae25 --- /dev/null +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OAuthSignUpDto.kt @@ -0,0 +1,19 @@ +package com.photi.core.domain.user.dto + +import com.photi.core.domain.user.model.OAuthInfo +import com.photi.core.domain.user.model.RoleType +import com.photi.core.domain.user.model.User + +data class OAuthSignUpDto( + val username: String, +) { + + fun toEntity(oAuthInfo: OAuthInfo, email: String) = + User( + email = email, + oAuthInfo = oAuthInfo, + username = username, + role = RoleType.USER, + isAuthenticated = true, + ) +} diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/repository/UserRepository.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/repository/UserRepository.kt index 9821dee3..25a13c5c 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/repository/UserRepository.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/repository/UserRepository.kt @@ -1,6 +1,7 @@ package com.photi.core.domain.user.model.repository import com.photi.core.domain.user.dto.FindInfoDto +import com.photi.core.domain.user.model.OAuthInfo import com.photi.core.domain.user.model.RoleType import com.photi.core.domain.user.model.User import org.springframework.data.jpa.repository.JpaRepository @@ -12,6 +13,8 @@ interface UserRepository : JpaRepository, UserCustomRepository { fun existsByUsername(username: String): Boolean + fun existsByOAuthInfo(oAuthInfo: OAuthInfo): Boolean + fun findByEmailAndRole(email: String, role: RoleType): User? fun findByUsername(username: String): User? diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/command/UserCommandService.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/command/UserCommandService.kt index 4cd25670..308cbba8 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/command/UserCommandService.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/command/UserCommandService.kt @@ -1,6 +1,8 @@ package com.photi.core.domain.user.service.command +import com.photi.core.domain.user.dto.OAuthSignUpDto import com.photi.core.domain.user.dto.SendEmailAuthenticationCodeDto +import com.photi.core.domain.user.model.OAuthInfo import com.photi.core.domain.user.model.repository.UserRepository import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -14,4 +16,7 @@ class UserCommandService( fun createUser(dto: SendEmailAuthenticationCodeDto, authenticationCode: String) { userRepository.save(dto.toEntity(authenticationCode)) } + + fun createUser(dto: OAuthSignUpDto, oAuthInfo: OAuthInfo, email: String) = + userRepository.save(dto.toEntity(oAuthInfo, email)) } diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/query/UserQueryService.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/query/UserQueryService.kt index 3dc727d9..c66a155b 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/query/UserQueryService.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/query/UserQueryService.kt @@ -1,5 +1,6 @@ package com.photi.core.domain.user.service.query +import com.photi.core.domain.user.model.OAuthInfo import com.photi.core.domain.user.model.RoleType import com.photi.core.domain.user.model.repository.UserRepository import org.springframework.data.domain.PageRequest @@ -53,4 +54,6 @@ class UserQueryService( fun existsEmail(email: String) = userRepository.existsByEmailAndRole(email, RoleType.USER) fun existsUsername(username: String) = userRepository.existsByUsername(username) + + fun existsBy(oAuthInfo: OAuthInfo) = userRepository.existsByOAuthInfo(oAuthInfo) } From b9fce571b605060ac0de487af37015726d7a8fff Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Fri, 28 Nov 2025 23:37:48 +0900 Subject: [PATCH 10/35] =?UTF-8?q?#277=20[feat]=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/domain/user/service/OAuthService.kt | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt new file mode 100644 index 00000000..81415c72 --- /dev/null +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt @@ -0,0 +1,37 @@ +package com.photi.core.domain.user.service + +import com.photi.core.domain.common.properties.KakaoOAuthProperties +import com.photi.core.domain.user.dto.OAuthSignUpDto +import com.photi.core.domain.user.dto.SignUpDto +import com.photi.core.domain.user.exception.UserException +import com.photi.core.domain.user.model.OAuthInfo +import com.photi.core.domain.user.model.OAuthProviderType +import com.photi.core.domain.user.port.OAuthPort +import com.photi.core.domain.user.service.command.UserCommandService +import com.photi.core.domain.user.service.query.UserQueryService +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class OAuthService( + private val oAuthPort: OAuthPort, + private val kakaoOAuthProperties: KakaoOAuthProperties, + private val userCommandService: UserCommandService, + private val userQueryService: UserQueryService, +) { + + @Transactional + fun kakaoSignUp(idToken: String, dto: OAuthSignUpDto): SignUpDto { + val idTokenPayload = oAuthPort.getIdTokenPayload( + idToken, + kakaoOAuthProperties.baseUrl, + kakaoOAuthProperties.restApiKey, + "nonce", // todo '인가 코드 요청 api' 요청 시 전달한 nonce 값과 동일한 값 + ) + val oAuthInfo = OAuthInfo.of(OAuthProviderType.KAKAO, idTokenPayload.sub) + if (userQueryService.existsBy(oAuthInfo)) throw UserException.ExistsUserException() + val user = userCommandService.createUser(dto, oAuthInfo, idTokenPayload.email) + return SignUpDto.of(user) + } +} From c6b7cc6f764fba930fe763f0093acf110cbf5b21 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Fri, 28 Nov 2025 23:38:09 +0900 Subject: [PATCH 11/35] =?UTF-8?q?#277=20[feat]=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/oauth/OAuthController.kt | 42 +++++++++++++++++++ .../oauth/request/OAuthSignUpRequest.kt | 24 +++++++++++ 2 files changed, 66 insertions(+) create mode 100644 photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthController.kt create mode 100644 photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/request/OAuthSignUpRequest.kt diff --git a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthController.kt b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthController.kt new file mode 100644 index 00000000..49979cba --- /dev/null +++ b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthController.kt @@ -0,0 +1,42 @@ +package com.photi.apis.enduser.controller.oauth + +import com.photi.apis.enduser.common.exception.UserApiErrorResponses +import com.photi.apis.enduser.config.security.JwtTokenProvider +import com.photi.apis.enduser.controller.auth.dto.response.SignUpResponse +import com.photi.apis.enduser.controller.oauth.request.OAuthSignUpRequest +import com.photi.core.domain.user.exception.UserErrorCode.* +import com.photi.core.domain.user.model.RoleType +import com.photi.core.domain.user.service.OAuthService +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import org.springframework.http.HttpStatus.CREATED +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.* + +@Validated +@RestController +@RequestMapping("/api/v2/oauth") +@Tag(name = "OAuth", description = "OAuth API") +class OAuthController( + private val oAuthService: OAuthService, + private val jwtTokenProvider: JwtTokenProvider, +) { + + @PostMapping("/kakao/signup") + @Operation(summary = "카카오 회원가입") + @ApiResponse(responseCode = "201") + @UserApiErrorResponses([EXISTING_USER]) + fun kakaoSignUp( + @RequestParam("id_token") @Parameter(description = "ID 토큰") idToken: String, + @RequestBody @Valid request: OAuthSignUpRequest, + ): ResponseEntity { + val signUpUser = oAuthService.kakaoSignUp(idToken, request.toServiceDto()) + val response = SignUpResponse.of(signUpUser) + val headers = jwtTokenProvider.createToken(response.userId, RoleType.USER) + return ResponseEntity.status(CREATED).headers(headers).body(response) + } +} diff --git a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/request/OAuthSignUpRequest.kt b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/request/OAuthSignUpRequest.kt new file mode 100644 index 00000000..bb4dce3e --- /dev/null +++ b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/request/OAuthSignUpRequest.kt @@ -0,0 +1,24 @@ +package com.photi.apis.enduser.controller.oauth.request + +import com.photi.core.domain.common.consts.RegexPattern.LOWERCASE_NUMBER_UNDERSCORE +import com.photi.core.domain.user.dto.OAuthSignUpDto +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size + +@Schema(description = "OAuth 회원가입 요청 객체") +data class OAuthSignUpRequest( + + @Schema(description = "아이디", example = "photi") + @field:NotBlank(message = "아이디는 필수 입력입니다.") + @field:Size(min = 5, max = 20, message = "아이디는 5~20자만 가능합니다.") + @field:Pattern( + regexp = LOWERCASE_NUMBER_UNDERSCORE, + message = "아이디는 소문자 영어, 숫자, 특수문자(_)의 조합으로 입력해 주세요." + ) + val username: String, +) { + + fun toServiceDto() = OAuthSignUpDto(username) +} From 80c23c75c08500acd98997d64ff7a8670238ee22 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Fri, 28 Nov 2025 23:39:40 +0900 Subject: [PATCH 12/35] =?UTF-8?q?#277=20[chore]=20=EB=A1=9C=EC=BB=AC=20db?= =?UTF-8?q?=20=EC=98=88=EC=8B=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/init.sql | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docker/init.sql b/docker/init.sql index 3d67db07..3a7ae605 100644 --- a/docker/init.sql +++ b/docker/init.sql @@ -20,7 +20,9 @@ CREATE TABLE users ( user_id BIGSERIAL PRIMARY KEY, email VARCHAR(100) NOT NULL, - authentication_code VARCHAR(6) NOT NULL, + provider VARCHAR(15) NULL, + sub VARCHAR(255) NULL, + authentication_code VARCHAR(6) NULL, is_authenticated BOOLEAN NOT NULL, username VARCHAR(20) NULL, password VARCHAR(255) NULL, @@ -29,7 +31,7 @@ CREATE TABLE users deleted_date TIMESTAMP(6) NULL, created_date_time TIMESTAMP(6) NULL, last_modified_date_time TIMESTAMP(6) NULL, - CONSTRAINT uq_email UNIQUE (email), + CONSTRAINT uq_user_provider_sub UNIQUE (provider, sub), CONSTRAINT uq_username UNIQUE (username) ); From 6254961e9adfe7e53973a243e24cbd45e01209ec Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Sat, 29 Nov 2025 00:02:54 +0900 Subject: [PATCH 13/35] =?UTF-8?q?#277=20[feat]=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20query=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../photi/core/domain/user/model/repository/UserRepository.kt | 2 ++ .../photi/core/domain/user/service/query/UserQueryService.kt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/repository/UserRepository.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/repository/UserRepository.kt index 25a13c5c..b463052b 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/repository/UserRepository.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/repository/UserRepository.kt @@ -25,6 +25,8 @@ interface UserRepository : JpaRepository, UserCustomRepository { fun findByEmailAndUsernameAndIsAuthenticatedTrue(email: String, username: String): User? + fun findByOAuthInfo(oAuthInfo: OAuthInfo): User? + @Query("select new com.photi.core.domain.user.dto.FindInfoDto(u.imageUrl, u.username, u.email) from User u where u.id = :id") fun findInfoById(id: Long): FindInfoDto? } diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/query/UserQueryService.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/query/UserQueryService.kt index c66a155b..bfd23a4a 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/query/UserQueryService.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/query/UserQueryService.kt @@ -23,6 +23,8 @@ class UserQueryService( fun getLoginUserBy(username: String) = userRepository.findByUsername(username) + fun getLoginUserBy(oAuthInfo: OAuthInfo) = userRepository.findByOAuthInfo(oAuthInfo) + fun getAuthenticatedUserBy(email: String) = userRepository.findByEmailAndIsAuthenticatedTrue(email) From b9b4f3bd6f02ab971d88c03b5af0d2f0703975a1 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Sat, 29 Nov 2025 00:04:07 +0900 Subject: [PATCH 14/35] =?UTF-8?q?#277=20[feat]=20user=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/kotlin/com/photi/core/domain/user/model/User.kt | 4 ++++ .../com/photi/core/domain/user/validator/UserValidator.kt | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/User.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/User.kt index 5e80b5a7..b9a0d8ca 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/User.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/User.kt @@ -128,5 +128,9 @@ class User( deletedDate = null } + fun login(userValidator: UserValidator) { + userValidator.validateDeletedUser(this) + } + private fun isImageNullOrEmpty() = this.imageUrl.isNullOrEmpty() } diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/validator/UserValidator.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/validator/UserValidator.kt index d4c5eefc..cb4ab718 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/validator/UserValidator.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/validator/UserValidator.kt @@ -48,7 +48,7 @@ class UserValidator( validateUsername(username) } - private fun validateDeletedUser(user: User) { + fun validateDeletedUser(user: User) { if (user.role == RoleType.DELETED_USER) { throw UserException.ExistsDeletedUserException() } From 8d72272fbfa0f427db69e0ed1b0dc4d2f9f3a0e1 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Sat, 29 Nov 2025 00:04:43 +0900 Subject: [PATCH 15/35] =?UTF-8?q?#277=20[feat]=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../photi/core/domain/user/model/OAuthInfo.kt | 2 +- .../core/domain/user/service/OAuthService.kt | 29 ++++++++++++++----- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/OAuthInfo.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/OAuthInfo.kt index eddd5275..10119357 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/OAuthInfo.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/OAuthInfo.kt @@ -18,6 +18,6 @@ class OAuthInfo( companion object { - fun of(provider: OAuthProviderType, sub: String) = OAuthInfo(provider, sub) + fun ofKakao(sub: String) = OAuthInfo(OAuthProviderType.KAKAO, sub) } } diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt index 81415c72..b819f0af 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt @@ -1,14 +1,15 @@ package com.photi.core.domain.user.service import com.photi.core.domain.common.properties.KakaoOAuthProperties +import com.photi.core.domain.user.dto.LoginDto import com.photi.core.domain.user.dto.OAuthSignUpDto import com.photi.core.domain.user.dto.SignUpDto import com.photi.core.domain.user.exception.UserException import com.photi.core.domain.user.model.OAuthInfo -import com.photi.core.domain.user.model.OAuthProviderType import com.photi.core.domain.user.port.OAuthPort import com.photi.core.domain.user.service.command.UserCommandService import com.photi.core.domain.user.service.query.UserQueryService +import com.photi.core.domain.user.validator.UserValidator import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -19,19 +20,31 @@ class OAuthService( private val kakaoOAuthProperties: KakaoOAuthProperties, private val userCommandService: UserCommandService, private val userQueryService: UserQueryService, + private val userValidator: UserValidator, ) { @Transactional fun kakaoSignUp(idToken: String, dto: OAuthSignUpDto): SignUpDto { - val idTokenPayload = oAuthPort.getIdTokenPayload( - idToken, - kakaoOAuthProperties.baseUrl, - kakaoOAuthProperties.restApiKey, - "nonce", // todo '인가 코드 요청 api' 요청 시 전달한 nonce 값과 동일한 값 - ) - val oAuthInfo = OAuthInfo.of(OAuthProviderType.KAKAO, idTokenPayload.sub) + val idTokenPayload = getOidcPayload(idToken) + val oAuthInfo = OAuthInfo.ofKakao(idTokenPayload.sub) if (userQueryService.existsBy(oAuthInfo)) throw UserException.ExistsUserException() val user = userCommandService.createUser(dto, oAuthInfo, idTokenPayload.email) return SignUpDto.of(user) } + + fun kakaoLogin(idToken: String): LoginDto { + val idTokenPayload = getOidcPayload(idToken) + val oAuthInfo = OAuthInfo.ofKakao(idTokenPayload.sub) + val user = userQueryService.getLoginUserBy(oAuthInfo) + ?: throw UserException.NotFoundUserException() + user.login(userValidator) + return LoginDto.of(user) + } + + private fun getOidcPayload(idToken: String) = oAuthPort.getIdTokenPayload( + idToken, + kakaoOAuthProperties.baseUrl, + kakaoOAuthProperties.restApiKey, + "nonce", // todo '인가 코드 요청 api' 요청 시 전달한 nonce 값과 동일한 값 + ) } From 1d3a194f7333bf72017817a57a0759e589ef0043 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Sat, 29 Nov 2025 00:05:52 +0900 Subject: [PATCH 16/35] =?UTF-8?q?#277=20[feat]=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../enduser/controller/oauth/OAuthController.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthController.kt b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthController.kt index 49979cba..838ed9ac 100644 --- a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthController.kt +++ b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthController.kt @@ -2,10 +2,12 @@ package com.photi.apis.enduser.controller.oauth import com.photi.apis.enduser.common.exception.UserApiErrorResponses import com.photi.apis.enduser.config.security.JwtTokenProvider +import com.photi.apis.enduser.controller.auth.dto.response.LoginResponse import com.photi.apis.enduser.controller.auth.dto.response.SignUpResponse import com.photi.apis.enduser.controller.oauth.request.OAuthSignUpRequest import com.photi.core.domain.user.exception.UserErrorCode.* import com.photi.core.domain.user.model.RoleType +import com.photi.core.domain.user.model.RoleType.Companion.getRole import com.photi.core.domain.user.service.OAuthService import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter @@ -13,6 +15,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.tags.Tag import jakarta.validation.Valid import org.springframework.http.HttpStatus.CREATED +import org.springframework.http.HttpStatus.OK import org.springframework.http.ResponseEntity import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.* @@ -39,4 +42,15 @@ class OAuthController( val headers = jwtTokenProvider.createToken(response.userId, RoleType.USER) return ResponseEntity.status(CREATED).headers(headers).body(response) } + + @GetMapping("/kakao/login") + @Operation(summary = "카카오 로그인") + @ApiResponse(responseCode = "200") + @UserApiErrorResponses([USER_NOT_FOUND, DELETED_USER]) + fun kakaoLogin(@RequestParam("id_token") @Parameter(description = "ID 토큰") idToken: String): ResponseEntity { + val loginUser = oAuthService.kakaoLogin(idToken) + val response = LoginResponse.of(loginUser) + val headers = jwtTokenProvider.createToken(response.userId, getRole(response.username)) + return ResponseEntity.status(OK).headers(headers).body(response) + } } From 70b2e550755393880c7442363d3813831b1bc068 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Mon, 1 Dec 2025 16:45:42 +0900 Subject: [PATCH 17/35] =?UTF-8?q?#278=20[refactor]=20=EB=AC=B8=EC=9E=90?= =?UTF-8?q?=EC=97=B4=20=EC=83=81=EC=88=98=EB=A1=9C=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../enduser/config/oidc/JwtOidcProvider.kt | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/oidc/JwtOidcProvider.kt b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/oidc/JwtOidcProvider.kt index 16f59715..89af05d5 100644 --- a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/oidc/JwtOidcProvider.kt +++ b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/oidc/JwtOidcProvider.kt @@ -20,16 +20,20 @@ class JwtOidcProvider : JwtOidcPort { aud: String, nonce: String, ) = getUnsignedIdTokenClaims(idToken, iss, aud, nonce) - .header["kid"] + .header[KID] .toString() - override fun getIdTokenPayload(idToken: String, modulus: String, exponent: String): OidcPayload { + override fun getIdTokenPayload( + idToken: String, + modulus: String, + exponent: String, + ): OidcPayload { val claims = getIdTokenClaims(idToken, modulus, exponent) return OidcPayload( claims.issuer, claims.audience.first(), claims.subject, - claims["email"].toString(), + claims[EMAIL].toString(), ) } @@ -43,7 +47,7 @@ class JwtOidcProvider : JwtOidcPort { Jwts.parser() .requireIssuer(iss) .requireAudience(aud) - .require("nonce", nonce) + .require(NONCE, nonce) .build() .parseUnsecuredClaims(getUnsignedIdToken(idToken)) } catch (e: ExpiredJwtException) { @@ -76,7 +80,7 @@ class JwtOidcProvider : JwtOidcPort { } private fun getRsaPublicKey(modulus: String, exponent: String): PublicKey { - val keyFactory = KeyFactory.getInstance("RSA") + val keyFactory = KeyFactory.getInstance(ALGORITHM) val decodeN = Base64.getUrlDecoder().decode(modulus) val decodeE = Base64.getUrlDecoder().decode(exponent) val n = BigInteger(1, decodeN) @@ -84,4 +88,11 @@ class JwtOidcProvider : JwtOidcPort { val keySpec = RSAPublicKeySpec(n, e) return keyFactory.generatePublic(keySpec) } + + companion object { + private const val KID = "kid" + private const val EMAIL = "email" + private const val NONCE = "nonce" + private const val ALGORITHM = "RSA" + } } From f8b48ab5ba151744464f1b991804c73e35b4580c Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Mon, 1 Dec 2025 16:49:08 +0900 Subject: [PATCH 18/35] =?UTF-8?q?#278=20[feat]=20=EA=B5=AC=EA=B8=80=20conf?= =?UTF-8?q?iguration=20properties=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/common/properties/GoogleOAuthProperties.kt | 9 +++++++++ .../domain/common/properties/KakaoOAuthProperties.kt | 6 +++--- .../core/domain/common/properties/OAuthProperties.kt | 6 ++++++ .../infra/properties/ConfigurationPropertiesConfig.kt | 3 ++- .../infra/src/main/resources/application-infra.yml | 6 ++++++ 5 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/GoogleOAuthProperties.kt create mode 100644 photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/OAuthProperties.kt diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/GoogleOAuthProperties.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/GoogleOAuthProperties.kt new file mode 100644 index 00000000..411d1d5d --- /dev/null +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/GoogleOAuthProperties.kt @@ -0,0 +1,9 @@ +package com.photi.core.domain.common.properties + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("oauth.google") +data class GoogleOAuthProperties( + override val baseUrl: String, + override val restApiKey: String, +) : OAuthProperties diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/KakaoOAuthProperties.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/KakaoOAuthProperties.kt index 960b3f70..151ac6f9 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/KakaoOAuthProperties.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/KakaoOAuthProperties.kt @@ -4,6 +4,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties @ConfigurationProperties("oauth.kakao") data class KakaoOAuthProperties( - val baseUrl: String, - val restApiKey: String, -) + override val baseUrl: String, + override val restApiKey: String, +) : OAuthProperties diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/OAuthProperties.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/OAuthProperties.kt new file mode 100644 index 00000000..ebda1e80 --- /dev/null +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/OAuthProperties.kt @@ -0,0 +1,6 @@ +package com.photi.core.domain.common.properties + +interface OAuthProperties { + val baseUrl: String + val restApiKey: String +} diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/properties/ConfigurationPropertiesConfig.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/properties/ConfigurationPropertiesConfig.kt index ad5e348c..1e4252ce 100644 --- a/photi-core/infra/src/main/kotlin/com/photi/core/infra/properties/ConfigurationPropertiesConfig.kt +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/properties/ConfigurationPropertiesConfig.kt @@ -1,10 +1,11 @@ package com.photi.core.infra.properties +import com.photi.core.domain.common.properties.GoogleOAuthProperties import com.photi.core.domain.common.properties.KakaoOAuthProperties import com.photi.core.infra.PhotiConfig import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Configuration @Configuration -@EnableConfigurationProperties(value = [KakaoOAuthProperties::class]) +@EnableConfigurationProperties(value = [KakaoOAuthProperties::class, GoogleOAuthProperties::class]) class ConfigurationPropertiesConfig : PhotiConfig diff --git a/photi-core/infra/src/main/resources/application-infra.yml b/photi-core/infra/src/main/resources/application-infra.yml index b0956983..e4bc8bb9 100644 --- a/photi-core/infra/src/main/resources/application-infra.yml +++ b/photi-core/infra/src/main/resources/application-infra.yml @@ -22,6 +22,9 @@ oauth: kakao: base-url: ${KAKAO_BASE_URL} rest-api-key: ${KAKAO_REST_API_KEY} + google: + base-url: ${GOOGLE_BASE_URL} + rest-api-key: ${GOOGLE_REST_API_KEY} --- spring: config: @@ -47,6 +50,9 @@ oauth: kakao: base-url: ${KAKAO_BASE_URL} rest-api-key: ${KAKAO_REST_API_KEY} + google: + base-url: ${GOOGLE_BASE_URL} + rest-api-key: ${GOOGLE_REST_API_KEY} --- spring: config: From db7b4f52d3071812f0221959408f0a1990984e83 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Mon, 1 Dec 2025 16:50:36 +0900 Subject: [PATCH 19/35] =?UTF-8?q?#278=20[feat]=20=EA=B5=AC=EA=B8=80=20?= =?UTF-8?q?=EA=B3=B5=EA=B0=9C=ED=82=A4=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20feign=20client=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/infra/oauth/client/GoogleOAuthClient.kt | 14 ++++++++++++++ .../core/infra/oauth/client/KakaoOAuthClient.kt | 4 ++-- .../photi/core/infra/oauth/client/OAuthClient.kt | 8 ++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/GoogleOAuthClient.kt create mode 100644 photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/OAuthClient.kt diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/GoogleOAuthClient.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/GoogleOAuthClient.kt new file mode 100644 index 00000000..c46c3841 --- /dev/null +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/GoogleOAuthClient.kt @@ -0,0 +1,14 @@ +package com.photi.core.infra.oauth.client + +import com.photi.core.infra.oauth.dto.OidcPublicKeysResponse +import org.springframework.cache.annotation.Cacheable +import org.springframework.cloud.openfeign.FeignClient +import org.springframework.web.bind.annotation.GetMapping + +@FeignClient(name = "GoogleOAuthClient", url = "https://www.googleapis.com") +interface GoogleOAuthClient : OAuthClient { + + @Cacheable(cacheNames = ["GoogleOidc"], cacheManager = "oidcCacheManager") + @GetMapping("/oauth2/v3/certs") + override fun getOidcPublicKeys(): OidcPublicKeysResponse +} diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/KakaoOAuthClient.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/KakaoOAuthClient.kt index b8979109..56c96c70 100644 --- a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/KakaoOAuthClient.kt +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/KakaoOAuthClient.kt @@ -6,9 +6,9 @@ import org.springframework.cloud.openfeign.FeignClient import org.springframework.web.bind.annotation.GetMapping @FeignClient(name = "KakaoOAuthClient", url = "https://kauth.kakao.com") -interface KakaoOAuthClient { +interface KakaoOAuthClient : OAuthClient { @Cacheable(cacheNames = ["KakaoOidc"], cacheManager = "oidcCacheManager") @GetMapping("/.well-known/jwks.json") - fun getOidcPublicKeys(): OidcPublicKeysResponse + override fun getOidcPublicKeys(): OidcPublicKeysResponse } diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/OAuthClient.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/OAuthClient.kt new file mode 100644 index 00000000..ba3687a5 --- /dev/null +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/OAuthClient.kt @@ -0,0 +1,8 @@ +package com.photi.core.infra.oauth.client + +import com.photi.core.infra.oauth.dto.OidcPublicKeysResponse + +interface OAuthClient { + + fun getOidcPublicKeys(): OidcPublicKeysResponse +} From d8b99d2293321dd6beeedafc72f3e4f224504079 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Mon, 1 Dec 2025 16:58:36 +0900 Subject: [PATCH 20/35] =?UTF-8?q?#278=20[feat]=20=EA=B5=AC=EA=B8=80=20oidc?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EB=A1=9C=EB=93=9C=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=B0=8F=20properties,=20oauthinfo=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../photi/core/domain/user/model/OAuthInfo.kt | 2 ++ .../photi/core/domain/user/port/OAuthPort.kt | 6 ++++ .../infra/oauth/adapter/GoogleOAuthAdapter.kt | 33 +++++++++++++++++++ .../infra/oauth/adapter/KakaoOAuthAdapter.kt | 7 ++++ 4 files changed, 48 insertions(+) create mode 100644 photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/GoogleOAuthAdapter.kt diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/OAuthInfo.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/OAuthInfo.kt index 10119357..e09d1e7b 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/OAuthInfo.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/OAuthInfo.kt @@ -19,5 +19,7 @@ class OAuthInfo( companion object { fun ofKakao(sub: String) = OAuthInfo(OAuthProviderType.KAKAO, sub) + + fun ofGoogle(sub: String) = OAuthInfo(OAuthProviderType.GOOGLE, sub) } } diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/port/OAuthPort.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/port/OAuthPort.kt index 83359855..4431a578 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/port/OAuthPort.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/port/OAuthPort.kt @@ -1,8 +1,14 @@ package com.photi.core.domain.user.port +import com.photi.core.domain.common.properties.OAuthProperties import com.photi.core.domain.user.dto.OidcPayload +import com.photi.core.domain.user.model.OAuthInfo interface OAuthPort { fun getIdTokenPayload(idToken: String, iss: String, aud: String, nonce: String): OidcPayload + + fun getProperties(): OAuthProperties + + fun createOAuthInfo(sub: String): OAuthInfo } diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/GoogleOAuthAdapter.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/GoogleOAuthAdapter.kt new file mode 100644 index 00000000..ec27f5d1 --- /dev/null +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/GoogleOAuthAdapter.kt @@ -0,0 +1,33 @@ +package com.photi.core.infra.oauth.adapter + +import com.photi.core.domain.common.properties.GoogleOAuthProperties +import com.photi.core.domain.user.dto.OidcPayload +import com.photi.core.domain.user.model.OAuthInfo +import com.photi.core.domain.user.port.OAuthPort +import com.photi.core.infra.oauth.client.GoogleOAuthClient +import com.photi.core.infra.oauth.port.JwtOidcPort +import org.springframework.stereotype.Component + +@Component +class GoogleOAuthAdapter( + private val googleOAuthProperties: GoogleOAuthProperties, + private val googleOAuthClient: GoogleOAuthClient, + private val jwtOidcPort: JwtOidcPort, +) : OAuthPort { + + override fun getIdTokenPayload( + idToken: String, + iss: String, + aud: String, + nonce: String, + ): OidcPayload { + val publicKeys = googleOAuthClient.getOidcPublicKeys() + val kid = jwtOidcPort.getKidFromUnsignedIdToken(idToken, iss, aud, nonce) + val jwk = publicKeys.keys.first { it.kid == kid } + return jwtOidcPort.getIdTokenPayload(idToken, jwk.n, jwk.e) + } + + override fun getProperties() = googleOAuthProperties + + override fun createOAuthInfo(sub: String) = OAuthInfo.ofGoogle(sub) +} diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/KakaoOAuthAdapter.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/KakaoOAuthAdapter.kt index 913118af..c6972c22 100644 --- a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/KakaoOAuthAdapter.kt +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/KakaoOAuthAdapter.kt @@ -1,6 +1,8 @@ package com.photi.core.infra.oauth.adapter +import com.photi.core.domain.common.properties.KakaoOAuthProperties import com.photi.core.domain.user.dto.OidcPayload +import com.photi.core.domain.user.model.OAuthInfo import com.photi.core.domain.user.port.OAuthPort import com.photi.core.infra.oauth.client.KakaoOAuthClient import com.photi.core.infra.oauth.port.JwtOidcPort @@ -8,6 +10,7 @@ import org.springframework.stereotype.Component @Component class KakaoOAuthAdapter( + private val kakaoOAuthProperties: KakaoOAuthProperties, private val kakaoOAuthClient: KakaoOAuthClient, private val jwtOidcPort: JwtOidcPort, ) : OAuthPort { @@ -23,4 +26,8 @@ class KakaoOAuthAdapter( val jwk = publicKeys.keys.first { it.kid == kid } return jwtOidcPort.getIdTokenPayload(idToken, jwk.n, jwk.e) } + + override fun getProperties() = kakaoOAuthProperties + + override fun createOAuthInfo(sub: String) = OAuthInfo.ofKakao(sub) } From 122914668d94ccdeb729c7a8e6c2ee073dea5388 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Mon, 1 Dec 2025 16:58:51 +0900 Subject: [PATCH 21/35] =?UTF-8?q?#278=20[feat]=20oauth=20=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/domain/user/port/OAuthFactoryPort.kt | 8 ++++++++ .../infra/oauth/adapter/OAuthFactoryAdapter.kt | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 photi-core/domain/src/main/kotlin/com/photi/core/domain/user/port/OAuthFactoryPort.kt create mode 100644 photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/OAuthFactoryAdapter.kt diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/port/OAuthFactoryPort.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/port/OAuthFactoryPort.kt new file mode 100644 index 00000000..ac393d69 --- /dev/null +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/port/OAuthFactoryPort.kt @@ -0,0 +1,8 @@ +package com.photi.core.domain.user.port + +import com.photi.core.domain.user.model.OAuthProviderType + +interface OAuthFactoryPort { + + fun getOAuthAdapter(provider: OAuthProviderType): OAuthPort +} diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/OAuthFactoryAdapter.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/OAuthFactoryAdapter.kt new file mode 100644 index 00000000..be83fd5f --- /dev/null +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/OAuthFactoryAdapter.kt @@ -0,0 +1,18 @@ +package com.photi.core.infra.oauth.adapter + +import com.photi.core.domain.user.model.OAuthProviderType +import com.photi.core.domain.user.port.OAuthFactoryPort +import org.springframework.stereotype.Component + +@Component +class OAuthFactoryAdapter( + private val kakaoOAuthAdapter: KakaoOAuthAdapter, + private val googleOAuthAdapter: GoogleOAuthAdapter, +) : OAuthFactoryPort { + + override fun getOAuthAdapter(provider: OAuthProviderType) = when (provider) { + OAuthProviderType.KAKAO -> kakaoOAuthAdapter + OAuthProviderType.GOOGLE -> googleOAuthAdapter + OAuthProviderType.APPLE -> TODO() + } +} From c0db85c68af3ad51c3defe82ff7bbaf20df702f6 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Mon, 1 Dec 2025 17:00:26 +0900 Subject: [PATCH 22/35] =?UTF-8?q?#278=20[refactor]=20oauth=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85,=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/domain/user/service/OAuthService.kt | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt index b819f0af..953e9b4f 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt @@ -1,11 +1,12 @@ package com.photi.core.domain.user.service -import com.photi.core.domain.common.properties.KakaoOAuthProperties import com.photi.core.domain.user.dto.LoginDto import com.photi.core.domain.user.dto.OAuthSignUpDto +import com.photi.core.domain.user.dto.OidcPayload import com.photi.core.domain.user.dto.SignUpDto import com.photi.core.domain.user.exception.UserException -import com.photi.core.domain.user.model.OAuthInfo +import com.photi.core.domain.user.model.OAuthProviderType +import com.photi.core.domain.user.port.OAuthFactoryPort import com.photi.core.domain.user.port.OAuthPort import com.photi.core.domain.user.service.command.UserCommandService import com.photi.core.domain.user.service.query.UserQueryService @@ -16,35 +17,39 @@ import org.springframework.transaction.annotation.Transactional @Service @Transactional(readOnly = true) class OAuthService( - private val oAuthPort: OAuthPort, - private val kakaoOAuthProperties: KakaoOAuthProperties, + private val oAuthFactory: OAuthFactoryPort, private val userCommandService: UserCommandService, private val userQueryService: UserQueryService, private val userValidator: UserValidator, ) { @Transactional - fun kakaoSignUp(idToken: String, dto: OAuthSignUpDto): SignUpDto { - val idTokenPayload = getOidcPayload(idToken) - val oAuthInfo = OAuthInfo.ofKakao(idTokenPayload.sub) + fun signUp(provider: OAuthProviderType, idToken: String, dto: OAuthSignUpDto): SignUpDto { + val oAuthPort = oAuthFactory.getOAuthAdapter(provider) + val idTokenPayload = getOidcPayload(idToken, oAuthPort) + val oAuthInfo = oAuthPort.createOAuthInfo(idTokenPayload.sub) if (userQueryService.existsBy(oAuthInfo)) throw UserException.ExistsUserException() val user = userCommandService.createUser(dto, oAuthInfo, idTokenPayload.email) return SignUpDto.of(user) } - fun kakaoLogin(idToken: String): LoginDto { - val idTokenPayload = getOidcPayload(idToken) - val oAuthInfo = OAuthInfo.ofKakao(idTokenPayload.sub) + fun login(provider: OAuthProviderType, idToken: String): LoginDto { + val oAuthPort = oAuthFactory.getOAuthAdapter(provider) + val idTokenPayload = getOidcPayload(idToken, oAuthPort) + val oAuthInfo = oAuthPort.createOAuthInfo(idTokenPayload.sub) val user = userQueryService.getLoginUserBy(oAuthInfo) ?: throw UserException.NotFoundUserException() user.login(userValidator) return LoginDto.of(user) } - private fun getOidcPayload(idToken: String) = oAuthPort.getIdTokenPayload( - idToken, - kakaoOAuthProperties.baseUrl, - kakaoOAuthProperties.restApiKey, - "nonce", // todo '인가 코드 요청 api' 요청 시 전달한 nonce 값과 동일한 값 - ) + private fun getOidcPayload(idToken: String, oAuthPort: OAuthPort): OidcPayload { + val properties = oAuthPort.getProperties() + return oAuthPort.getIdTokenPayload( + idToken, + properties.baseUrl, + properties.restApiKey, + "nonce", // todo '인가 코드 요청 api' 요청 시 전달한 nonce 값과 동일한 값 + ) + } } From cbb3a2c3484d01a20652c66d6c5cf752a877b551 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Mon, 1 Dec 2025 17:01:29 +0900 Subject: [PATCH 23/35] =?UTF-8?q?#278=20[refactor]=20oauth=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85,=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/oauth/OAuthController.kt | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthController.kt b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthController.kt index 838ed9ac..6274ba51 100644 --- a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthController.kt +++ b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthController.kt @@ -6,6 +6,7 @@ import com.photi.apis.enduser.controller.auth.dto.response.LoginResponse import com.photi.apis.enduser.controller.auth.dto.response.SignUpResponse import com.photi.apis.enduser.controller.oauth.request.OAuthSignUpRequest import com.photi.core.domain.user.exception.UserErrorCode.* +import com.photi.core.domain.user.model.OAuthProviderType import com.photi.core.domain.user.model.RoleType import com.photi.core.domain.user.model.RoleType.Companion.getRole import com.photi.core.domain.user.service.OAuthService @@ -29,26 +30,30 @@ class OAuthController( private val jwtTokenProvider: JwtTokenProvider, ) { - @PostMapping("/kakao/signup") - @Operation(summary = "카카오 회원가입") + @PostMapping("/{provider}/signup") + @Operation(summary = "OAuth 회원가입") @ApiResponse(responseCode = "201") @UserApiErrorResponses([EXISTING_USER]) - fun kakaoSignUp( + fun signUp( + @PathVariable provider: OAuthProviderType, @RequestParam("id_token") @Parameter(description = "ID 토큰") idToken: String, @RequestBody @Valid request: OAuthSignUpRequest, ): ResponseEntity { - val signUpUser = oAuthService.kakaoSignUp(idToken, request.toServiceDto()) + val signUpUser = oAuthService.signUp(provider, idToken, request.toServiceDto()) val response = SignUpResponse.of(signUpUser) val headers = jwtTokenProvider.createToken(response.userId, RoleType.USER) return ResponseEntity.status(CREATED).headers(headers).body(response) } - @GetMapping("/kakao/login") - @Operation(summary = "카카오 로그인") + @GetMapping("/{provider}/login") + @Operation(summary = "OAuth 로그인") @ApiResponse(responseCode = "200") @UserApiErrorResponses([USER_NOT_FOUND, DELETED_USER]) - fun kakaoLogin(@RequestParam("id_token") @Parameter(description = "ID 토큰") idToken: String): ResponseEntity { - val loginUser = oAuthService.kakaoLogin(idToken) + fun login( + @PathVariable provider: OAuthProviderType, + @RequestParam("id_token") @Parameter(description = "ID 토큰") idToken: String, + ): ResponseEntity { + val loginUser = oAuthService.login(provider, idToken) val response = LoginResponse.of(loginUser) val headers = jwtTokenProvider.createToken(response.userId, getRole(response.username)) return ResponseEntity.status(OK).headers(headers).body(response) From fefa2166e8e2763187bcb6413a2cce02a15074e7 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Mon, 1 Dec 2025 17:01:50 +0900 Subject: [PATCH 24/35] =?UTF-8?q?#278=20[feat]=20oauth=20provider=20?= =?UTF-8?q?=EC=BB=A8=EB=B2=84=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/oauth/OAuthProviderConverter.kt | 18 ++++++++++++++++++ .../domain/user/exception/UserErrorCode.kt | 3 ++- .../domain/user/exception/UserException.kt | 2 ++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthProviderConverter.kt diff --git a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthProviderConverter.kt b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthProviderConverter.kt new file mode 100644 index 00000000..b158e1d0 --- /dev/null +++ b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthProviderConverter.kt @@ -0,0 +1,18 @@ +package com.photi.apis.enduser.controller.oauth + +import com.photi.core.domain.user.exception.UserException +import com.photi.core.domain.user.model.OAuthProviderType +import org.springframework.core.convert.converter.Converter +import org.springframework.stereotype.Component + +@Component +class OAuthProviderConverter : Converter { + + override fun convert(source: String): OAuthProviderType? { + return try { + OAuthProviderType.valueOf(source.lowercase()) + } catch (e: IllegalArgumentException) { + throw UserException.InvalidOAuthProviderException() + } + } +} diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/exception/UserErrorCode.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/exception/UserErrorCode.kt index fb725166..129f863f 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/exception/UserErrorCode.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/exception/UserErrorCode.kt @@ -31,5 +31,6 @@ enum class UserErrorCode( "USERNAME_FORMAT_INVALID", "아이디는 소문자 영어, 숫자, 특수문자(_)의 조합으로 입력해 주세요.", "아이디는 5~20자만 가능하고, 정규식은 ^[a-z0-9_]+$ 입니다.", - ); + ), + OAUTH_PROVIDER_INVALID(BAD_REQUEST, "OAUTH_PROVIDER_INVALID", "지원하지 않는 OAuth Provider 입니다."), } diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/exception/UserException.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/exception/UserException.kt index 9d1acc1d..57496b6e 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/exception/UserException.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/exception/UserException.kt @@ -24,4 +24,6 @@ sealed class UserException(errorCode: UserErrorCode) : PhotiException(errorCode) class ExistsUsernameException : UserException(UserErrorCode.EXISTING_USERNAME) class NotAvailableUsernameException : UserException(UserErrorCode.UNAVAILABLE_USERNAME) + + class InvalidOAuthProviderException : UserException(UserErrorCode.OAUTH_PROVIDER_INVALID) } From 49490128783cc14d0088d2b9b3451a38c55faf80 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Mon, 1 Dec 2025 18:01:09 +0900 Subject: [PATCH 25/35] =?UTF-8?q?#278=20[fix]=20provider=20=EC=86=8C?= =?UTF-8?q?=EB=AC=B8=EC=9E=90=EB=8F=84=20=ED=97=88=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/exception/CustomExceptionHandler.kt | 17 +++++++++++++---- .../controller/oauth/OAuthProviderConverter.kt | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/common/exception/CustomExceptionHandler.kt b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/common/exception/CustomExceptionHandler.kt index af469c45..e88eb19e 100644 --- a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/common/exception/CustomExceptionHandler.kt +++ b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/common/exception/CustomExceptionHandler.kt @@ -3,6 +3,7 @@ package com.photi.apis.enduser.common.exception import com.photi.apis.enduser.common.exception.dto.ErrorResponse import com.photi.core.domain.common.exception.GlobalErrorCode import com.photi.core.domain.common.exception.PhotiException +import com.photi.core.domain.user.exception.UserErrorCode import jakarta.servlet.http.HttpServletRequest import jakarta.validation.ConstraintViolationException import org.springframework.beans.TypeMismatchException @@ -71,10 +72,18 @@ class CustomExceptionHandler : ResponseEntityExceptionHandler() { status: HttpStatusCode, request: WebRequest ): ResponseEntity? { - val response = ErrorResponse( - GlobalErrorCode.DATE_FORMAT_INVALID.name, - GlobalErrorCode.DATE_FORMAT_INVALID.message, - ) + val isEnum = ex.requiredType?.isEnum == true + val response = if (isEnum) { + ErrorResponse( + UserErrorCode.OAUTH_PROVIDER_INVALID.name, + UserErrorCode.OAUTH_PROVIDER_INVALID.message, + ) + } else { + ErrorResponse( + GlobalErrorCode.DATE_FORMAT_INVALID.name, + GlobalErrorCode.DATE_FORMAT_INVALID.message, + ) + } return ResponseEntity.status(BAD_REQUEST).body(response) } diff --git a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthProviderConverter.kt b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthProviderConverter.kt index b158e1d0..4df72922 100644 --- a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthProviderConverter.kt +++ b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthProviderConverter.kt @@ -10,7 +10,7 @@ class OAuthProviderConverter : Converter { override fun convert(source: String): OAuthProviderType? { return try { - OAuthProviderType.valueOf(source.lowercase()) + OAuthProviderType.valueOf(source.uppercase()) } catch (e: IllegalArgumentException) { throw UserException.InvalidOAuthProviderException() } From cac38111e7dff816999938edd58ec19c032c48ec Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Mon, 1 Dec 2025 23:42:10 +0900 Subject: [PATCH 26/35] =?UTF-8?q?#279=20[feat]=20=EC=95=A0=ED=94=8C=20conf?= =?UTF-8?q?iguration=20properties=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/common/properties/AppleOAuthProperties.kt | 9 +++++++++ .../infra/properties/ConfigurationPropertiesConfig.kt | 9 ++++++++- .../infra/src/main/resources/application-infra.yml | 6 ++++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/AppleOAuthProperties.kt diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/AppleOAuthProperties.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/AppleOAuthProperties.kt new file mode 100644 index 00000000..a5090d60 --- /dev/null +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/AppleOAuthProperties.kt @@ -0,0 +1,9 @@ +package com.photi.core.domain.common.properties + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("oauth.apple") +data class AppleOAuthProperties( + override val baseUrl: String, + override val restApiKey: String, +) : OAuthProperties diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/properties/ConfigurationPropertiesConfig.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/properties/ConfigurationPropertiesConfig.kt index 1e4252ce..ba239008 100644 --- a/photi-core/infra/src/main/kotlin/com/photi/core/infra/properties/ConfigurationPropertiesConfig.kt +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/properties/ConfigurationPropertiesConfig.kt @@ -1,5 +1,6 @@ package com.photi.core.infra.properties +import com.photi.core.domain.common.properties.AppleOAuthProperties import com.photi.core.domain.common.properties.GoogleOAuthProperties import com.photi.core.domain.common.properties.KakaoOAuthProperties import com.photi.core.infra.PhotiConfig @@ -7,5 +8,11 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Configuration @Configuration -@EnableConfigurationProperties(value = [KakaoOAuthProperties::class, GoogleOAuthProperties::class]) +@EnableConfigurationProperties( + value = [ + KakaoOAuthProperties::class, + GoogleOAuthProperties::class, + AppleOAuthProperties::class, + ] +) class ConfigurationPropertiesConfig : PhotiConfig diff --git a/photi-core/infra/src/main/resources/application-infra.yml b/photi-core/infra/src/main/resources/application-infra.yml index e4bc8bb9..67bf2085 100644 --- a/photi-core/infra/src/main/resources/application-infra.yml +++ b/photi-core/infra/src/main/resources/application-infra.yml @@ -25,6 +25,9 @@ oauth: google: base-url: ${GOOGLE_BASE_URL} rest-api-key: ${GOOGLE_REST_API_KEY} + apple: + base-url: ${APPLE_BASE_URL} + rest-api-key: ${APPLE_REST_API_KEY} --- spring: config: @@ -53,6 +56,9 @@ oauth: google: base-url: ${GOOGLE_BASE_URL} rest-api-key: ${GOOGLE_REST_API_KEY} + apple: + base-url: ${APPLE_BASE_URL} + rest-api-key: ${APPLE_REST_API_KEY} --- spring: config: From 65a29264273eb8ad106ae7ba4b17ef737c367cdf Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Mon, 1 Dec 2025 23:42:58 +0900 Subject: [PATCH 27/35] =?UTF-8?q?#279=20[feat]=20=EC=95=A0=ED=94=8C=20?= =?UTF-8?q?=EA=B3=B5=EA=B0=9C=ED=82=A4=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20feign=20client=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/infra/oauth/client/AppleOAuthClient.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/AppleOAuthClient.kt diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/AppleOAuthClient.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/AppleOAuthClient.kt new file mode 100644 index 00000000..ee7e3bfe --- /dev/null +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/client/AppleOAuthClient.kt @@ -0,0 +1,14 @@ +package com.photi.core.infra.oauth.client + +import com.photi.core.infra.oauth.dto.OidcPublicKeysResponse +import org.springframework.cache.annotation.Cacheable +import org.springframework.cloud.openfeign.FeignClient +import org.springframework.web.bind.annotation.GetMapping + +@FeignClient(name = "AppleOAuthClient", url = "https://appleid.apple.com") +interface AppleOAuthClient : OAuthClient { + + @Cacheable(cacheNames = ["AppleOidc"], cacheManager = "oidcCacheManager") + @GetMapping("/auth/keys") + override fun getOidcPublicKeys(): OidcPublicKeysResponse +} From 25e5635eba64c0ed46c60d68b7b4166b98079520 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Mon, 1 Dec 2025 23:44:04 +0900 Subject: [PATCH 28/35] =?UTF-8?q?#279=20[feat]=20=EC=95=A0=ED=94=8C=20oaut?= =?UTF-8?q?h=20adapter=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../photi/core/domain/user/model/OAuthInfo.kt | 2 ++ .../infra/oauth/adapter/AppleOAuthAdapter.kt | 33 +++++++++++++++++++ .../oauth/adapter/OAuthFactoryAdapter.kt | 3 +- 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/AppleOAuthAdapter.kt diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/OAuthInfo.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/OAuthInfo.kt index e09d1e7b..481dc133 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/OAuthInfo.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/OAuthInfo.kt @@ -21,5 +21,7 @@ class OAuthInfo( fun ofKakao(sub: String) = OAuthInfo(OAuthProviderType.KAKAO, sub) fun ofGoogle(sub: String) = OAuthInfo(OAuthProviderType.GOOGLE, sub) + + fun ofApple(sub: String) = OAuthInfo(OAuthProviderType.APPLE, sub) } } diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/AppleOAuthAdapter.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/AppleOAuthAdapter.kt new file mode 100644 index 00000000..27f0473d --- /dev/null +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/AppleOAuthAdapter.kt @@ -0,0 +1,33 @@ +package com.photi.core.infra.oauth.adapter + +import com.photi.core.domain.common.properties.AppleOAuthProperties +import com.photi.core.domain.user.dto.OidcPayload +import com.photi.core.domain.user.model.OAuthInfo +import com.photi.core.domain.user.port.OAuthPort +import com.photi.core.infra.oauth.client.AppleOAuthClient +import com.photi.core.infra.oauth.port.JwtOidcPort +import org.springframework.stereotype.Component + +@Component +class AppleOAuthAdapter( + private val appleOAuthProperties: AppleOAuthProperties, + private val appleOAuthClient: AppleOAuthClient, + private val jwtOidcPort: JwtOidcPort, +) : OAuthPort { + + override fun getIdTokenPayload( + idToken: String, + iss: String, + aud: String, + nonce: String, + ): OidcPayload { + val publicKeys = appleOAuthClient.getOidcPublicKeys() + val kid = jwtOidcPort.getKidFromUnsignedIdToken(idToken, iss, aud, nonce) + val jwk = publicKeys.keys.first { it.kid == kid } + return jwtOidcPort.getIdTokenPayload(idToken, jwk.n, jwk.e) + } + + override fun getProperties() = appleOAuthProperties + + override fun createOAuthInfo(sub: String) = OAuthInfo.ofApple(sub) +} diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/OAuthFactoryAdapter.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/OAuthFactoryAdapter.kt index be83fd5f..5334d7f1 100644 --- a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/OAuthFactoryAdapter.kt +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/OAuthFactoryAdapter.kt @@ -8,11 +8,12 @@ import org.springframework.stereotype.Component class OAuthFactoryAdapter( private val kakaoOAuthAdapter: KakaoOAuthAdapter, private val googleOAuthAdapter: GoogleOAuthAdapter, + private val appleOAuthAdapter: AppleOAuthAdapter, ) : OAuthFactoryPort { override fun getOAuthAdapter(provider: OAuthProviderType) = when (provider) { OAuthProviderType.KAKAO -> kakaoOAuthAdapter OAuthProviderType.GOOGLE -> googleOAuthAdapter - OAuthProviderType.APPLE -> TODO() + OAuthProviderType.APPLE -> appleOAuthAdapter } } From 11fded16d80dd7d72ff3ba982acea60cdce408e0 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Mon, 22 Dec 2025 00:38:10 +0900 Subject: [PATCH 29/35] =?UTF-8?q?#279=20[fix]=20json=20=EC=97=AD=EC=A7=81?= =?UTF-8?q?=EB=A0=AC=ED=99=94=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/infra/oauth/dto/OidcPublicKeysResponse.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/dto/OidcPublicKeysResponse.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/dto/OidcPublicKeysResponse.kt index 6c0dd29a..2f919d80 100644 --- a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/dto/OidcPublicKeysResponse.kt +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/dto/OidcPublicKeysResponse.kt @@ -1,14 +1,14 @@ package com.photi.core.infra.oauth.dto data class OidcPublicKeysResponse( - val keys: List, + val keys: List = listOf(), ) data class Jwk( - val kid: String, - val kty: String, - val alg: String, - val use: String, - val n: String, - val e: String, + val kid: String = "", + val kty: String = "", + val alg: String = "", + val use: String = "", + val n: String = "", + val e: String = "", ) From b428215470c5082071e2e228d1b8381f672d9518 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Mon, 22 Dec 2025 00:39:31 +0900 Subject: [PATCH 30/35] =?UTF-8?q?#279=20[refactor]=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=EC=97=90=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20url?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/photi/core/domain/user/dto/OAuthSignUpDto.kt | 3 ++- .../kotlin/com/photi/core/domain/user/dto/OidcPayload.kt | 1 + .../main/kotlin/com/photi/core/domain/user/model/User.kt | 3 ++- .../com/photi/core/domain/user/service/OAuthService.kt | 7 ++++++- .../core/domain/user/service/command/UserCommandService.kt | 4 ++-- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OAuthSignUpDto.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OAuthSignUpDto.kt index 8659ae25..3debca70 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OAuthSignUpDto.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OAuthSignUpDto.kt @@ -8,12 +8,13 @@ data class OAuthSignUpDto( val username: String, ) { - fun toEntity(oAuthInfo: OAuthInfo, email: String) = + fun toEntity(oAuthInfo: OAuthInfo, email: String, image: String) = User( email = email, oAuthInfo = oAuthInfo, username = username, role = RoleType.USER, isAuthenticated = true, + imageUrl = image, ) } diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OidcPayload.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OidcPayload.kt index 1e61b78b..7b579173 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OidcPayload.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OidcPayload.kt @@ -5,4 +5,5 @@ data class OidcPayload( val aud: String, val sub: String, val email: String, + val image: String, ) diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/User.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/User.kt index b9a0d8ca..7bdcf6e4 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/User.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/User.kt @@ -25,6 +25,7 @@ class User( username: String? = null, oAuthInfo: OAuthInfo? = null, role: RoleType = RoleType.UNAUTHENTICATED_USER, + imageUrl: String? = null, ) : BaseTimeEntity() { @Id @@ -58,7 +59,7 @@ class User( protected set @Column(nullable = true, length = 500) - var imageUrl: String? = null + var imageUrl: String? = imageUrl protected set @Enumerated(value = EnumType.STRING) diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt index 953e9b4f..4d16c428 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt @@ -29,7 +29,12 @@ class OAuthService( val idTokenPayload = getOidcPayload(idToken, oAuthPort) val oAuthInfo = oAuthPort.createOAuthInfo(idTokenPayload.sub) if (userQueryService.existsBy(oAuthInfo)) throw UserException.ExistsUserException() - val user = userCommandService.createUser(dto, oAuthInfo, idTokenPayload.email) + val user = userCommandService.createUser( + dto, + oAuthInfo, + idTokenPayload.email, + idTokenPayload.image, + ) return SignUpDto.of(user) } diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/command/UserCommandService.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/command/UserCommandService.kt index 308cbba8..f08d4a07 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/command/UserCommandService.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/command/UserCommandService.kt @@ -17,6 +17,6 @@ class UserCommandService( userRepository.save(dto.toEntity(authenticationCode)) } - fun createUser(dto: OAuthSignUpDto, oAuthInfo: OAuthInfo, email: String) = - userRepository.save(dto.toEntity(oAuthInfo, email)) + fun createUser(dto: OAuthSignUpDto, oAuthInfo: OAuthInfo, email: String, image: String) = + userRepository.save(dto.toEntity(oAuthInfo, email, image)) } From 530deb7f05c4b37bc2ff4e0ffd32c50c796d9444 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Mon, 22 Dec 2025 00:41:19 +0900 Subject: [PATCH 31/35] =?UTF-8?q?#279=20[fix]=20jwt=20oidc=20=ED=8C=8C?= =?UTF-8?q?=EC=8B=B1=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../enduser/config/oidc/JwtOidcProvider.kt | 51 ++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/oidc/JwtOidcProvider.kt b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/oidc/JwtOidcProvider.kt index 89af05d5..514ee60b 100644 --- a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/oidc/JwtOidcProvider.kt +++ b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/oidc/JwtOidcProvider.kt @@ -1,5 +1,6 @@ package com.photi.apis.enduser.config.oidc +import com.fasterxml.jackson.databind.ObjectMapper import com.photi.core.domain.common.exception.GlobalException import com.photi.core.domain.user.dto.OidcPayload import com.photi.core.infra.oauth.port.JwtOidcPort @@ -12,16 +13,22 @@ import java.security.spec.RSAPublicKeySpec import java.util.* @Component -class JwtOidcProvider : JwtOidcPort { +class JwtOidcProvider( + private val objectMapper: ObjectMapper, +) : JwtOidcPort { override fun getKidFromUnsignedIdToken( idToken: String, iss: String, aud: String, nonce: String, - ) = getUnsignedIdTokenClaims(idToken, iss, aud, nonce) - .header[KID] - .toString() + ): String { + val splitIdToken = getSplitIdToken(idToken) + validatePayload(splitIdToken[1], iss, aud, nonce) + val headerJson = String(Base64.getUrlDecoder().decode(splitIdToken[0])) + val headerMap = objectMapper.readValue(headerJson, Map::class.java) + return headerMap[KID].toString() + } override fun getIdTokenPayload( idToken: String, @@ -34,35 +41,27 @@ class JwtOidcProvider : JwtOidcPort { claims.audience.first(), claims.subject, claims[EMAIL].toString(), + claims[PICTURE].toString(), ) } - private fun getUnsignedIdTokenClaims( - idToken: String, - iss: String, - aud: String, - nonce: String, - ): Jwt { - return try { - Jwts.parser() - .requireIssuer(iss) - .requireAudience(aud) - .require(NONCE, nonce) - .build() - .parseUnsecuredClaims(getUnsignedIdToken(idToken)) - } catch (e: ExpiredJwtException) { - throw GlobalException.ExpiredTokenException() - } catch (e: Exception) { + private fun getSplitIdToken(idToken: String): List { + val splitToken = idToken.split(".") + if (splitToken.size != 3) { throw GlobalException.InvalidTokenException() } + return splitToken } - private fun getUnsignedIdToken(idToken: String): String { - val splitToken = idToken.split("\\.") - if (splitToken.size != 3) { + private fun validatePayload(payload: String, iss: String, aud: String, nonce: String) { + val payloadJson = String(Base64.getUrlDecoder().decode(payload)) + val payloadMap = objectMapper.readValue(payloadJson, Map::class.java) + if (payloadMap[ISS] != iss || payloadMap[AUD] != aud || payloadMap[NONCE] != nonce) { throw GlobalException.InvalidTokenException() } - return "${splitToken[0]}.${splitToken[1]}." + val exp = (payloadMap[EXP] as? Number)?.toLong() + ?: throw GlobalException.InvalidTokenException() + if (Date().time / 1000 > exp) throw GlobalException.ExpiredTokenException() } private fun getIdTokenClaims(idToken: String, modulus: String, exponent: String): Claims { @@ -91,7 +90,11 @@ class JwtOidcProvider : JwtOidcPort { companion object { private const val KID = "kid" + private const val ISS = "iss" + private const val AUD = "aud" + private const val EXP = "exp" private const val EMAIL = "email" + private const val PICTURE = "picture" private const val NONCE = "nonce" private const val ALGORITHM = "RSA" } From 967ade55fa891e5543d5d655638993db55f6ba4d Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Mon, 22 Dec 2025 01:00:55 +0900 Subject: [PATCH 32/35] =?UTF-8?q?#279=20[refactor]=20restapi=20=ED=82=A4?= =?UTF-8?q?=20->=20=EB=84=A4=EC=9D=B4=ED=8B=B0=EB=B8=8C=20=EC=95=B1=20?= =?UTF-8?q?=ED=82=A4=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/common/properties/AppleOAuthProperties.kt | 2 +- .../common/properties/GoogleOAuthProperties.kt | 2 +- .../domain/common/properties/KakaoOAuthProperties.kt | 2 +- .../core/domain/common/properties/OAuthProperties.kt | 2 +- .../photi/core/domain/user/service/OAuthService.kt | 2 +- .../infra/src/main/resources/application-infra.yml | 12 ++++++------ 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/AppleOAuthProperties.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/AppleOAuthProperties.kt index a5090d60..53c41ab2 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/AppleOAuthProperties.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/AppleOAuthProperties.kt @@ -5,5 +5,5 @@ import org.springframework.boot.context.properties.ConfigurationProperties @ConfigurationProperties("oauth.apple") data class AppleOAuthProperties( override val baseUrl: String, - override val restApiKey: String, + override val nativeAppKey: String, ) : OAuthProperties diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/GoogleOAuthProperties.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/GoogleOAuthProperties.kt index 411d1d5d..163fcfaf 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/GoogleOAuthProperties.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/GoogleOAuthProperties.kt @@ -5,5 +5,5 @@ import org.springframework.boot.context.properties.ConfigurationProperties @ConfigurationProperties("oauth.google") data class GoogleOAuthProperties( override val baseUrl: String, - override val restApiKey: String, + override val nativeAppKey: String, ) : OAuthProperties diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/KakaoOAuthProperties.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/KakaoOAuthProperties.kt index 151ac6f9..18a9de01 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/KakaoOAuthProperties.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/KakaoOAuthProperties.kt @@ -5,5 +5,5 @@ import org.springframework.boot.context.properties.ConfigurationProperties @ConfigurationProperties("oauth.kakao") data class KakaoOAuthProperties( override val baseUrl: String, - override val restApiKey: String, + override val nativeAppKey: String, ) : OAuthProperties diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/OAuthProperties.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/OAuthProperties.kt index ebda1e80..d0531dc8 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/OAuthProperties.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/common/properties/OAuthProperties.kt @@ -2,5 +2,5 @@ package com.photi.core.domain.common.properties interface OAuthProperties { val baseUrl: String - val restApiKey: String + val nativeAppKey: String } diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt index 4d16c428..c0d4f5ab 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt @@ -53,7 +53,7 @@ class OAuthService( return oAuthPort.getIdTokenPayload( idToken, properties.baseUrl, - properties.restApiKey, + properties.nativeAppKey, "nonce", // todo '인가 코드 요청 api' 요청 시 전달한 nonce 값과 동일한 값 ) } diff --git a/photi-core/infra/src/main/resources/application-infra.yml b/photi-core/infra/src/main/resources/application-infra.yml index 67bf2085..5e090ee6 100644 --- a/photi-core/infra/src/main/resources/application-infra.yml +++ b/photi-core/infra/src/main/resources/application-infra.yml @@ -21,13 +21,13 @@ cloud: oauth: kakao: base-url: ${KAKAO_BASE_URL} - rest-api-key: ${KAKAO_REST_API_KEY} + native-app-key: ${KAKAO_NATIVE_APP_KEY} google: base-url: ${GOOGLE_BASE_URL} - rest-api-key: ${GOOGLE_REST_API_KEY} + native-app-key: ${GOOGLE_NATIVE_APP_KEY} apple: base-url: ${APPLE_BASE_URL} - rest-api-key: ${APPLE_REST_API_KEY} + native-app-key: ${APPLE_NATIVE_APP_KEY} --- spring: config: @@ -52,13 +52,13 @@ cloud: oauth: kakao: base-url: ${KAKAO_BASE_URL} - rest-api-key: ${KAKAO_REST_API_KEY} + native-app-key: ${KAKAO_NATIVE_APP_KEY} google: base-url: ${GOOGLE_BASE_URL} - rest-api-key: ${GOOGLE_REST_API_KEY} + native-app-key: ${GOOGLE_NATIVE_APP_KEY} apple: base-url: ${APPLE_BASE_URL} - rest-api-key: ${APPLE_REST_API_KEY} + native-app-key: ${APPLE_NATIVE_APP_KEY} --- spring: config: From 188be9487e5eebcf18f945d936a0f147d0e3d51e Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Mon, 22 Dec 2025 01:10:27 +0900 Subject: [PATCH 33/35] =?UTF-8?q?#279=20[feat]=20oauth=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85,=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../photi/apis/enduser/controller/oauth/OAuthController.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthController.kt b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthController.kt index 6274ba51..6eef298b 100644 --- a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthController.kt +++ b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthController.kt @@ -1,10 +1,13 @@ package com.photi.apis.enduser.controller.oauth +import com.photi.apis.enduser.common.exception.GlobalApiErrorResponses import com.photi.apis.enduser.common.exception.UserApiErrorResponses import com.photi.apis.enduser.config.security.JwtTokenProvider import com.photi.apis.enduser.controller.auth.dto.response.LoginResponse import com.photi.apis.enduser.controller.auth.dto.response.SignUpResponse import com.photi.apis.enduser.controller.oauth.request.OAuthSignUpRequest +import com.photi.core.domain.common.exception.GlobalErrorCode.EXPIRED_TOKEN +import com.photi.core.domain.common.exception.GlobalErrorCode.INVALID_TOKEN import com.photi.core.domain.user.exception.UserErrorCode.* import com.photi.core.domain.user.model.OAuthProviderType import com.photi.core.domain.user.model.RoleType @@ -34,6 +37,7 @@ class OAuthController( @Operation(summary = "OAuth 회원가입") @ApiResponse(responseCode = "201") @UserApiErrorResponses([EXISTING_USER]) + @GlobalApiErrorResponses([INVALID_TOKEN, EXPIRED_TOKEN]) fun signUp( @PathVariable provider: OAuthProviderType, @RequestParam("id_token") @Parameter(description = "ID 토큰") idToken: String, @@ -49,6 +53,7 @@ class OAuthController( @Operation(summary = "OAuth 로그인") @ApiResponse(responseCode = "200") @UserApiErrorResponses([USER_NOT_FOUND, DELETED_USER]) + @GlobalApiErrorResponses([INVALID_TOKEN, EXPIRED_TOKEN]) fun login( @PathVariable provider: OAuthProviderType, @RequestParam("id_token") @Parameter(description = "ID 토큰") idToken: String, From 302baf4d9a001ee6f64973715feb862b4c20ca40 Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Mon, 22 Dec 2025 01:21:30 +0900 Subject: [PATCH 34/35] =?UTF-8?q?#279=20[refactor]=20oidc=20nonce=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apis/enduser/config/oidc/JwtOidcProvider.kt | 14 ++++---------- .../com/photi/core/domain/user/port/OAuthPort.kt | 2 +- .../photi/core/domain/user/service/OAuthService.kt | 7 +------ .../core/infra/oauth/adapter/AppleOAuthAdapter.kt | 9 ++------- .../core/infra/oauth/adapter/GoogleOAuthAdapter.kt | 9 ++------- .../core/infra/oauth/adapter/KakaoOAuthAdapter.kt | 9 ++------- .../com/photi/core/infra/oauth/port/JwtOidcPort.kt | 7 +------ 7 files changed, 13 insertions(+), 44 deletions(-) diff --git a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/oidc/JwtOidcProvider.kt b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/oidc/JwtOidcProvider.kt index 514ee60b..65db6d52 100644 --- a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/oidc/JwtOidcProvider.kt +++ b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/oidc/JwtOidcProvider.kt @@ -17,14 +17,9 @@ class JwtOidcProvider( private val objectMapper: ObjectMapper, ) : JwtOidcPort { - override fun getKidFromUnsignedIdToken( - idToken: String, - iss: String, - aud: String, - nonce: String, - ): String { + override fun getKidFromUnsignedIdToken(idToken: String, iss: String, aud: String): String { val splitIdToken = getSplitIdToken(idToken) - validatePayload(splitIdToken[1], iss, aud, nonce) + validatePayload(splitIdToken[1], iss, aud) val headerJson = String(Base64.getUrlDecoder().decode(splitIdToken[0])) val headerMap = objectMapper.readValue(headerJson, Map::class.java) return headerMap[KID].toString() @@ -53,10 +48,10 @@ class JwtOidcProvider( return splitToken } - private fun validatePayload(payload: String, iss: String, aud: String, nonce: String) { + private fun validatePayload(payload: String, iss: String, aud: String) { val payloadJson = String(Base64.getUrlDecoder().decode(payload)) val payloadMap = objectMapper.readValue(payloadJson, Map::class.java) - if (payloadMap[ISS] != iss || payloadMap[AUD] != aud || payloadMap[NONCE] != nonce) { + if (payloadMap[ISS] != iss || payloadMap[AUD] != aud) { throw GlobalException.InvalidTokenException() } val exp = (payloadMap[EXP] as? Number)?.toLong() @@ -95,7 +90,6 @@ class JwtOidcProvider( private const val EXP = "exp" private const val EMAIL = "email" private const val PICTURE = "picture" - private const val NONCE = "nonce" private const val ALGORITHM = "RSA" } } diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/port/OAuthPort.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/port/OAuthPort.kt index 4431a578..333f6998 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/port/OAuthPort.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/port/OAuthPort.kt @@ -6,7 +6,7 @@ import com.photi.core.domain.user.model.OAuthInfo interface OAuthPort { - fun getIdTokenPayload(idToken: String, iss: String, aud: String, nonce: String): OidcPayload + fun getIdTokenPayload(idToken: String, iss: String, aud: String): OidcPayload fun getProperties(): OAuthProperties diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt index c0d4f5ab..a4222c1b 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt @@ -50,11 +50,6 @@ class OAuthService( private fun getOidcPayload(idToken: String, oAuthPort: OAuthPort): OidcPayload { val properties = oAuthPort.getProperties() - return oAuthPort.getIdTokenPayload( - idToken, - properties.baseUrl, - properties.nativeAppKey, - "nonce", // todo '인가 코드 요청 api' 요청 시 전달한 nonce 값과 동일한 값 - ) + return oAuthPort.getIdTokenPayload(idToken, properties.baseUrl, properties.nativeAppKey) } } diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/AppleOAuthAdapter.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/AppleOAuthAdapter.kt index 27f0473d..0fe846ac 100644 --- a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/AppleOAuthAdapter.kt +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/AppleOAuthAdapter.kt @@ -15,14 +15,9 @@ class AppleOAuthAdapter( private val jwtOidcPort: JwtOidcPort, ) : OAuthPort { - override fun getIdTokenPayload( - idToken: String, - iss: String, - aud: String, - nonce: String, - ): OidcPayload { + override fun getIdTokenPayload(idToken: String, iss: String, aud: String): OidcPayload { val publicKeys = appleOAuthClient.getOidcPublicKeys() - val kid = jwtOidcPort.getKidFromUnsignedIdToken(idToken, iss, aud, nonce) + val kid = jwtOidcPort.getKidFromUnsignedIdToken(idToken, iss, aud) val jwk = publicKeys.keys.first { it.kid == kid } return jwtOidcPort.getIdTokenPayload(idToken, jwk.n, jwk.e) } diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/GoogleOAuthAdapter.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/GoogleOAuthAdapter.kt index ec27f5d1..c32e7072 100644 --- a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/GoogleOAuthAdapter.kt +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/GoogleOAuthAdapter.kt @@ -15,14 +15,9 @@ class GoogleOAuthAdapter( private val jwtOidcPort: JwtOidcPort, ) : OAuthPort { - override fun getIdTokenPayload( - idToken: String, - iss: String, - aud: String, - nonce: String, - ): OidcPayload { + override fun getIdTokenPayload(idToken: String, iss: String, aud: String): OidcPayload { val publicKeys = googleOAuthClient.getOidcPublicKeys() - val kid = jwtOidcPort.getKidFromUnsignedIdToken(idToken, iss, aud, nonce) + val kid = jwtOidcPort.getKidFromUnsignedIdToken(idToken, iss, aud) val jwk = publicKeys.keys.first { it.kid == kid } return jwtOidcPort.getIdTokenPayload(idToken, jwk.n, jwk.e) } diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/KakaoOAuthAdapter.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/KakaoOAuthAdapter.kt index c6972c22..ad9e5837 100644 --- a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/KakaoOAuthAdapter.kt +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/adapter/KakaoOAuthAdapter.kt @@ -15,14 +15,9 @@ class KakaoOAuthAdapter( private val jwtOidcPort: JwtOidcPort, ) : OAuthPort { - override fun getIdTokenPayload( - idToken: String, - iss: String, - aud: String, - nonce: String, - ): OidcPayload { + override fun getIdTokenPayload(idToken: String, iss: String, aud: String): OidcPayload { val publicKeys = kakaoOAuthClient.getOidcPublicKeys() - val kid = jwtOidcPort.getKidFromUnsignedIdToken(idToken, iss, aud, nonce) + val kid = jwtOidcPort.getKidFromUnsignedIdToken(idToken, iss, aud) val jwk = publicKeys.keys.first { it.kid == kid } return jwtOidcPort.getIdTokenPayload(idToken, jwk.n, jwk.e) } diff --git a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/port/JwtOidcPort.kt b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/port/JwtOidcPort.kt index 7c2fea6a..91f11589 100644 --- a/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/port/JwtOidcPort.kt +++ b/photi-core/infra/src/main/kotlin/com/photi/core/infra/oauth/port/JwtOidcPort.kt @@ -4,12 +4,7 @@ import com.photi.core.domain.user.dto.OidcPayload interface JwtOidcPort { - fun getKidFromUnsignedIdToken( - idToken: String, - iss: String, - aud: String, - nonce: String, - ): String + fun getKidFromUnsignedIdToken(idToken: String, iss: String, aud: String): String fun getIdTokenPayload(idToken: String, modulus: String, exponent: String): OidcPayload } From 1a7dc80c2f33988435a61a7f706e35e8c0ea75ec Mon Sep 17 00:00:00 2001 From: YuGyeong98 Date: Mon, 22 Dec 2025 01:43:09 +0900 Subject: [PATCH 35/35] =?UTF-8?q?#279=20[chore]=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../db/migration/V8__add_provider_sub_in_user.sql | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 photi-core/domain/src/main/resources/db/migration/V8__add_provider_sub_in_user.sql diff --git a/photi-core/domain/src/main/resources/db/migration/V8__add_provider_sub_in_user.sql b/photi-core/domain/src/main/resources/db/migration/V8__add_provider_sub_in_user.sql new file mode 100644 index 00000000..71554ab6 --- /dev/null +++ b/photi-core/domain/src/main/resources/db/migration/V8__add_provider_sub_in_user.sql @@ -0,0 +1,11 @@ +ALTER TABLE users + ADD COLUMN provider VARCHAR(15) NULL, + ADD COLUMN sub VARCHAR(255) NULL, + ALTER COLUMN authentication_code DROP NOT NULL; + +ALTER TABLE users + DROP CONSTRAINT IF EXISTS uq_email; + +ALTER TABLE users + ADD CONSTRAINT uq_user_provider_sub + UNIQUE (provider, sub); \ No newline at end of file