Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "cd \"$CLAUDE_PROJECT_DIR\" && ./gradlew spotlessApply",
"timeout": 120
},
{
"type": "command",
"command": "cd \"$CLAUDE_PROJECT_DIR\" && ./gradlew test",
"timeout": 300
}
]
}
]
}
}
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ val kotestVersion = "5.9.1"
val kotestExtensionsVersion = "1.3.0"
val mockkVersion = "1.13.10"
val ktlintVersion = "1.5.0"
val archunitVersion = "1.3.0"

group = "com.neki"
version = "1.0.0"
Expand Down Expand Up @@ -111,6 +112,7 @@ dependencies {
testImplementation("io.mockk:mockk:$mockkVersion")
testRuntimeOnly("com.h2database:h2")
testImplementation("io.rest-assured:rest-assured")
testImplementation("com.tngtech.archunit:archunit-junit5:$archunitVersion")
}

spotless {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import com.neki.user.domain.enums.ProviderType
/**
* 카카오 사용자정보 추출 DTO
*/
data class OauthInfoResponse(
data class OauthInfoPayload(
val providerType: ProviderType,
val oid: String,
val email: String?,
val name: String?,
val imageUrl: String?,
)

data class OIDCDecodePayloadResponse(
data class OIDCDecodePayload(
/** issuer ex https://kauth.kakao.com */
val iss: String,
/** client id */
Expand All @@ -28,7 +28,7 @@ data class OIDCDecodePayloadResponse(
val imageUrl: String?,
)

data class OIDCPublicKeysResponse(val keys: MutableList<OIDCPublicKeyDto>)
data class OIDCPublicKeysPayload(val keys: MutableList<OIDCPublicKeyDto>)

data class OIDCPublicKeyDto(val kid: String, val alg: String, val use: String, val n: String, val e: String)

Expand All @@ -38,7 +38,7 @@ data class OIDCPublicKeyDto(val kid: String, val alg: String, val use: String, v
* date : 2025. 12. 26. 18:20
* description : Auth usercase 관련 result idToken을 얻기 위한 테스트 DTO
*/
data class GetKakaoTokenResponse(
data class KakaoTokenPayload(
val accessToken: String,
val tokenType: String,
val refreshToken: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.neki.auth.application.port

import com.neki.auth.application.contract.OIDCPublicKeysResponse
import com.neki.auth.application.contract.OIDCPublicKeysPayload
import java.time.Duration

/**
Expand All @@ -10,9 +10,9 @@ import java.time.Duration
* description : 캐시사용을 위한 Port 인터페이스
*/
interface AuthCachePort {
fun setPublicKeys(key: String, value: OIDCPublicKeysResponse, ttl: Duration)
fun setPublicKeys(key: String, value: OIDCPublicKeysPayload, ttl: Duration)

fun getPublicKeys(key: String): OIDCPublicKeysResponse?
fun getPublicKeys(key: String): OIDCPublicKeysPayload?

fun clearPublicKeys(key: String)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.neki.auth.application.port

import com.neki.auth.application.contract.OauthInfoResponse
import com.neki.auth.application.contract.OauthInfoPayload
import com.neki.auth.domain.Platform
import com.neki.user.domain.enums.ProviderType

Expand All @@ -11,5 +11,5 @@ import com.neki.user.domain.enums.ProviderType
* description :
*/
interface OidcTokenValidatorPort {
fun validateIdToken(idToken: String, providerType: ProviderType, platform: Platform): OauthInfoResponse
fun validateIdToken(idToken: String, providerType: ProviderType, platform: Platform): OauthInfoPayload
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package com.neki.auth.application.usecase

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.neki.auth.application.command.RegisterOauthUserCommand
import com.neki.auth.application.contract.GetKakaoTokenResponse
import com.neki.auth.application.contract.OauthInfoResponse
import com.neki.auth.application.contract.KakaoTokenPayload
import com.neki.auth.application.contract.OauthInfoPayload
import com.neki.auth.application.port.AuthTokenProviderPort
import com.neki.auth.application.port.NicknameGeneratorPort
import com.neki.auth.application.port.OidcTokenValidatorPort
Expand Down Expand Up @@ -45,14 +45,14 @@ class OauthLoginUseCase(
* 4. oauthInfoResult 값 여부에 따라 회원가입 처리
*/
fun execute(command: RegisterOauthUserCommand): GetAuthResult {
val oauthInfoResponse = oidcTokenValidatorPort.validateIdToken(
val oauthInfoPayload = oidcTokenValidatorPort.validateIdToken(
command.idToken,
command.providerType,
command.platform,
)

// 신규 사용자 추가
val (user, _) = transactionRunner.run { registerOauthUserIfEmpty(oauthInfoResponse) }
val (user, _) = transactionRunner.run { registerOauthUserIfEmpty(oauthInfoPayload) }

// 토큰 생성
val accessToken = tokenProviderPort.createAccessToken(
Expand All @@ -75,10 +75,10 @@ class OauthLoginUseCase(
)
}

private fun registerOauthUserIfEmpty(oauthInfoResponse: OauthInfoResponse): Pair<User, Boolean> {
private fun registerOauthUserIfEmpty(oauthInfoPayload: OauthInfoPayload): Pair<User, Boolean> {
val existingUser = userRepositoryPort.findByOid(
oid = oauthInfoResponse.oid,
provider = oauthInfoResponse.providerType,
oid = oauthInfoPayload.oid,
provider = oauthInfoPayload.providerType,
)

return if (existingUser != null) {
Expand All @@ -87,11 +87,11 @@ class OauthLoginUseCase(
val nickname = nicknameGenerator.generateUniqueNickname()
val newUser = userRepositoryPort.save(
User(
email = oauthInfoResponse.email,
oid = oauthInfoResponse.oid,
email = oauthInfoPayload.email,
oid = oauthInfoPayload.oid,
name = nickname,
roles = RoleType.USER.role,
providerType = oauthInfoResponse.providerType,
providerType = oauthInfoPayload.providerType,
profileImageId = null,
),
)
Expand All @@ -108,7 +108,7 @@ class OauthLoginUseCase(
* @return KakaoTokenResponse 카카오 토큰 정보
* @throws Exception 토큰 획득 실패 시
*/
fun getAccessTokenByCode(code: String): GetKakaoTokenResponse {
fun getAccessTokenByCode(code: String): KakaoTokenPayload {
val clientId = oauthProperties.kakao.androidClientId
val clientSecret = oauthProperties.kakao.clientSecret

Expand All @@ -133,7 +133,7 @@ class OauthLoginUseCase(
val objectMapper = jacksonObjectMapper()
val jsonNode = objectMapper.readTree(response)

return GetKakaoTokenResponse(
return KakaoTokenPayload(
accessToken = jsonNode.get("access_token").asText(),
tokenType = jsonNode.get("token_type").asText(),
refreshToken = jsonNode.get("refresh_token").asText(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.neki.auth.infra.oauth

import com.neki.auth.application.contract.AuthCacheKeys
import com.neki.auth.application.contract.OauthInfoResponse
import com.neki.auth.application.contract.OauthInfoPayload
import com.neki.auth.application.port.OidcTokenValidatorPort
import com.neki.auth.domain.Platform
import com.neki.auth.infra.oauth.helper.OauthHelper
Expand Down Expand Up @@ -34,7 +34,7 @@ class OidcTokenValidator(
* - 1차 시도: 캐시된 공개키로 토큰 검증
* - BusinessException 발생 시: 캐시 무효화 후 재시도 (공개키 로테이션 대응)
*/
override fun validateIdToken(idToken: String, providerType: ProviderType, platform: Platform): OauthInfoResponse {
override fun validateIdToken(idToken: String, providerType: ProviderType, platform: Platform): OauthInfoPayload {
val oidcAdapter = oidcRegistry.getAdapter(providerType)
val oauthHelperAdapter = oauthHelperRegistry.getAdapter(providerType)

Expand All @@ -60,7 +60,7 @@ class OidcTokenValidator(
oidc: Oidc,
oauthHelper: OauthHelper,
platform: Platform,
): OauthInfoResponse {
): OauthInfoPayload {
val publicKeys = oidc.getOIDCPublicKey()
return oauthHelper.getOauthInfoByIdToken(
idToken = idToken,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.neki.auth.infra.oauth.helper

import com.fasterxml.jackson.databind.ObjectMapper
import com.neki.auth.application.contract.OIDCDecodePayloadResponse
import com.neki.auth.application.contract.OIDCDecodePayload
import com.neki.auth.application.contract.OIDCPublicKeyDto
import com.neki.auth.application.contract.OIDCPublicKeysResponse
import com.neki.auth.application.contract.OauthInfoResponse
import com.neki.auth.application.contract.OIDCPublicKeysPayload
import com.neki.auth.application.contract.OauthInfoPayload
import com.neki.auth.domain.Platform
import com.neki.auth.infra.security.properties.OauthProperties
import com.neki.common.api.dto.ResultCode
Expand Down Expand Up @@ -61,9 +61,9 @@ class AppleOauthHelper(private val oauthProperties: OauthProperties, private val
*/
override fun getOauthInfoByIdToken(
idToken: String,
publicKeys: OIDCPublicKeysResponse,
publicKeys: OIDCPublicKeysPayload,
platform: Platform,
): OauthInfoResponse {
): OauthInfoPayload {
// Step 1: 헤더에서 kid 추출
val kid = extractKidFromTokenHeader(idToken)

Expand All @@ -79,7 +79,7 @@ class AppleOauthHelper(private val oauthProperties: OauthProperties, private val
expectedAudience = oauthProperties.apple.clientId,
)

return OauthInfoResponse(
return OauthInfoPayload(
providerType = ProviderType.APPLE,
oid = payload.sub, // String 그대로 사용 (UUID)
email = payload.email,
Expand Down Expand Up @@ -122,11 +122,11 @@ class AppleOauthHelper(private val oauthProperties: OauthProperties, private val
publicKey: OIDCPublicKeyDto,
expectedIssuer: String,
expectedAudience: String,
): OIDCDecodePayloadResponse {
): OIDCDecodePayload {
val rsaPublicKey = convertToRSAPublicKey(publicKey.n, publicKey.e)
val claims = verifyTokenSignatureAndClaims(token, rsaPublicKey, expectedIssuer, expectedAudience)

return OIDCDecodePayloadResponse(
return OIDCDecodePayload(
iss = claims.issuer,
aud = claims.audience.toString(),
sub = claims.subject, // String 그대로 사용 (Apple UUID)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.neki.auth.infra.oauth.helper

import com.fasterxml.jackson.databind.ObjectMapper
import com.neki.auth.application.contract.OIDCDecodePayloadResponse
import com.neki.auth.application.contract.OIDCDecodePayload
import com.neki.auth.application.contract.OIDCPublicKeyDto
import com.neki.auth.application.contract.OIDCPublicKeysResponse
import com.neki.auth.application.contract.OauthInfoResponse
import com.neki.auth.application.contract.OIDCPublicKeysPayload
import com.neki.auth.application.contract.OauthInfoPayload
import com.neki.auth.domain.Platform
import com.neki.auth.infra.security.properties.OauthProperties
import com.neki.common.api.dto.ResultCode
Expand Down Expand Up @@ -59,9 +59,9 @@ class KakaoOauthHelper(private val oauthProperties: OauthProperties, private val
*/
override fun getOauthInfoByIdToken(
idToken: String,
publicKeys: OIDCPublicKeysResponse,
publicKeys: OIDCPublicKeysPayload,
platform: Platform,
): OauthInfoResponse {
): OauthInfoPayload {
// Step 1: 헤더에서 kid 추출 (토큰 분리 및 Base64 디코딩)
val kid = extractKidFromTokenHeader(idToken)

Expand All @@ -82,7 +82,7 @@ class KakaoOauthHelper(private val oauthProperties: OauthProperties, private val
expectedAudience = expectedAudience,
)

return OauthInfoResponse(
return OauthInfoPayload(
providerType = ProviderType.KAKAO,
oid = payload.sub,
email = payload.email,
Expand Down Expand Up @@ -133,11 +133,11 @@ class KakaoOauthHelper(private val oauthProperties: OauthProperties, private val
publicKey: OIDCPublicKeyDto,
expectedIssuer: String,
expectedAudience: String,
): OIDCDecodePayloadResponse {
): OIDCDecodePayload {
val rsaPublicKey = convertToRSAPublicKey(publicKey.n, publicKey.e)
val claims = verifyTokenSignatureAndClaims(token, rsaPublicKey, expectedIssuer, expectedAudience)

return OIDCDecodePayloadResponse(
return OIDCDecodePayload(
iss = claims.issuer,
aud = claims.audience.toString(),
sub = claims.subject,
Expand Down
10 changes: 3 additions & 7 deletions src/main/kotlin/com/neki/auth/infra/oauth/helper/OauthHelper.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.neki.auth.infra.oauth.helper

import com.neki.auth.application.contract.OIDCPublicKeysResponse
import com.neki.auth.application.contract.OauthInfoResponse
import com.neki.auth.application.contract.OIDCPublicKeysPayload
import com.neki.auth.application.contract.OauthInfoPayload
import com.neki.auth.domain.Platform

/**
Expand All @@ -11,9 +11,5 @@ import com.neki.auth.domain.Platform
* description : OAuth OIDC 검증을 위한 Port
*/
interface OauthHelper {
fun getOauthInfoByIdToken(
idToken: String,
publicKeys: OIDCPublicKeysResponse,
platform: Platform,
): OauthInfoResponse
fun getOauthInfoByIdToken(idToken: String, publicKeys: OIDCPublicKeysPayload, platform: Platform): OauthInfoPayload
}
6 changes: 3 additions & 3 deletions src/main/kotlin/com/neki/auth/infra/oauth/oidc/AppleOidc.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.neki.auth.infra.oauth.oidc

import com.neki.auth.application.contract.OIDCPublicKeysResponse
import com.neki.auth.application.contract.OIDCPublicKeysPayload
import com.neki.auth.infra.oauth.oidc.Oidc
import com.neki.auth.infra.security.properties.OauthProperties
import org.springframework.stereotype.Component
Expand All @@ -14,8 +14,8 @@ import org.springframework.web.client.RestClient
*/
@Component
class AppleOidc(private val restClient: RestClient, private val oauthProperties: OauthProperties) : Oidc {
override fun getOIDCPublicKey(): OIDCPublicKeysResponse = restClient.get()
override fun getOIDCPublicKey(): OIDCPublicKeysPayload = restClient.get()
.uri(oauthProperties.apple.jwksUri)
.retrieve()
.body(OIDCPublicKeysResponse::class.java)!!
.body(OIDCPublicKeysPayload::class.java)!!
}
6 changes: 3 additions & 3 deletions src/main/kotlin/com/neki/auth/infra/oauth/oidc/KakaoOidc.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.neki.auth.infra.oauth.oidc

import com.neki.auth.application.contract.AuthCacheKeys
import com.neki.auth.application.contract.OIDCPublicKeysResponse
import com.neki.auth.application.contract.OIDCPublicKeysPayload
import com.neki.auth.infra.oauth.oidc.Oidc
import com.neki.auth.infra.redis.AuthRedisCacheAdapter
import com.neki.auth.infra.security.properties.OauthProperties
Expand Down Expand Up @@ -36,7 +36,7 @@ class KakaoOidc(
* - TTL: 6시간
* - 캐시 미스 시 카카오 API 호출 후 캐싱
*/
override fun getOIDCPublicKey(): OIDCPublicKeysResponse {
override fun getOIDCPublicKey(): OIDCPublicKeysPayload {
// 1. 캐시 조회
val cached = authRedisCacheAdapter.getPublicKeys(AuthCacheKeys.KAKAO_OIDC_KEY)

Expand All @@ -49,7 +49,7 @@ class KakaoOidc(
val publicKeys = restClient.get()
.uri(oauthProperties.kakao.jwksUri)
.retrieve()
.body(OIDCPublicKeysResponse::class.java)!!
.body(OIDCPublicKeysPayload::class.java)!!

// 3. 캐시 저장 (TTL 14일)
authRedisCacheAdapter.setPublicKeys(AuthCacheKeys.KAKAO_OIDC_KEY, publicKeys, Duration.ofDays(14))
Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/com/neki/auth/infra/oauth/oidc/Oidc.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.neki.auth.infra.oauth.oidc

import com.neki.auth.application.contract.OIDCPublicKeysResponse
import com.neki.auth.application.contract.OIDCPublicKeysPayload

/**
* fileName : Oidc
Expand All @@ -12,5 +12,5 @@ interface Oidc {
/**
* 카카오 OIDC 공개키 조회
*/
fun getOIDCPublicKey(): OIDCPublicKeysResponse
fun getOIDCPublicKey(): OIDCPublicKeysPayload
}
Loading