애플 로그인 검증 로직 추가
This commit is contained in:
@@ -8,6 +8,7 @@ interface SocialAuthService {
|
||||
token: String,
|
||||
container: String,
|
||||
marketingPid: String?,
|
||||
pushToken: String?
|
||||
pushToken: String?,
|
||||
nonce: String?
|
||||
): SocialLoginResponse
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package kr.co.vividnext.sodalive.member.social.apple
|
||||
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.jwt.TokenProvider
|
||||
import kr.co.vividnext.sodalive.member.MemberAdapter
|
||||
import kr.co.vividnext.sodalive.member.MemberProvider
|
||||
import kr.co.vividnext.sodalive.member.MemberService
|
||||
import kr.co.vividnext.sodalive.member.login.LoginResponse
|
||||
import kr.co.vividnext.sodalive.member.social.SocialAuthService
|
||||
import kr.co.vividnext.sodalive.member.social.SocialLoginResponse
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class AppleAuthService(
|
||||
private val appleIdentityTokenVerifier: AppleIdentityTokenVerifier,
|
||||
private val memberService: MemberService,
|
||||
private val tokenProvider: TokenProvider,
|
||||
|
||||
@Value("\${cloud.aws.cloud-front.host}")
|
||||
private val cloudFrontHost: String
|
||||
) : SocialAuthService {
|
||||
override fun getProvider(): MemberProvider = MemberProvider.APPLE
|
||||
|
||||
override fun authenticate(
|
||||
token: String,
|
||||
container: String,
|
||||
marketingPid: String?,
|
||||
pushToken: String?,
|
||||
nonce: String?
|
||||
): SocialLoginResponse {
|
||||
val rawNonce = nonce?.takeIf { it.isNotBlank() }
|
||||
?: throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||
|
||||
val appleUserInfo = appleIdentityTokenVerifier.verify(token, rawNonce)
|
||||
val memberResolveResult = memberService.findOrRegister(appleUserInfo, container, marketingPid, pushToken)
|
||||
val member = memberResolveResult.member
|
||||
val principal = MemberAdapter(member)
|
||||
val authToken = AppleAuthenticationToken(token, principal.authorities)
|
||||
authToken.setPrincipal(principal)
|
||||
SecurityContextHolder.getContext().authentication = authToken
|
||||
|
||||
val jwt = tokenProvider.createToken(
|
||||
authentication = authToken,
|
||||
memberId = member.id!!
|
||||
)
|
||||
|
||||
val loginResponse = LoginResponse(
|
||||
userId = member.id!!,
|
||||
token = jwt,
|
||||
nickname = member.nickname,
|
||||
email = member.email ?: "",
|
||||
profileImage = if (member.profileImage != null) {
|
||||
"$cloudFrontHost/${member.profileImage}"
|
||||
} else {
|
||||
"$cloudFrontHost/profile/default-profile.png"
|
||||
}
|
||||
)
|
||||
|
||||
return SocialLoginResponse(
|
||||
memberId = member.id!!,
|
||||
marketingPid = marketingPid,
|
||||
loginResponse = loginResponse,
|
||||
isNew = memberResolveResult.isNew
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package kr.co.vividnext.sodalive.member.social.apple
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
|
||||
class AppleAuthenticationToken(
|
||||
private val idToken: String,
|
||||
authorities: Collection<GrantedAuthority>? = null
|
||||
) : AbstractAuthenticationToken(authorities) {
|
||||
private var principal: Any? = null
|
||||
|
||||
init {
|
||||
isAuthenticated = authorities != null
|
||||
}
|
||||
|
||||
override fun getCredentials(): Any = idToken
|
||||
|
||||
override fun getPrincipal(): Any? = principal
|
||||
|
||||
fun setPrincipal(principal: Any) {
|
||||
this.principal = principal
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package kr.co.vividnext.sodalive.member.social.apple
|
||||
|
||||
import com.nimbusds.jose.JWSAlgorithm
|
||||
import com.nimbusds.jose.jwk.source.JWKSource
|
||||
import com.nimbusds.jose.jwk.source.JWKSourceBuilder
|
||||
import com.nimbusds.jose.proc.JWSVerificationKeySelector
|
||||
import com.nimbusds.jose.proc.SecurityContext
|
||||
import com.nimbusds.jwt.JWTClaimsSet
|
||||
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor
|
||||
import com.nimbusds.jwt.proc.DefaultJWTProcessor
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Service
|
||||
import java.net.URL
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.MessageDigest
|
||||
import java.util.Base64
|
||||
import java.util.Date
|
||||
|
||||
@Service
|
||||
class AppleIdentityTokenVerifier(
|
||||
@Value("\${apple.bundle-id}")
|
||||
private val bundleId: String
|
||||
) {
|
||||
private val jwkUrl = URL("https://appleid.apple.com/auth/keys")
|
||||
private val jwkSource: JWKSource<SecurityContext> = JWKSourceBuilder.create<SecurityContext>(jwkUrl)
|
||||
.build()
|
||||
|
||||
private val jwtProcessor: ConfigurableJWTProcessor<SecurityContext> =
|
||||
DefaultJWTProcessor<SecurityContext>().apply {
|
||||
jwsKeySelector = JWSVerificationKeySelector(JWSAlgorithm.RS256, jwkSource)
|
||||
}
|
||||
|
||||
fun verify(identityToken: String, rawNonce: String): AppleUserInfo {
|
||||
if (bundleId.isBlank()) {
|
||||
throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||
}
|
||||
|
||||
if (rawNonce.isBlank()) {
|
||||
throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||
}
|
||||
|
||||
val claims = try {
|
||||
jwtProcessor.process(identityToken, null)
|
||||
} catch (_: Exception) {
|
||||
throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||
}
|
||||
|
||||
validateClaims(claims, rawNonce)
|
||||
|
||||
return AppleUserInfo(
|
||||
sub = claims.subject ?: throw SodaException(messageKey = "member.social.apple_login_failed"),
|
||||
email = claims.getStringClaim("email")
|
||||
)
|
||||
}
|
||||
|
||||
private fun validateClaims(claims: JWTClaimsSet, rawNonce: String) {
|
||||
if (claims.issuer != ISSUER) {
|
||||
throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||
}
|
||||
|
||||
if (!claims.audience.contains(bundleId)) {
|
||||
throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||
}
|
||||
|
||||
val now = Date()
|
||||
val expirationTime = claims.expirationTime ?: throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||
if (expirationTime.before(now)) {
|
||||
throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||
}
|
||||
|
||||
val issuedAt = claims.issueTime ?: throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||
if (issuedAt.after(now)) {
|
||||
throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||
}
|
||||
|
||||
val nonce = claims.getStringClaim("nonce") ?: throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||
val expectedNonce = hashNonce(rawNonce)
|
||||
if (nonce != expectedNonce) {
|
||||
throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||
}
|
||||
}
|
||||
|
||||
private fun hashNonce(rawNonce: String): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val hashed = digest.digest(rawNonce.toByteArray(StandardCharsets.UTF_8))
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(hashed)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ISSUER = "https://appleid.apple.com"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package kr.co.vividnext.sodalive.member.social.apple
|
||||
|
||||
data class AppleUserInfo(
|
||||
val sub: String,
|
||||
val email: String?
|
||||
)
|
||||
@@ -27,7 +27,8 @@ class GoogleAuthService(
|
||||
token: String,
|
||||
container: String,
|
||||
marketingPid: String?,
|
||||
pushToken: String?
|
||||
pushToken: String?,
|
||||
nonce: String?
|
||||
): SocialLoginResponse {
|
||||
val googleUserInfo = googleService.getUserInfo(token)
|
||||
?: throw SodaException(messageKey = "member.social.google_login_failed")
|
||||
|
||||
@@ -27,7 +27,8 @@ class KakaoAuthService(
|
||||
token: String,
|
||||
container: String,
|
||||
marketingPid: String?,
|
||||
pushToken: String?
|
||||
pushToken: String?,
|
||||
nonce: String?
|
||||
): SocialLoginResponse {
|
||||
val kakaoUserInfo = kakaoService.getUserInfo(token)
|
||||
?: throw SodaException(messageKey = "member.social.kakao_login_failed")
|
||||
|
||||
Reference in New Issue
Block a user