Skip to content

Commit

Permalink
Fix/apple login (#311)
Browse files Browse the repository at this point in the history
  • Loading branch information
SeonghaeJo authored Jan 10, 2025
1 parent 5b6db29 commit 0b58d1f
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 4 deletions.
6 changes: 6 additions & 0 deletions api/src/main/kotlin/handler/AuthHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ class AuthHandler(
userService.loginKakao(socialLoginRequest)
}

suspend fun loginAppleLegacy(req: ServerRequest): ServerResponse =
handle(req) {
val socialLoginRequest: SocialLoginRequest = req.awaitBodyOrNull() ?: throw ServerWebInputException("Invalid body")
userService.loginApple(socialLoginRequest)
}

suspend fun loginApple(req: ServerRequest): ServerResponse =
handle(req) {
val socialLoginRequest: SocialLoginRequest = req.awaitBodyOrNull() ?: throw ServerWebInputException("Invalid body")
Expand Down
1 change: 1 addition & 0 deletions api/src/main/kotlin/router/MainRouter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class MainRouter(
POST("/login/facebook", authHandler::loginFacebook)
POST("/login/google", authHandler::loginGoogle)
POST("/login/kakao", authHandler::loginKakao)
POST("/login_apple", authHandler::loginAppleLegacy)
POST("/login/apple", authHandler::loginApple)
POST("/logout", authHandler::logout)
POST("/password/reset/email/check", authHandler::getMaskedEmail)
Expand Down
24 changes: 24 additions & 0 deletions api/src/main/kotlin/router/docs/AuthDocs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,30 @@ import org.springframework.web.bind.annotation.RequestMethod
],
),
),
RouterOperation(
path = "/v1/auth/login/apple",
method = [RequestMethod.POST],
produces = [MediaType.APPLICATION_JSON_VALUE],
operation =
Operation(
operationId = "loginApple",
requestBody =
RequestBody(
content = [
Content(
schema = Schema(implementation = SocialLoginRequest::class),
mediaType = MediaType.APPLICATION_JSON_VALUE,
),
],
),
responses = [
ApiResponse(
responseCode = "200",
content = [Content(schema = Schema(implementation = LoginResponse::class))],
),
],
),
),
RouterOperation(
path = "/v1/auth/logout",
method = [RequestMethod.POST],
Expand Down
25 changes: 21 additions & 4 deletions core/src/main/kotlin/auth/apple/AppleClient.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.wafflestudio.snu4t.auth.apple

import com.fasterxml.jackson.databind.ObjectMapper
import com.wafflestudio.snu4t.auth.OAuth2Client
import com.wafflestudio.snu4t.auth.OAuth2UserResponse
import com.wafflestudio.snu4t.common.exception.InvalidAppleLoginTokenException
import com.wafflestudio.snu4t.common.extension.get
import io.jsonwebtoken.Jwts
import org.springframework.http.client.reactive.ReactorClientHttpConnector
Expand All @@ -18,6 +20,7 @@ import java.util.Base64
@Component("APPLE")
class AppleClient(
webClientBuilder: WebClient.Builder,
private val objectMapper: ObjectMapper,
) : OAuth2Client {
private val webClient =
webClientBuilder.clientConnector(
Expand All @@ -35,9 +38,9 @@ class AppleClient(
override suspend fun getMe(token: String): OAuth2UserResponse? {
val jwtHeader = extractJwtHeader(token)
val appleJwk =
webClient.get<List<AppleJwk>>(uri = APPLE_JWK_URI).getOrNull()
?.find {
it.kid == jwtHeader.keyId && it.alg == jwtHeader.algorithm
webClient.get<Map<String, List<AppleJwk>>>(uri = APPLE_JWK_URI).getOrNull()
?.get("keys")?.find {
it.kid == jwtHeader.kid && it.alg == jwtHeader.alg
} ?: return null
val publicKey = convertJwkToPublicKey(appleJwk)
val jwtPayload = verifyAndDecodeToken(token, publicKey)
Expand All @@ -51,7 +54,16 @@ class AppleClient(
)
}

private suspend fun extractJwtHeader(token: String) = Jwts.parser().parseClaimsJws(token).header
private suspend fun extractJwtHeader(token: String): AppleJwtHeader {
val headerJson = Base64.getDecoder().decode(token.substringBefore(".")).toString(Charsets.UTF_8)
val headerMap = objectMapper.readValue(headerJson, Map::class.java)
val kid = headerMap["kid"] as? String ?: throw InvalidAppleLoginTokenException
val alg = headerMap["alg"] as? String ?: throw InvalidAppleLoginTokenException
return AppleJwtHeader(
kid = kid,
alg = alg,
)
}

private suspend fun convertJwkToPublicKey(jwk: AppleJwk): PublicKey {
val modulus = BigInteger(1, Base64.getUrlDecoder().decode(jwk.n))
Expand All @@ -65,3 +77,8 @@ class AppleClient(
publicKey: PublicKey,
) = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token).body
}

private data class AppleJwtHeader(
val kid: String,
val alg: String,
)
1 change: 1 addition & 0 deletions core/src/main/kotlin/common/exception/ErrorType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ enum class ErrorType(
UPDATE_APP_VERSION(HttpStatus.BAD_REQUEST, 40021, "앱 버전을 업데이트해주세요.", "앱 버전을 업데이트해주세요."),

SOCIAL_CONNECT_FAIL(HttpStatus.UNAUTHORIZED, 40100, "소셜 로그인에 실패했습니다.", "소셜 로그인에 실패했습니다."),
INVALID_APPLE_LOGIN_TOKEN(HttpStatus.UNAUTHORIZED, 40101, "유효하지 않은 애플 로그인 토큰입니다.", "소셜 로그인에 실패했습니다."),

TIMETABLE_NOT_FOUND(HttpStatus.NOT_FOUND, 40400, "timetable_id가 유효하지 않습니다", "존재하지 않는 시간표입니다."),
PRIMARY_TIMETABLE_NOT_FOUND(HttpStatus.NOT_FOUND, 40401, "timetable_id가 유효하지 않습니다", "대표 시간표가 존재하지 않습니다."),
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/kotlin/common/exception/Snu4tException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ object UpdateAppVersionException : Snu4tException(ErrorType.UPDATE_APP_VERSION)

object SocialConnectFailException : Snu4tException(ErrorType.SOCIAL_CONNECT_FAIL)

object InvalidAppleLoginTokenException : Snu4tException(ErrorType.INVALID_APPLE_LOGIN_TOKEN)

object NoUserFcmKeyException : Snu4tException(ErrorType.NO_USER_FCM_KEY)

object InvalidRegistrationForPreviousSemesterCourseException :
Expand Down

0 comments on commit 0b58d1f

Please sign in to comment.