Skip to content
This repository was archived by the owner on Jul 7, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
package com.asap.application.user.port.`in`

interface SocialLoginUsecase {

fun login(command: Command): Response

data class Command(
val provider: String,
val accessToken: String,
)

sealed class Response {
}
sealed class Response

data class Success(
val accessToken: String,
val refreshToken: String,
val isProcessedOnboarding: Boolean
val isProcessedOnboarding: Boolean,
) : Response()

data class NonRegistered(
val registerToken: String
val registerToken: String,
) : Response()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ import com.asap.application.user.vo.AuthInfo
import com.asap.domain.user.enums.SocialLoginProvider

interface AuthInfoRetrievePort {

@Throws(UserException.UserAuthNotFoundException::class)
fun getAuthInfo(provider: SocialLoginProvider, accessToken: String): AuthInfo
}
fun getAuthInfo(
provider: SocialLoginProvider,
accessToken: String,
): AuthInfo

fun getAccessToken(
provider: SocialLoginProvider,
code: String,
state: String,
): String
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package com.asap.bootstrap.web.auth.api

import com.asap.bootstrap.common.exception.ExceptionResponse
import com.asap.bootstrap.web.auth.dto.ReissueRequest
import com.asap.bootstrap.web.auth.dto.ReissueResponse
import com.asap.bootstrap.web.auth.dto.SocialLoginRequest
import com.asap.bootstrap.web.auth.dto.SocialLoginResponse
import com.asap.bootstrap.web.auth.dto.*
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema
Expand Down Expand Up @@ -62,6 +59,38 @@ interface AuthApi {
@RequestBody request: SocialLoginRequest,
): ResponseEntity<SocialLoginResponse>

@Operation(summary = "OAuth 액세스 토큰 획득")
@PostMapping("/token/{provider}")
@ApiResponses(
value = [
ApiResponse(
responseCode = "200",
description = "액세스 토큰 획득 성공",
content = [
Content(
mediaType = "application/json",
schema = Schema(implementation = OAuthAccessTokenResponse::class),
),
],
),
ApiResponse(
responseCode = "4XX",
description = "액세스 토큰 획득 실패",
content = [
Content(
mediaType = "application/json",
schema = Schema(implementation = ExceptionResponse::class),
),
],
),
],
)
fun getAccessToken(
@Schema(description = "소셜 로그인 플랫폼, ex) KAKAO, GOOGLE, NAVER")
@PathVariable provider: String,
@RequestBody request: OAuthAccessTokenRequest,
): OAuthAccessTokenResponse

@Operation(summary = "토큰 재발급")
@PostMapping("/reissue")
@ApiResponses(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ package com.asap.bootstrap.web.auth.controller

import com.asap.application.user.port.`in`.ReissueTokenUsecase
import com.asap.application.user.port.`in`.SocialLoginUsecase
import com.asap.application.user.port.out.AuthInfoRetrievePort
import com.asap.bootstrap.web.auth.api.AuthApi
import com.asap.bootstrap.web.auth.dto.ReissueRequest
import com.asap.bootstrap.web.auth.dto.ReissueResponse
import com.asap.bootstrap.web.auth.dto.SocialLoginRequest
import com.asap.bootstrap.web.auth.dto.SocialLoginResponse
import com.asap.bootstrap.web.auth.dto.*
import com.asap.domain.user.enums.SocialLoginProvider
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RestController
Expand All @@ -15,6 +14,7 @@ import org.springframework.web.bind.annotation.RestController
class AuthController(
private val socialLoginUsecase: SocialLoginUsecase,
private val reissueTokenUsecase: ReissueTokenUsecase,
private val authInfoRetrievePort: AuthInfoRetrievePort,
) : AuthApi {
override fun socialLogin(
provider: String,
Expand Down Expand Up @@ -54,4 +54,19 @@ class AuthController(
refreshToken = response.refreshToken,
)
}

override fun getAccessToken(
provider: String,
request: OAuthAccessTokenRequest,
): OAuthAccessTokenResponse {
val accessToken =
authInfoRetrievePort.getAccessToken(
provider = SocialLoginProvider.valueOf(provider),
code = request.code,
state = request.state,
)
return OAuthAccessTokenResponse(
accessToken = accessToken,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.asap.bootstrap.web.auth.dto

data class OAuthAccessTokenRequest(
val code: String,
val state: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.asap.bootstrap.web.auth.dto

data class OAuthAccessTokenResponse(
val accessToken: String,
)
3 changes: 3 additions & 0 deletions Bootstrap-Module/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ spring:
- security
- aws
- persistence
- client
local:
- security-local
- aws-local
- persistence-local
- client-local
test:
- security-local
- aws-local
- persistence-test
- client-local
active: local


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import com.asap.application.user.port.`in`.LogoutUsecase
import com.asap.application.user.port.`in`.ReissueTokenUsecase
import com.asap.application.user.port.`in`.SocialLoginUsecase
import com.asap.application.user.port.`in`.TokenResolveUsecase
import com.asap.application.user.port.out.AuthInfoRetrievePort
import com.asap.bootstrap.AcceptanceSupporter
import com.asap.bootstrap.web.auth.dto.OAuthAccessTokenRequest
import com.asap.bootstrap.web.auth.dto.ReissueRequest
import com.asap.bootstrap.web.auth.dto.SocialLoginRequest
import com.asap.domain.user.enums.SocialLoginProvider
import org.junit.jupiter.api.Test
import org.mockito.BDDMockito
import org.springframework.boot.test.mock.mockito.MockBean
Expand All @@ -26,6 +29,9 @@ class AuthControllerTest : AcceptanceSupporter() {
@MockBean
private lateinit var logoutUsecase: LogoutUsecase

@MockBean
private lateinit var authInfoRetrievePort: AuthInfoRetrievePort

@Test
fun socialLoginSuccessTest() {
// given
Expand Down Expand Up @@ -112,4 +118,35 @@ class AuthControllerTest : AcceptanceSupporter() {
}
}
}

@Test
fun getAccessTokenTest() {
// given
val provider = "KAKAO"
val code = "authorization_code"
val state = "state"
val request = OAuthAccessTokenRequest(code, state)
val expectedAccessToken = "access_token"

BDDMockito
.given(authInfoRetrievePort.getAccessToken(SocialLoginProvider.valueOf(provider), code, state))
.willReturn(expectedAccessToken)

// when
val response =
mockMvc.post("/api/v1/auth/token/{provider}", provider) {
contentType = MediaType.APPLICATION_JSON
content = objectMapper.writeValueAsString(request)
}

// then
response.andExpect {
status { isOk() }
jsonPath("$.accessToken") {
exists()
isString()
value(expectedAccessToken)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package com.asap.client

import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration

@Configuration
@ComponentScan(basePackages = ["com.asap.client"])
class ClientConfig {
}
@EnableConfigurationProperties(ClientProperties::class)
class ClientConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.asap.client

import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties(prefix = "client")
class ClientProperties(
var oauth: OAuthProperties = OAuthProperties(),
)

class OAuthProperties(
var naver: NaverOAuthProperties = NaverOAuthProperties(),
)

class NaverOAuthProperties(
var clientId: String = "",
var clientSecret: String = "",
)
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,16 @@ class OAuthInfoRetrieveAdapter(
throw UserException.UserAuthNotFoundException("OAuth 정보를 가져오는데 실패했습니다. 에러 메시지: ${e.message}")
}
}

override fun getAccessToken(
provider: SocialLoginProvider,
code: String,
state: String,
): String {
val accessTokenResponse =
oAuthRetrieveHandlers[provider]?.getAccessToken(OAuthRetrieveHandler.OAuthGetAccessTokenRequest(code, state))
?: throw OAuthException.OAuthRetrieveFailedException("OAuth Access Token을 가져오는 핸들러가 존재하지 않습니다.")

return accessTokenResponse.accessToken
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package com.asap.client.oauth
interface OAuthRetrieveHandler {
fun getOAuthInfo(request: OAuthRequest): OAuthResponse

fun getAccessToken(request: OAuthGetAccessTokenRequest): OAuthAccessTokenResponse =
throw UnsupportedOperationException("This operation is not supported yet.")

data class OAuthRequest(
val accessToken: String,
)
Expand All @@ -13,4 +16,14 @@ interface OAuthRetrieveHandler {
val email: String,
val profileImage: String,
)

data class OAuthGetAccessTokenRequest(
val code: String,
val state: String,
)

data class OAuthAccessTokenResponse(
val accessToken: String,
val refreshToken: String?,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,13 @@ class OAuthWebClientConfig {
.baseUrl("https://openapi.naver.com")
.defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.build()

@Bean
@Qualifier("getNaverAccessTokenWebClient")
fun getNaverAccessTokenWebClient(): WebClient =
WebClient
.builder()
.baseUrl("https://nid.naver.com")
.defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.build()
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@ import com.asap.client.oauth.exception.OAuthException
import org.springframework.web.reactive.function.client.WebClient

abstract class AbstractOAuthRetrieveHandler<T>(
private val webClient: WebClient,
protected val webClient: WebClient,
) : OAuthRetrieveHandler {

override fun getOAuthInfo(request: OAuthRetrieveHandler.OAuthRequest): OAuthRetrieveHandler.OAuthResponse {
val response = webClient
.get()
.uri(getApiEndpoint())
.header("Authorization", "Bearer ${request.accessToken}")
.retrieve()
.onStatus({ it.isError }, {
throw OAuthException.OAuthRetrieveFailedException(getErrorMessage())
})
.bodyToMono(getResponseType())
.block()
val response =
webClient
.get()
.uri(getApiEndpoint())
.header("Authorization", "Bearer ${request.accessToken}")
.retrieve()
.onStatus({ it.isError }, {
throw OAuthException.OAuthRetrieveFailedException(getErrorMessage())
})
.bodyToMono(getResponseType())
.block()

if (response == null) {
throw OAuthException.OAuthRetrieveFailedException(getErrorMessage())
Expand Down Expand Up @@ -46,4 +46,4 @@ abstract class AbstractOAuthRetrieveHandler<T>(
* Maps the provider-specific response to a common OAuthResponse
*/
protected abstract fun mapToOAuthResponse(response: T): OAuthRetrieveHandler.OAuthResponse
}
}
Loading