-
Notifications
You must be signed in to change notification settings - Fork 1
feat: #172 Apple 로그인 구현 #227
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
93ada66
151222a
c09842e
de038fe
1f8c967
bb57b7d
254433f
38cf547
0ee4797
43b57c2
2882fbe
d29fea2
1703ce7
01aebac
4b4937b
ea99690
c9133a6
848ecc9
5886099
f903972
11dd186
fa36216
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,8 @@ | ||
| package org.appjam.smashing.domain.auth.dto.command | ||
|
|
||
| import org.appjam.smashing.domain.auth.enums.ProviderType | ||
|
|
||
| data class SignInRequestCommand( | ||
| val accessToken: String, | ||
| val idToken: String, | ||
| val provider: ProviderType, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package org.appjam.smashing.domain.auth.dto.response | ||
|
|
||
| import org.appjam.smashing.domain.auth.enums.ProviderType | ||
|
|
||
| data class SocialType( | ||
| val provider: ProviderType, | ||
| val socialId: String, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package org.appjam.smashing.domain.auth.enums | ||
|
|
||
| enum class ProviderType { | ||
| KAKAO, APPLE | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,12 +42,12 @@ class AuthService( | |
| fun signIn( | ||
| requestCommand: SignInRequestCommand, | ||
| ): SignInResponse { | ||
| val kakaoId = socialAuthServiceManager.getKakaoId(requestCommand.accessToken) | ||
| val (provider, socialId) = socialAuthServiceManager.getSocialId(requestCommand) | ||
|
|
||
| val user = userRepository.findByKakaoId(kakaoId) | ||
| ?: return SignInResponse.from( | ||
| kakaoId = kakaoId, | ||
| ) | ||
| val user = userRepository.findBySocialIdAndProvider( | ||
| socialId = socialId, | ||
| provider = provider, | ||
| ) ?: return SignInResponse.from(socialId = socialId) | ||
|
|
||
| val userId = user.id ?: throw CustomException(ErrorCode.USER_NOT_FOUND) | ||
|
|
||
|
|
@@ -56,7 +56,7 @@ class AuthService( | |
| return SignInResponse.from( | ||
| accessToken = token.accessToken.token, | ||
| refreshToken = token.refreshToken.token, | ||
| kakaoId = kakaoId, | ||
| socialId = socialId, | ||
| userId = userId, | ||
| nickname = user.nickname, | ||
| ) | ||
|
|
@@ -81,7 +81,8 @@ class AuthService( | |
|
|
||
| val user = userRepository.save( | ||
| User.create( | ||
| kakaoId = requestCommand.kakaoId, | ||
| socialId = requestCommand.socialId, | ||
| provider = requestCommand.provider, | ||
| nickname = requestCommand.nickname, | ||
| gender = requestCommand.gender, | ||
| openchatUrl = requestCommand.openChatUrl.trim(), | ||
|
|
@@ -173,8 +174,8 @@ class AuthService( | |
| } | ||
|
|
||
| private fun validateUser(requestCommand: SignUpRequestCommand) { | ||
| if (userRepository.existsByKakaoId(requestCommand.kakaoId)) { | ||
| throw CustomException(ErrorCode.DUPLICATE_KAKAO_ID) | ||
| if (userRepository.existsBySocialId(requestCommand.socialId)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 로그인 조회는 (socialId, provider) 조합으로 하고 있는데, 가입 시 중복 체크는 existsBySocialId()만 사용하고 있어서 기준이 조금 달라질 수 있을 것같아요.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 좋은 의견 감사합니다 :) |
||
| throw CustomException(ErrorCode.DUPLICATE_SOCIAL_ID) | ||
| } | ||
|
Comment on lines
176
to
179
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 중복 체크는
예시 수정- if (userRepository.existsBySocialId(requestCommand.socialId)) {
+ if (userRepository.existsBySocialIdAndProvider(
+ socialId = requestCommand.socialId,
+ provider = requestCommand.provider,
+ )) {
throw CustomException(ErrorCode.DUPLICATE_SOCIAL_ID)
}🤖 Prompt for AI Agents |
||
|
|
||
| if (userRepository.existsByNickname(requestCommand.nickname)) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package org.appjam.smashing.domain.auth.social | ||
|
|
||
| import com.fasterxml.jackson.databind.JsonNode | ||
| import com.github.benmanes.caffeine.cache.Cache | ||
| import com.github.benmanes.caffeine.cache.Caffeine | ||
| import org.appjam.smashing.global.exception.CustomException | ||
| import org.appjam.smashing.global.exception.ErrorCode | ||
| import org.springframework.stereotype.Component | ||
| import org.springframework.web.client.RestTemplate | ||
| import java.util.concurrent.TimeUnit | ||
|
|
||
| @Component | ||
| class OidcJwksClient( | ||
| private val restTemplate: RestTemplate, | ||
| ) { | ||
| private val cache: Cache<String, JsonNode> = Caffeine.newBuilder() | ||
| .expireAfterWrite(1, TimeUnit.HOURS) | ||
| .maximumSize(10) | ||
| .build() | ||
|
|
||
| fun getKeys( | ||
| jwksUri: String, | ||
| ): JsonNode = cache.get(jwksUri) { | ||
| restTemplate.getForObject(jwksUri, JsonNode::class.java) | ||
| ?: throw CustomException(ErrorCode.INVALID_ID_TOKEN) | ||
| } ?: throw CustomException(ErrorCode.INVALID_ID_TOKEN) | ||
|
Comment on lines
+21
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. JWKS 키 롤오버 및 네트워크 오류 처리 보완 필요. 두 가지 우려가 있습니다.
또한 Line 26의 바깥쪽 🛠️ 제안: 강제 갱신 메서드 추가 + 네트워크 예외 분리 `@Component`
class OidcJwksClient(
private val restTemplate: RestTemplate,
) {
private val cache: Cache<String, JsonNode> = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.maximumSize(10)
.build()
fun getKeys(
jwksUri: String,
): JsonNode = cache.get(jwksUri) {
- restTemplate.getForObject(jwksUri, JsonNode::class.java)
- ?: throw CustomException(ErrorCode.INVALID_ID_TOKEN)
+ fetch(jwksUri)
} ?: throw CustomException(ErrorCode.INVALID_ID_TOKEN)
+
+ fun refresh(jwksUri: String): JsonNode {
+ cache.invalidate(jwksUri)
+ return getKeys(jwksUri)
+ }
+
+ private fun fetch(jwksUri: String): JsonNode = try {
+ restTemplate.getForObject(jwksUri, JsonNode::class.java)
+ ?: throw CustomException(ErrorCode.INVALID_ID_TOKEN)
+ } catch (e: RestClientException) {
+ throw CustomException(ErrorCode.JWKS_FETCH_FAILED) // 신규 에러코드 권장
+ }
}그리고 🤖 Prompt for AI Agents |
||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package org.appjam.smashing.domain.auth.social | ||
|
|
||
| import org.appjam.smashing.domain.auth.enums.ProviderType | ||
| import org.springframework.boot.context.properties.ConfigurationProperties | ||
|
|
||
| @ConfigurationProperties(prefix = "oidc") | ||
| data class OidcProperties( | ||
| val kakaoClientId: String, | ||
| val appleClientId: String, | ||
| ) { | ||
| fun getClientId(providerType: ProviderType): String = when (providerType) { | ||
| ProviderType.KAKAO -> kakaoClientId | ||
| ProviderType.APPLE -> appleClientId | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| package org.appjam.smashing.domain.auth.social | ||
|
|
||
| import com.fasterxml.jackson.databind.ObjectMapper | ||
| import io.jsonwebtoken.Jwts | ||
| import org.appjam.smashing.domain.auth.enums.ProviderType | ||
| import org.appjam.smashing.global.exception.CustomException | ||
| import org.appjam.smashing.global.exception.ErrorCode | ||
| import org.springframework.stereotype.Component | ||
| import java.math.BigInteger | ||
| import java.security.KeyFactory | ||
| import java.security.PublicKey | ||
| import java.security.spec.RSAPublicKeySpec | ||
| import java.util.* | ||
|
|
||
| @Component | ||
| class OidcTokenValidator( | ||
| private val oidcProperties: OidcProperties, | ||
| private val jwksClient: OidcJwksClient, | ||
| ) { | ||
| fun extractSocialId( | ||
| idToken: String, | ||
| providerType: ProviderType, | ||
| ): String { | ||
| // 회웑 정보 가져오기 | ||
| val (iss, jwksUri) = when (providerType) { | ||
| ProviderType.KAKAO -> KAKAO_ISS to KAKAO_JWKS_URI | ||
| ProviderType.APPLE -> APPLE_ISS to APPLE_JWKS_URI | ||
| } | ||
| val clientId = oidcProperties.getClientId(providerType) | ||
|
|
||
| val publicKey = getPublicKey( | ||
| idToken = idToken, | ||
| jwksUri = jwksUri, | ||
| ) | ||
|
|
||
| val claims = Jwts.parserBuilder() | ||
| .setSigningKey(publicKey) | ||
| .build() | ||
| .parseClaimsJws(idToken) | ||
| .body | ||
|
|
||
| // 회원 검증 | ||
| if (claims.issuer != iss) throw CustomException(ErrorCode.INVALID_ISS) | ||
| if (claims.audience != clientId) throw CustomException(ErrorCode.INVALID_AUD) | ||
|
|
||
| return claims.subject | ||
| } | ||
|
|
||
| private fun getPublicKey( | ||
| idToken: String, | ||
| jwksUri: String, | ||
| ): PublicKey { | ||
| val header = String(Base64.getUrlDecoder().decode(idToken.split(".")[0])) | ||
| val kid = ObjectMapper().readTree(header).get(KID).asText() | ||
|
|
||
| val keys = jwksClient.getKeys(jwksUri) | ||
|
|
||
| val key = keys[KEYS].find { it[KID].asText() == kid } | ||
| ?: throw CustomException(ErrorCode.INVALID_ID_TOKEN) | ||
|
|
||
| val n = BigInteger(1, Base64.getUrlDecoder().decode(key[N].asText())) | ||
| val e = BigInteger(1, Base64.getUrlDecoder().decode(key[E].asText())) | ||
|
|
||
| return KeyFactory.getInstance(RSA) | ||
| .generatePublic(RSAPublicKeySpec(n, e)) | ||
| } | ||
|
|
||
| companion object { | ||
| private const val KID = "kid" | ||
| private const val KEYS = "keys" | ||
| private const val N = "n" | ||
| private const val E = "e" | ||
| private const val RSA = "RSA" | ||
| private const val KAKAO_JWKS_URI = "https://kauth.kakao.com/.well-known/jwks.json" | ||
| private const val KAKAO_ISS = "https://kauth.kakao.com" | ||
| private const val APPLE_JWKS_URI = "https://appleid.apple.com/auth/keys" | ||
| private const val APPLE_ISS = "https://appleid.apple.com" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,22 @@ | ||
| package org.appjam.smashing.domain.auth.social | ||
|
|
||
| import org.appjam.smashing.domain.auth.social.kakao.KakaoAuthTokenValidator | ||
| import org.appjam.smashing.domain.auth.dto.command.SignInRequestCommand | ||
| import org.appjam.smashing.domain.auth.dto.response.SocialType | ||
| import org.springframework.stereotype.Component | ||
|
|
||
| @Component | ||
| class SocialAuthServiceManager( | ||
| private val kakaoAuthTokenValidator: KakaoAuthTokenValidator, | ||
| private val oidcTokenValidator: OidcTokenValidator, | ||
| ) { | ||
| fun getKakaoId(authAccessToken: String): String = kakaoAuthTokenValidator.extractKakaoId(authAccessToken) | ||
| fun getSocialId(command: SignInRequestCommand): SocialType { | ||
| val socialId = oidcTokenValidator.extractSocialId( | ||
| idToken = command.idToken, | ||
| providerType = command.provider | ||
| ) | ||
|
|
||
| return SocialType( | ||
| provider = command.provider, | ||
| socialId = socialId, | ||
| ) | ||
| } | ||
| } |
This file was deleted.
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
provider는 현재 enum validation만 하고 toCommand()에서 바로 provider!!로 사용하고 있어서, null/blank가 validation에서 걸러지지 않으면 500으로 떨어질 수 있을 것 같아요.
@notblank까지 같이 두어서 요청 검증 단계에서 400으로 막아주면 더 안전할 수 있을 것같습니다!
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
예리하십니다.. 추가했습니다! f903972