Skip to content

Commit

Permalink
feat: Apple Login process
Browse files Browse the repository at this point in the history
  • Loading branch information
210-reverof committed Aug 29, 2024
1 parent 141f87b commit 3dd5e35
Show file tree
Hide file tree
Showing 15 changed files with 95 additions and 126 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.mashup.pic.auth.applicationService

import com.mashup.pic.auth.applicationService.dto.AppleLoginServiceRequest
import com.mashup.pic.auth.applicationService.dto.LoginServiceRequest
import com.mashup.pic.auth.applicationService.dto.ReissueServiceRequest
import com.mashup.pic.auth.controller.dto.LoginResponse
import com.mashup.pic.auth.controller.dto.ReissueResponse
import com.mashup.pic.domain.auth.RefreshTokenService
import com.mashup.pic.domain.user.LoginProvider
import com.mashup.pic.domain.user.UserDto
import com.mashup.pic.domain.user.UserService
import com.mashup.pic.security.authentication.UserInfo
Expand All @@ -26,13 +26,17 @@ class AuthApplicationService(
) {
@Transactional
fun login(request: LoginServiceRequest): LoginResponse {
val oAuthId =
if (request.provider == LoginProvider.KAKAO) {
kakaoIdTokenValidator.validateAndGetId(request.idToken, request.nickname)
} else {
appleIdTokenValidator.validateAndGetId(request.idToken, request.nickname)
}
val oAuthId = kakaoIdTokenValidator.validateAndGetId(request.idToken, request.nickname)
val user = userService.findUserByOAuthIdOrNull(oAuthId) ?: createUser(oAuthId, request)

val authToken = jwtManager.generateAuthToken(user.toUserInfo())
refreshTokenService.saveToken(user.id, authToken.refreshToken)
return LoginResponse.from(user, authToken)
}

@Transactional
fun appleLogin(request: AppleLoginServiceRequest): LoginResponse? {
val oAuthId = appleIdTokenValidator.validateAndGetId(request.idToken, request.user)
val user = userService.findUserByOAuthIdOrNull(oAuthId) ?: createUser(oAuthId, request)

val authToken = jwtManager.generateAuthToken(user.toUserInfo())
Expand All @@ -58,13 +62,23 @@ class AuthApplicationService(
oAuthId: String,
request: LoginServiceRequest
): UserDto {
val profileImage = request.profileImage ?: DEFAULT_PROFILE_IMAGE

return userService.create(
oAuthId = oAuthId,
provider = request.provider,
nickname = request.nickname,
profileImage = profileImage
profileImage = request.profileImage
)
}

private fun createUser(
oAuthId: String,
request: AppleLoginServiceRequest
): UserDto {
return userService.create(
oAuthId = oAuthId,
provider = request.provider,
nickname = request.fullName,
profileImage = DEFAULT_PROFILE_IMAGE
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.mashup.pic.auth.applicationService.dto

import com.mashup.pic.domain.user.LoginProvider

data class AppleLoginServiceRequest(
val idToken: String,
val provider: LoginProvider,
val fullName: String,
val user: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ data class LoginServiceRequest(
val idToken: String,
val provider: LoginProvider,
val nickname: String,
val profileImage: String?
val profileImage: String
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.mashup.pic.auth.controller

import com.mashup.pic.auth.applicationService.AuthApplicationService
import com.mashup.pic.auth.controller.dto.AppleLoginRequest
import com.mashup.pic.auth.controller.dto.LoginRequest
import com.mashup.pic.auth.controller.dto.LoginResponse
import com.mashup.pic.auth.controller.dto.ReissueRequest
Expand All @@ -22,14 +23,23 @@ class AuthController(
private val authApplicationService: AuthApplicationService
) {
@SecurityRequirements(value = [])
@Operation(summary = "로그인", description = "OIDC의 ID토큰으로 로그인")
@Operation(summary = "카카오 로그인", description = "OIDC의 ID토큰으로 로그인")
@PostMapping("/login")
fun login(
fun kakaoLogin(
@Valid @RequestBody loginRequest: LoginRequest
): ApiResponse<LoginResponse> {
return ApiResponse.success(authApplicationService.login(loginRequest.toServiceRequest()))
}

@SecurityRequirements(value = [])
@Operation(summary = "애플 로그인", description = "OIDC의 ID토큰으로 로그인")
@PostMapping("/apple-login")
fun appleLogin(
@Valid @RequestBody loginRequest: AppleLoginRequest
): ApiResponse<LoginResponse> {
return ApiResponse.success(authApplicationService.appleLogin(loginRequest.toServiceRequest()))
}

@SecurityRequirements(value = [])
@Operation(summary = "토큰 재발급", description = "리프레시 토큰으로 액세스 토큰, 리프레시 토큰 재발급")
@PostMapping("/token")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.mashup.pic.auth.controller.dto

import com.mashup.pic.auth.applicationService.dto.AppleLoginServiceRequest
import com.mashup.pic.domain.user.LoginProvider
import jakarta.validation.constraints.NotBlank

data class AppleLoginRequest(
@NotBlank val idToken: String,
@NotBlank val fullName: String,
@NotBlank val user: String
) {
fun toServiceRequest(): AppleLoginServiceRequest {
return AppleLoginServiceRequest(
idToken = idToken,
provider = LoginProvider.APPLE,
fullName = fullName,
user = user
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ data class LoginRequest(
@NotBlank val idToken: String,
@NotBlank val provider: LoginProvider,
@NotBlank val nickname: String,
val profileImage: String?
@NotBlank val profileImage: String
) {
fun toServiceRequest(): LoginServiceRequest {
return LoginServiceRequest(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package com.mashup.pic.security.oidc
import com.fasterxml.jackson.databind.ObjectMapper
import com.mashup.pic.common.exception.PicException
import com.mashup.pic.common.exception.PicExceptionType
import com.mashup.pic.external.apple.AppleClient
import com.mashup.pic.external.common.response.JwkKey
import com.mashup.pic.external.kakao.KakaoClient
import io.jsonwebtoken.Jwts
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Profile
Expand All @@ -18,11 +18,10 @@ import java.util.Base64
@Component
@Profile("!test")
class AppleIdTokenValidator(
private val kakaoJwksClient: KakaoClient,
private val appleJwksClient: AppleClient,
private val objectMapper: ObjectMapper,
@Value("\${kakao.issuer}") private val issuer: String,
@Value("\${kakao.audience.rest}") private val restAudience: String,
@Value("\${kakao.audience.native}") private val nativeAudience: String
@Value("\${apple.issuer}") private val issuer: String,
@Value("\${apple.audience}") private val audience: String
) : IdTokenValidator {
private val decoder = Base64.getUrlDecoder()
private val keyFactory = KeyFactory.getInstance(SIGNING_ALGORITHM)
Expand All @@ -43,12 +42,12 @@ class AppleIdTokenValidator(

private fun verifyPayload(
idToken: String,
nickname: String
sub: String
) {
val payload = decodePayload(idToken)
require(payload[ISSUER_KEY] == issuer) { "Invalid issuer" }
require(payload[AUDIENCE_KEY] == restAudience || payload[AUDIENCE_KEY] == nativeAudience) { "Invalid audience" }
require(payload[NICKNAME_KEY] == nickname) { "Invalid nickname" }
require(payload[AUDIENCE_KEY] == audience) { "Invalid audience" }
require(payload[SUB_KEY] == sub) { "Invalid nickname" }
}

private fun verifySignature(idToken: String) {
Expand All @@ -74,8 +73,8 @@ class AppleIdTokenValidator(
}

private fun getJwkByKid(kid: String): JwkKey {
return kakaoJwksClient.getJwks().getJwkKeyByKid(kid)
?: kakaoJwksClient.refreshAndGetJwks().getJwkKeyByKid(kid)
return appleJwksClient.getJwks().getJwkKeyByKid(kid)
?: appleJwksClient.refreshAndGetJwks().getJwkKeyByKid(kid)
?: throw PicException.of(PicExceptionType.ARGUMENT_NOT_VALID, "Can't find the Jwk matching the KID")
}

Expand All @@ -94,7 +93,6 @@ class AppleIdTokenValidator(
companion object {
private const val ISSUER_KEY = "iss"
private const val AUDIENCE_KEY = "aud"
private const val NICKNAME_KEY = "nickname"
private const val SUB_KEY = "sub"
private const val KID_KEY = "kid"
private const val SIGNING_ALGORITHM = "RSA"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.mashup.pic.user.applicationService

import com.mashup.pic.domain.user.UserService
import com.mashup.pic.external.common.JwksClient
import com.mashup.pic.external.kakao.KakaoClient
import com.mashup.pic.security.authentication.UserInfo
import com.mashup.pic.security.jwt.JwtManager
import org.springframework.stereotype.Service
Expand All @@ -11,7 +11,7 @@ import org.springframework.transaction.annotation.Transactional
@Transactional(readOnly = true)
class UserApplicationService(
private val userService: UserService,
private val kakaoClient: JwksClient,
private val kakaoClient: KakaoClient,
private val jwtManager: JwtManager
) {
fun callbackPage(code: String): String? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,75 +4,31 @@ import com.mashup.pic.common.exception.PicException
import com.mashup.pic.common.exception.PicExceptionType
import com.mashup.pic.external.common.JwksClient
import com.mashup.pic.external.common.response.JwksResponse
import com.mashup.pic.external.kakao.dto.AppleTokenInfoResponse
import com.mashup.pic.external.kakao.dto.AppleTokenResponse
import org.springframework.beans.factory.annotation.Value
import org.springframework.cache.annotation.CachePut
import org.springframework.cache.annotation.Cacheable
import org.springframework.context.annotation.Profile
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatusCode
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.util.LinkedMultiValueMap
import org.springframework.util.MultiValueMap
import org.springframework.web.client.RestClient
import org.springframework.web.client.body

@Component
@Profile("!test")
class AppleClient(
private val restClient: RestClient,
@Value("\${kakao.jwk-uri}") private val jwkUri: String,
@Value("\${kakao.token-uri}") private val tokenUri: String,
@Value("\${kakao.info-uri}") private val infoUri: String,
@Value("\${kakao.audience.rest}") private val restApiKey: String,
@Value("\${kakao.redirect.uri}") private val redirectUri: String
@Value("\${apple.jwk-uri}") private val jwkUri: String
) : JwksClient {
@Cacheable("kakao-jwks")
@Cacheable("apple-jwks")
override fun getJwks(): JwksResponse {
return requestJwks()
}

@CachePut("kakao-jwks")
@CachePut("apple-jwks")
override fun refreshAndGetJwks(): JwksResponse {
return requestJwks()
}

override fun getOAuthId(code: String): String {
val tokenResponse = requestToken(code)
return requestTokenInfo(tokenResponse.accessToken).id
}

private fun requestTokenInfo(accessToken: String): AppleTokenInfoResponse {
return restClient.get()
.uri(infoUri)
.header(HttpHeaders.AUTHORIZATION, TOKEN_BEARER + accessToken)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError) { _, response ->
throw PicException.of(
PicExceptionType.EXTERNAL_COMMUNICATION_FAILURE,
"Error fetching JWKS: ${response.statusCode}"
)
}
.body<AppleTokenInfoResponse>() ?: throw PicException.of(PicExceptionType.ARGUMENT_NOT_VALID)
}

private fun requestToken(code: String): AppleTokenResponse {
return restClient.post()
.uri(tokenUri)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(createTokenRequestBody(code))
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError) { _, response ->
throw PicException.of(
PicExceptionType.EXTERNAL_COMMUNICATION_FAILURE,
"Error requesting access token: ${response.statusCode}"
)
}
.body<AppleTokenResponse>() ?: throw PicException.of(PicExceptionType.ARGUMENT_NOT_VALID)
}

private fun requestJwks(): JwksResponse {
return restClient.get()
.uri(jwkUri)
Expand All @@ -85,17 +41,4 @@ class AppleClient(
}
.body<JwksResponse>() ?: throw PicException.of(PicExceptionType.ARGUMENT_NOT_VALID)
}

private fun createTokenRequestBody(code: String): MultiValueMap<String, String> {
return LinkedMultiValueMap<String, String>().apply {
add("grant_type", "authorization_code")
add("client_id", restApiKey)
add("redirect_uri", redirectUri)
add("code", code)
}
}

companion object {
private const val TOKEN_BEARER = "Bearer "
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,4 @@ interface JwksClient {
fun getJwks(): JwksResponse

fun refreshAndGetJwks(): JwksResponse

fun getOAuthId(code: String): String
}
Loading

0 comments on commit 3dd5e35

Please sign in to comment.