Skip to content

Commit

Permalink
Apple OAuth
Browse files Browse the repository at this point in the history
  • Loading branch information
gurdl0525 committed Dec 1, 2023
1 parent 1c69206 commit 9787c7b
Show file tree
Hide file tree
Showing 23 changed files with 296 additions and 105 deletions.
2 changes: 2 additions & 0 deletions maeumgagym-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ dependencies {
implementation(project(":maeumgagym-common"))

implementation(Dependencies.SPRING_TRANSACTION)
implementation(Dependencies.JWT)
}

allOpen {
annotation("com.info.common.UseCase")
annotation("org.springframework.stereotype.Service")
annotation("com.info.common.PersistenceAdapter")
annotation("com.info.common.WebAdapter")
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.info.maeumgagym.feign.oauth.apple.dto
package com.info.maeumgagym.auth.dto.response

import org.springframework.util.Base64Utils
import java.math.BigInteger
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.info.maeumgagym.feign.oauth.apple.dto
package com.info.maeumgagym.auth.dto.response

data class ApplePublicKeys(
val keys: MutableList<ApplePublicKey>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.info.maeumgagym.auth.port.`in`

import com.info.maeumgagym.auth.dto.response.TokenResponse

interface AppleLoginUseCase {

fun execute(token: String): TokenResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.info.maeumgagym.auth.port.out

interface AppleJwtParsePort {

fun parseHeaders(token: String): MutableMap<String?, String?>
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ import com.info.maeumgagym.auth.dto.response.TokenResponse
import java.util.UUID

interface GenerateJwtPort {

fun generateToken(userId: UUID): TokenResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.info.maeumgagym.auth.port.out

import com.info.maeumgagym.auth.dto.response.ApplePublicKeys
import java.security.PublicKey

interface GeneratePublicKeyPort {

fun generatePublicKey(
tokenHeaders: MutableMap<String?, String?>,
applePublicKeys: ApplePublicKeys
): PublicKey
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.info.maeumgagym.auth.port.out

import io.jsonwebtoken.Claims
import java.security.PublicKey

interface ParsePublicKeyPort {

fun parseClaimsFromPublicKey(token: String, publicKey: PublicKey): Claims
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.info.maeumgagym.auth.port.out

import com.info.maeumgagym.auth.dto.response.ApplePublicKeys

interface ReadApplePublicKeyPort {

fun readPublicKey(): ApplePublicKeys
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.info.maeumgagym.auth.service

import com.info.maeumgagym.auth.dto.response.TokenResponse
import com.info.maeumgagym.auth.port.`in`.AppleLoginUseCase
import com.info.maeumgagym.auth.port.out.*
import com.info.maeumgagym.user.model.Role
import com.info.maeumgagym.user.model.User
import com.info.maeumgagym.user.port.out.CreateUserPort
import com.info.maeumgagym.user.port.out.FindUserByOAuthIdPort
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
class AppleLoginService(
private val appleJwtParsePort: AppleJwtParsePort,
private val generatePublicKeyPort: GeneratePublicKeyPort,
private val parsePublicKeyPort: ParsePublicKeyPort,
private val readApplePublicKey: ReadApplePublicKeyPort,
private val findUserByOAuthIdPort: FindUserByOAuthIdPort,
private val createUserPort: CreateUserPort,
private val generateJwtPort: GenerateJwtPort
): AppleLoginUseCase{

@Transactional
override fun execute(token: String): TokenResponse {

val idToken = parseIdToken(token)

val sub = idToken.subject

val user: User = findUserByOAuthIdPort.findUserByOAuthId(sub) ?: createUserPort.createUser(
User(
nickname = idToken.get("name", String::class.java),
roles = mutableListOf(Role.USER),
oauthId = sub,
profilePath = null
)
)

return generateJwtPort.generateToken(user.id)
}

private fun parseIdToken(token: String) = parsePublicKeyPort.parseClaimsFromPublicKey(
token, generatePublicKeyPort.generatePublicKey(appleJwtParsePort.parseHeaders(token), readApplePublicKey.readPublicKey())
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.info.maeumgagym.auth.adapter

import com.info.common.WebAdapter
import com.info.maeumgagym.auth.port.out.ReadApplePublicKeyPort
import com.info.maeumgagym.feign.oauth.apple.AppleClient

@WebAdapter
class AppleAuthAdapter(
private val appleClient: AppleClient
): ReadApplePublicKeyPort {

override fun readPublicKey() = appleClient.applePublicKeys()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.info.maeumgagym.auth.adapter

import com.info.common.WebAdapter
import com.info.maeumgagym.auth.dto.response.ApplePublicKey
import com.info.maeumgagym.auth.dto.response.ApplePublicKeys
import com.info.maeumgagym.auth.port.out.GeneratePublicKeyPort
import com.info.maeumgagym.global.exception.InvalidTokenException
import java.security.KeyFactory
import java.security.NoSuchAlgorithmException
import java.security.PublicKey
import java.security.spec.InvalidKeySpecException

@WebAdapter
class GeneratePublicKeyAdapter: GeneratePublicKeyPort {

private companion object {
const val ALG_HEADER_KEY = "alg"
const val KID_HEADER_KEY = "kid"
}

override fun generatePublicKey(tokenHeaders: MutableMap<String?, String?>, applePublicKeys: ApplePublicKeys): PublicKey {

val publicKey: ApplePublicKey = applePublicKeys.matchesKey(
tokenHeaders[ALG_HEADER_KEY]!!,
tokenHeaders[KID_HEADER_KEY]!!
) ?: throw InvalidTokenException

return try {
KeyFactory.getInstance(publicKey.kty).generatePublic(publicKey.publicKeySpec())
} catch (e: NoSuchAlgorithmException) {
throw IllegalStateException("Apple OAuth 로그인 중 public key 생성에 문제가 발생했습니다.")
} catch (e: InvalidKeySpecException) {
throw IllegalStateException("Apple OAuth 로그인 중 public key 생성에 문제가 발생했습니다.")
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.info.maeumgagym.feign.oauth.apple

import com.info.maeumgagym.auth.dto.response.ApplePublicKeys
import com.info.maeumgagym.global.config.feign.FeignConfig
import com.info.maeumgagym.feign.oauth.apple.dto.ApplePublicKeys
import org.springframework.cloud.openfeign.FeignClient
import org.springframework.web.bind.annotation.GetMapping

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.info.maeumgagym.global.jwt

import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.ObjectMapper
import com.info.maeumgagym.auth.port.out.AppleJwtParsePort
import com.info.maeumgagym.global.exception.InvalidTokenException
import org.springframework.stereotype.Component
import org.springframework.util.Base64Utils


@Component
class AppleJwtParser(private val objectMapper: ObjectMapper): AppleJwtParsePort {

companion object {
private const val IDENTITY_TOKEN_VALUE_DELIMITER = "\\."
private const val HEADER_INDEX = 0
}

@Suppress("unchecked_cast")
override fun parseHeaders(token: String): MutableMap<String?, String?> {
return try {
val encodedHeader: String = token.split(IDENTITY_TOKEN_VALUE_DELIMITER.toRegex())[HEADER_INDEX]
val decodedHeader = String(Base64Utils.decodeFromUrlSafeString(encodedHeader))
objectMapper.readValue(decodedHeader, MutableMap::class.java) as MutableMap<String?, String?>
} catch (e: JsonProcessingException) {
throw InvalidTokenException
} catch (e: ArrayIndexOutOfBoundsException) {
throw InvalidTokenException
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.info.maeumgagym.global.jwt

import com.info.maeumgagym.auth.dto.response.TokenResponse
import com.info.maeumgagym.auth.port.out.GenerateJwtPort
import com.info.maeumgagym.auth.port.out.ParsePublicKeyPort
import com.info.maeumgagym.global.env.jwt.JwtProperties
import com.info.maeumgagym.global.exception.ExpiredTokenException
import com.info.maeumgagym.global.exception.InvalidTokenException
import com.info.maeumgagym.global.security.principle.CustomUserDetailService
import com.info.maeumgagym.global.security.principle.CustomUserDetails
import io.jsonwebtoken.*
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.stereotype.Component
import java.security.PublicKey
import java.util.*

@Component
class JwtAdapter(
val jwtProperties: JwtProperties,
private val customUserDetailService: CustomUserDetailService
) : GenerateJwtPort, ParsePublicKeyPort {

override fun generateToken(userId: UUID): TokenResponse {
return TokenResponse(
generateAccessToken(userId)
)
}

private fun generateAccessToken(userId: UUID): String {
val now = Date()
return Jwts.builder()
.setSubject(userId.toString())
.setIssuedAt(now)
.setExpiration(Date(now.time + jwtProperties.accessExpiredExp * 1000L))
.signWith(SignatureAlgorithm.HS256, jwtProperties.secretKey)
.compact()
}

fun getAuthentication(token: String): Authentication {

val subject = getBody(token).subject

val authDetails = customUserDetailService.loadUserByUsername(subject) as CustomUserDetails

return UsernamePasswordAuthenticationToken(authDetails, null, authDetails.authorities)
}

private fun getBody(token: String): Claims {
try {
return Jwts.parser().setSigningKey(jwtProperties.secretKey).parseClaimsJws(token).body
} catch (e: JwtException) {
when (e) {
is ExpiredJwtException -> throw ExpiredTokenException
else -> throw InvalidTokenException
}
}
}

override fun parseClaimsFromPublicKey(token: String, publicKey: PublicKey): Claims {
return try {
Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token).body
} catch (e: Exception) {
when (e) {
is ExpiredJwtException -> throw ExpiredTokenException
else -> throw InvalidTokenException
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.info.maeumgagym.global.jwt

import com.info.maeumgagym.global.security.principle.CustomUserDetailService
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.filter.OncePerRequestFilter
import javax.servlet.FilterChain
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

class JwtFilter(
customUserDetailService: CustomUserDetailService,
private val jwtResolver: JwtResolver,
private val jwtAdapter: JwtAdapter
) : OncePerRequestFilter() {

override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {

jwtResolver.resolveToken(request)
?.let {
SecurityContextHolder.getContext().authentication = jwtAdapter.getAuthentication(it)

filterChain.doFilter(request, response)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.info.maeumgagym.global.security.token
package com.info.maeumgagym.global.jwt

import com.info.maeumgagym.global.env.jwt.JwtProperties
import org.springframework.stereotype.Component
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package com.info.maeumgagym.global.security

import com.fasterxml.jackson.databind.ObjectMapper
import com.info.maeumgagym.global.error.GlobalExceptionFilter
import com.info.maeumgagym.global.security.jwt.JwtFilter
import com.info.maeumgagym.global.jwt.JwtFilter
import com.info.maeumgagym.global.security.principle.CustomUserDetailService
import com.info.maeumgagym.global.security.token.JwtAdapter
import com.info.maeumgagym.global.security.token.JwtResolver
import com.info.maeumgagym.global.jwt.JwtAdapter
import com.info.maeumgagym.global.jwt.JwtResolver
import org.springframework.security.config.annotation.SecurityConfigurerAdapter
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.web.DefaultSecurityFilterChain
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package com.info.maeumgagym.global.security

import com.fasterxml.jackson.databind.ObjectMapper
import com.info.maeumgagym.global.security.principle.CustomUserDetailService
import com.info.maeumgagym.global.security.token.JwtAdapter
import com.info.maeumgagym.global.security.token.JwtResolver
import com.info.maeumgagym.global.jwt.JwtAdapter
import com.info.maeumgagym.global.jwt.JwtResolver
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
Expand Down

This file was deleted.

Loading

0 comments on commit 9787c7b

Please sign in to comment.