diff --git a/pic-api/src/main/kotlin/com/mashup/pic/auth/applicationService/AuthApplicationService.kt b/pic-api/src/main/kotlin/com/mashup/pic/auth/applicationService/AuthApplicationService.kt index aabbee1..8ede19f 100644 --- a/pic-api/src/main/kotlin/com/mashup/pic/auth/applicationService/AuthApplicationService.kt +++ b/pic-api/src/main/kotlin/com/mashup/pic/auth/applicationService/AuthApplicationService.kt @@ -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 @@ -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()) @@ -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 ) } diff --git a/pic-api/src/main/kotlin/com/mashup/pic/auth/applicationService/dto/AppleLoginServiceRequest.kt b/pic-api/src/main/kotlin/com/mashup/pic/auth/applicationService/dto/AppleLoginServiceRequest.kt new file mode 100644 index 0000000..a4d3ec3 --- /dev/null +++ b/pic-api/src/main/kotlin/com/mashup/pic/auth/applicationService/dto/AppleLoginServiceRequest.kt @@ -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 +) diff --git a/pic-api/src/main/kotlin/com/mashup/pic/auth/applicationService/dto/LoginServiceRequest.kt b/pic-api/src/main/kotlin/com/mashup/pic/auth/applicationService/dto/LoginServiceRequest.kt index 0943068..e334548 100644 --- a/pic-api/src/main/kotlin/com/mashup/pic/auth/applicationService/dto/LoginServiceRequest.kt +++ b/pic-api/src/main/kotlin/com/mashup/pic/auth/applicationService/dto/LoginServiceRequest.kt @@ -6,5 +6,5 @@ data class LoginServiceRequest( val idToken: String, val provider: LoginProvider, val nickname: String, - val profileImage: String? + val profileImage: String ) diff --git a/pic-api/src/main/kotlin/com/mashup/pic/auth/controller/AuthController.kt b/pic-api/src/main/kotlin/com/mashup/pic/auth/controller/AuthController.kt index de9088e..951cd81 100644 --- a/pic-api/src/main/kotlin/com/mashup/pic/auth/controller/AuthController.kt +++ b/pic-api/src/main/kotlin/com/mashup/pic/auth/controller/AuthController.kt @@ -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 @@ -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 { return ApiResponse.success(authApplicationService.login(loginRequest.toServiceRequest())) } + @SecurityRequirements(value = []) + @Operation(summary = "애플 로그인", description = "OIDC의 ID토큰으로 로그인") + @PostMapping("/apple-login") + fun appleLogin( + @Valid @RequestBody loginRequest: AppleLoginRequest + ): ApiResponse { + return ApiResponse.success(authApplicationService.appleLogin(loginRequest.toServiceRequest())) + } + @SecurityRequirements(value = []) @Operation(summary = "토큰 재발급", description = "리프레시 토큰으로 액세스 토큰, 리프레시 토큰 재발급") @PostMapping("/token") diff --git a/pic-api/src/main/kotlin/com/mashup/pic/auth/controller/dto/AppleLoginRequest.kt b/pic-api/src/main/kotlin/com/mashup/pic/auth/controller/dto/AppleLoginRequest.kt new file mode 100644 index 0000000..42dc5c8 --- /dev/null +++ b/pic-api/src/main/kotlin/com/mashup/pic/auth/controller/dto/AppleLoginRequest.kt @@ -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 + ) + } +} diff --git a/pic-api/src/main/kotlin/com/mashup/pic/auth/controller/dto/LoginRequest.kt b/pic-api/src/main/kotlin/com/mashup/pic/auth/controller/dto/LoginRequest.kt index d7a4b44..b9a7e69 100644 --- a/pic-api/src/main/kotlin/com/mashup/pic/auth/controller/dto/LoginRequest.kt +++ b/pic-api/src/main/kotlin/com/mashup/pic/auth/controller/dto/LoginRequest.kt @@ -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( diff --git a/pic-api/src/main/kotlin/com/mashup/pic/security/oidc/AppleIdTokenValidator.kt b/pic-api/src/main/kotlin/com/mashup/pic/security/oidc/AppleIdTokenValidator.kt index 5c6faa9..1aa183d 100644 --- a/pic-api/src/main/kotlin/com/mashup/pic/security/oidc/AppleIdTokenValidator.kt +++ b/pic-api/src/main/kotlin/com/mashup/pic/security/oidc/AppleIdTokenValidator.kt @@ -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 @@ -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) @@ -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) { @@ -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") } @@ -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" diff --git a/pic-api/src/main/kotlin/com/mashup/pic/user/applicationService/UserApplicationService.kt b/pic-api/src/main/kotlin/com/mashup/pic/user/applicationService/UserApplicationService.kt index 1714820..a9906f2 100644 --- a/pic-api/src/main/kotlin/com/mashup/pic/user/applicationService/UserApplicationService.kt +++ b/pic-api/src/main/kotlin/com/mashup/pic/user/applicationService/UserApplicationService.kt @@ -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 @@ -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? { diff --git a/pic-external/src/main/kotlin/com/mashup/pic/external/apple/AppleClient.kt b/pic-external/src/main/kotlin/com/mashup/pic/external/apple/AppleClient.kt index bcbde2e..03b2d93 100644 --- a/pic-external/src/main/kotlin/com/mashup/pic/external/apple/AppleClient.kt +++ b/pic-external/src/main/kotlin/com/mashup/pic/external/apple/AppleClient.kt @@ -4,18 +4,12 @@ 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 @@ -23,56 +17,18 @@ import org.springframework.web.client.body @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() ?: 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() ?: throw PicException.of(PicExceptionType.ARGUMENT_NOT_VALID) - } - private fun requestJwks(): JwksResponse { return restClient.get() .uri(jwkUri) @@ -85,17 +41,4 @@ class AppleClient( } .body() ?: throw PicException.of(PicExceptionType.ARGUMENT_NOT_VALID) } - - private fun createTokenRequestBody(code: String): MultiValueMap { - return LinkedMultiValueMap().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 " - } } diff --git a/pic-external/src/main/kotlin/com/mashup/pic/external/apple/dto/AppleTokenInfoResponse.kt b/pic-external/src/main/kotlin/com/mashup/pic/external/apple/dto/AppleTokenInfoResponse.kt deleted file mode 100644 index 15e133e..0000000 --- a/pic-external/src/main/kotlin/com/mashup/pic/external/apple/dto/AppleTokenInfoResponse.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.mashup.pic.external.kakao.dto - -data class AppleTokenInfoResponse( - val id: String, - val expiresIn: Int, - val appId: Int -) diff --git a/pic-external/src/main/kotlin/com/mashup/pic/external/apple/dto/AppleTokenResponse.kt b/pic-external/src/main/kotlin/com/mashup/pic/external/apple/dto/AppleTokenResponse.kt deleted file mode 100644 index 192c74f..0000000 --- a/pic-external/src/main/kotlin/com/mashup/pic/external/apple/dto/AppleTokenResponse.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.mashup.pic.external.kakao.dto - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties -import com.fasterxml.jackson.annotation.JsonProperty - -@JsonIgnoreProperties(ignoreUnknown = true) -data class AppleTokenResponse( - @JsonProperty("token_type") - val tokenType: String, - @JsonProperty("access_token") - val accessToken: String, - @JsonProperty("id_token") - val idToken: String, - @JsonProperty("expires_in") - val expiresIn: Int, - @JsonProperty("refresh_token") - val refreshToken: String, - @JsonProperty("refresh_token_expires_in") - val refreshTokenExpiresIn: Int, - @JsonProperty("scope") - val scope: String -) diff --git a/pic-external/src/main/kotlin/com/mashup/pic/external/common/JwksClient.kt b/pic-external/src/main/kotlin/com/mashup/pic/external/common/JwksClient.kt index dd39ac4..c2c4998 100644 --- a/pic-external/src/main/kotlin/com/mashup/pic/external/common/JwksClient.kt +++ b/pic-external/src/main/kotlin/com/mashup/pic/external/common/JwksClient.kt @@ -6,6 +6,4 @@ interface JwksClient { fun getJwks(): JwksResponse fun refreshAndGetJwks(): JwksResponse - - fun getOAuthId(code: String): String } diff --git a/pic-external/src/main/kotlin/com/mashup/pic/external/kakao/KakaoClient.kt b/pic-external/src/main/kotlin/com/mashup/pic/external/kakao/KakaoClient.kt index 36a05a5..364df04 100644 --- a/pic-external/src/main/kotlin/com/mashup/pic/external/kakao/KakaoClient.kt +++ b/pic-external/src/main/kotlin/com/mashup/pic/external/kakao/KakaoClient.kt @@ -4,8 +4,8 @@ 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 com.mashup.pic.external.kakao.dto.KakaoTokenInfoResponse +import com.mashup.pic.external.kakao.dto.KakaoTokenResponse import org.springframework.beans.factory.annotation.Value import org.springframework.cache.annotation.CachePut import org.springframework.cache.annotation.Cacheable @@ -39,12 +39,12 @@ class KakaoClient( return requestJwks() } - override fun getOAuthId(code: String): String { + fun getOAuthId(code: String): String { val tokenResponse = requestToken(code) return requestTokenInfo(tokenResponse.accessToken).id } - private fun requestTokenInfo(accessToken: String): AppleTokenInfoResponse { + private fun requestTokenInfo(accessToken: String): KakaoTokenInfoResponse { return restClient.get() .uri(infoUri) .header(HttpHeaders.AUTHORIZATION, TOKEN_BEARER + accessToken) @@ -55,10 +55,10 @@ class KakaoClient( "Error fetching JWKS: ${response.statusCode}" ) } - .body() ?: throw PicException.of(PicExceptionType.ARGUMENT_NOT_VALID) + .body() ?: throw PicException.of(PicExceptionType.ARGUMENT_NOT_VALID) } - private fun requestToken(code: String): AppleTokenResponse { + private fun requestToken(code: String): KakaoTokenResponse { return restClient.post() .uri(tokenUri) .contentType(MediaType.APPLICATION_FORM_URLENCODED) @@ -70,7 +70,7 @@ class KakaoClient( "Error requesting access token: ${response.statusCode}" ) } - .body() ?: throw PicException.of(PicExceptionType.ARGUMENT_NOT_VALID) + .body() ?: throw PicException.of(PicExceptionType.ARGUMENT_NOT_VALID) } private fun requestJwks(): JwksResponse { diff --git a/pic-external/src/main/kotlin/com/mashup/pic/external/kakao/dto/KakaoTokenInfoResponse.kt b/pic-external/src/main/kotlin/com/mashup/pic/external/kakao/dto/KakaoTokenInfoResponse.kt index c49bc04..d35f737 100644 --- a/pic-external/src/main/kotlin/com/mashup/pic/external/kakao/dto/KakaoTokenInfoResponse.kt +++ b/pic-external/src/main/kotlin/com/mashup/pic/external/kakao/dto/KakaoTokenInfoResponse.kt @@ -1,7 +1,7 @@ package com.mashup.pic.external.kakao.dto data class KakaoTokenInfoResponse( - val id: Long, + val id: String, val expiresIn: Int, val appId: Int ) diff --git a/pic-external/src/main/resources/application-external.yaml b/pic-external/src/main/resources/application-external.yaml index 3eaa64b..55edc5a 100644 --- a/pic-external/src/main/resources/application-external.yaml +++ b/pic-external/src/main/resources/application-external.yaml @@ -12,6 +12,11 @@ kakao: token-uri: https://kauth.kakao.com/oauth/token info-uri: https://kapi.kakao.com/v1/user/access_token_info +apple: + issuer: https://appleid.apple.com + audience: com.mashup.gabbangzip + jwk-uri: https://appleid.apple.com/auth/oauth2/v2/keys + cloud: aws: credentials: