Skip to content

Commit

Permalink
Merge pull request #79 from mash-up-kr/feature/apple-login
Browse files Browse the repository at this point in the history
feat: Apple login
  • Loading branch information
210-reverof authored Aug 29, 2024
2 parents b75342c + cde9535 commit 4df4bc1
Show file tree
Hide file tree
Showing 24 changed files with 250 additions and 105 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
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
Expand All @@ -9,7 +10,8 @@ import com.mashup.pic.domain.user.UserDto
import com.mashup.pic.domain.user.UserService
import com.mashup.pic.security.authentication.UserInfo
import com.mashup.pic.security.jwt.JwtManager
import com.mashup.pic.security.oidc.IdTokenValidator
import com.mashup.pic.security.oidc.AppleIdTokenValidator
import com.mashup.pic.security.oidc.KakaoIdTokenValidator
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

Expand All @@ -19,11 +21,22 @@ class AuthApplicationService(
private val userService: UserService,
private val refreshTokenService: RefreshTokenService,
private val jwtManager: JwtManager,
private val idTokenValidator: IdTokenValidator
private val kakaoIdTokenValidator: KakaoIdTokenValidator,
private val appleIdTokenValidator: AppleIdTokenValidator
) {
@Transactional
fun login(request: LoginServiceRequest): LoginResponse {
val oAuthId = idTokenValidator.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 @@ -46,21 +59,38 @@ class AuthApplicationService(
}

private fun createUser(
oAuthId: Long,
oAuthId: String,
request: LoginServiceRequest
): UserDto {
return userService.create(
oAuthId = oAuthId,
provider = request.provider,
nickname = request.nickname,
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
)
}

fun UserDto.toUserInfo(): UserInfo {
return UserInfo(
id = this.id,
nickname = this.nickname,
roles = this.roles
)
}

companion object {
const val DEFAULT_PROFILE_IMAGE = "https://www.testhouse.net/wp-content/uploads/2021/11/default-avatar.jpg"
}
}
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
@@ -1,14 +1,10 @@
package com.mashup.pic.auth.applicationService.dto

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

data class LoginServiceRequest(
val idToken: String,
val provider: LoginProvider,
val nickname: String,
val profileImage: String
)

enum class LoginProvider {
KAKAO,
NAVER,
GOOGLE
}
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
@@ -1,7 +1,7 @@
package com.mashup.pic.auth.controller.dto

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

data class LoginRequest(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
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 io.jsonwebtoken.Jwts
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Component
import java.math.BigInteger
import java.security.Key
import java.security.KeyFactory
import java.security.spec.RSAPublicKeySpec
import java.util.Base64

@Component
@Profile("!test")
class AppleIdTokenValidator(
private val appleJwksClient: AppleClient,
private val objectMapper: ObjectMapper,
@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)

override fun validateAndGetId(
idToken: String,
nickname: String
): String {
verifyPayload(idToken, nickname)
verifySignature(idToken)
return extractSub(idToken)
}

private fun extractSub(idToken: String): String {
val payload = decodePayload(idToken)
return payload[SUB_KEY] as String? ?: throw PicException.of(PicExceptionType.ARGUMENT_NOT_VALID, "Can't extract SUB")
}

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

private fun verifySignature(idToken: String) {
val kid = extractKid(idToken)
val publicKey = getPublicKey(kid)
Jwts.parserBuilder()
.setSigningKey(publicKey)
.build()
.parseClaimsJws(idToken)
}

private fun extractKid(idToken: String): String {
val header = decodeHeader(idToken)
return header[KID_KEY] as? String ?: throw PicException.of(PicExceptionType.ARGUMENT_NOT_VALID, "Can't extract KID")
}

private fun getPublicKey(kid: String): Key {
val jwk = getJwkByKid(kid)
val n = BigInteger(1, decoder.decode(jwk.n))
val e = BigInteger(1, decoder.decode(jwk.e))
val keySpec = RSAPublicKeySpec(n, e)
return keyFactory.generatePublic(keySpec)
}

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

private fun decodePayload(idToken: String): Map<String, Any> {
val payload = idToken.split(".")[1]
val decodedPayload = String(decoder.decode(payload))
return objectMapper.readValue(decodedPayload, Map::class.java) as Map<String, Any>
}

private fun decodeHeader(idToken: String): Map<String, Any> {
val header = idToken.split(".")[0]
val decodedHeader = String(decoder.decode(header))
return objectMapper.readValue(decodedHeader, Map::class.java) as Map<String, Any>
}

companion object {
private const val ISSUER_KEY = "iss"
private const val AUDIENCE_KEY = "aud"
private const val SUB_KEY = "sub"
private const val KID_KEY = "kid"
private const val SIGNING_ALGORITHM = "RSA"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ interface IdTokenValidator {
fun validateAndGetId(
idToken: String,
nickname: String
): Long
): String
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ class KakaoIdTokenValidator(
override fun validateAndGetId(
idToken: String,
nickname: String
): Long {
): String {
verifyPayload(idToken, nickname)
verifySignature(idToken)
return extractSub(idToken).toLong()
return extractSub(idToken)
}

private fun extractSub(idToken: String): String {
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

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class FakeJwksClient : JwksClient {
TODO("Do Nothing in testing environment")
}

override fun getOAuthId(code: String): Long {
fun getOAuthId(code: String): Long {
TODO("Do Nothing in testing environment")
}
}

This file was deleted.

Loading

0 comments on commit 4df4bc1

Please sign in to comment.