Skip to content

Commit 93b1cce

Browse files
authored
[YS-456] feat: OAuth 로그인 API의 redirect URI 동적 처리 (#140)
* feat: add InvalidRedirectUriException for GoogleOauthLogin API * feat: add redirectUri field to GoogleOauthLoginRequest * feat: add RedirectUrivalidator to ensure valid OAuth callback URLs * feat: update Google OAuth to use redirect URI from request body * fix: update test to handle additional parameters * chore: add Google OAuth redirectUris configuration * style: apply ktlint formatting fixes * style: apply ktlint formatting fixes * style: format code according to local ktlint rules
1 parent d276663 commit 93b1cce

File tree

13 files changed

+115
-30
lines changed

13 files changed

+115
-30
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.dobby.auth
2+
3+
import org.springframework.boot.context.properties.ConfigurationProperties
4+
import org.springframework.stereotype.Component
5+
6+
@Component
7+
@ConfigurationProperties(prefix = "oauth.google")
8+
data class GoogleRedirectUriProperties(
9+
var redirectUris: List<String> = emptyList()
10+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.dobby.auth
2+
3+
import org.springframework.stereotype.Component
4+
5+
@Component
6+
class RedirectUriValidator(
7+
private val properties: GoogleRedirectUriProperties
8+
) {
9+
fun isValidGoogleRedirectUri(uri: String): Boolean = properties.redirectUris.contains(uri)
10+
}

application/src/main/kotlin/com/dobby/service/AuthService.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.dobby.service
22

3+
import com.dobby.auth.RedirectUriValidator
4+
import com.dobby.exception.InvalidRedirectUriException
35
import com.dobby.usecase.auth.FetchGoogleUserInfoUseCase
46
import com.dobby.usecase.auth.FetchNaverUserInfoUseCase
57
import com.dobby.usecase.auth.GenerateTestTokenUseCase
@@ -12,12 +14,20 @@ class AuthService(
1214
private val fetchGoogleUserInfoUseCase: FetchGoogleUserInfoUseCase,
1315
private val fetchNaverUserInfoUseCase: FetchNaverUserInfoUseCase,
1416
private val generateTokenWithRefreshTokenUseCase: GenerateTokenWithRefreshTokenUseCase,
15-
private val generateTestTokenUseCase: GenerateTestTokenUseCase
17+
private val generateTestTokenUseCase: GenerateTestTokenUseCase,
18+
private val redirectUriValidator: RedirectUriValidator
1619
) {
1720
fun getGoogleUserInfo(input: FetchGoogleUserInfoUseCase.Input): FetchGoogleUserInfoUseCase.Output {
21+
validateGoogleRedirectUri(input.redirectUri)
1822
return fetchGoogleUserInfoUseCase.execute(input)
1923
}
2024

25+
private fun validateGoogleRedirectUri(uri: String) {
26+
if (!redirectUriValidator.isValidGoogleRedirectUri(uri)) {
27+
throw InvalidRedirectUriException
28+
}
29+
}
30+
2131
fun getNaverUserInfo(input: FetchNaverUserInfoUseCase.Input): FetchNaverUserInfoUseCase.Output {
2232
return fetchNaverUserInfoUseCase.execute(input)
2333
}

application/src/main/kotlin/com/dobby/usecase/auth/FetchGoogleUserInfoUseCase.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ class FetchGoogleUserInfoUseCase(
1717

1818
data class Input(
1919
val authorizationCode: String,
20-
val role: RoleType
20+
val role: RoleType,
21+
val redirectUri: String
2122
)
2223

2324
data class Output(
@@ -33,7 +34,7 @@ class FetchGoogleUserInfoUseCase(
3334
)
3435

3536
override fun execute(input: Input): Output {
36-
val oauthToken = googleAuthGateway.getAccessToken(input.authorizationCode).accessToken
37+
val oauthToken = googleAuthGateway.getAccessToken(input.authorizationCode, input.redirectUri).accessToken
3738
val userInfo = googleAuthGateway.getUserInfo(oauthToken)
3839
val email = userInfo.email
3940
val member = email.let { memberGateway.findByOauthEmailAndStatus(email, MemberStatus.ACTIVE) }

application/src/test/kotlin/com/dobby/usecase/auth/FetchGoogleUserInfoUseCaseTest.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ class FetchGoogleUserInfoUseCaseTest : BehaviorSpec({
2828
)
2929

3030
given("Google OAuth 요청이 들어왔을 때") {
31-
val input = FetchGoogleUserInfoUseCase.Input(authorizationCode = "valid-auth-code", role = RoleType.PARTICIPANT)
31+
val input = FetchGoogleUserInfoUseCase.Input(
32+
authorizationCode = "valid-auth-code",
33+
role = RoleType.PARTICIPANT,
34+
redirectUri = "http://localhost:8080/auth/google/callback"
35+
)
3236
val mockMember = Member(
3337
id = "1",
3438
oauthEmail = "test@example.com",
@@ -44,7 +48,7 @@ class FetchGoogleUserInfoUseCaseTest : BehaviorSpec({
4448

4549
val mockEmptyMember = null
4650
val mockGoogleTokenResponse = GoogleToken("mock-access-token")
47-
every { googleAuthGateway.getAccessToken(any()) } returns mockGoogleTokenResponse
51+
every { googleAuthGateway.getAccessToken(any(), any()) } returns mockGoogleTokenResponse
4852
every { googleAuthGateway.getUserInfo("mock-access-token") } returns mockk {
4953
every { email } returns "test@example.com"
5054
}
@@ -68,7 +72,12 @@ class FetchGoogleUserInfoUseCaseTest : BehaviorSpec({
6872
}
6973

7074
// 테스트 2: 등록된 멤버가 없는 경우
71-
every { memberGateway.findByOauthEmailAndStatus("test@example.com", MemberStatus.ACTIVE) } returns mockEmptyMember
75+
every {
76+
memberGateway.findByOauthEmailAndStatus(
77+
"test@example.com",
78+
MemberStatus.ACTIVE
79+
)
80+
} returns mockEmptyMember
7281

7382
`when`("등록되지 않은 유저가 있는 경우") {
7483
val result: FetchGoogleUserInfoUseCase.Output = fetchGoogleUserInfoUseCase.execute(input)

domain/src/main/kotlin/com/dobby/exception/DobbyException.kt

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ package com.dobby.exception
33
/**
44
* Top-level exception class for Dobby application
55
*/
6-
sealed class DobbyException(val code: String, message: String, cause: Throwable? = null) : RuntimeException(message, cause)
6+
sealed class DobbyException(val code: String, message: String, cause: Throwable? = null) :
7+
RuntimeException(message, cause)
78

89
/**
910
* ClientException: Exceptions caused by invalid client requests
1011
* - Authentication and authorization failures
1112
* - Invalid or missing request data
1213
*/
13-
sealed class ClientException(code: String, message: String, cause: Throwable? = null) : DobbyException(code, message, cause)
14+
sealed class ClientException(code: String, message: String, cause: Throwable? = null) :
15+
DobbyException(code, message, cause)
1416

1517
/**
1618
* Common error codes
@@ -27,6 +29,7 @@ data object AuthenticationTokenNotValidException : ClientException("AU0002", "Au
2729
data object AuthenticationTokenExpiredException : ClientException("AU0003", "Authentication token has expired.")
2830
data object InvalidTokenTypeException : ClientException("AU0004", "Invalid token type")
2931
data object InvalidTokenValueException : ClientException("AU0005", "Invalid token value")
32+
data object InvalidRedirectUriException : ClientException("AU0006", "Invalid redirect URI")
3033

3134
/**
3235
* Email Verification error codes
@@ -38,31 +41,47 @@ data object CodeNotCorrectException : ClientException("VE0004", "Verification co
3841
data object CodeExpiredException : ClientException("VE0005", "Verification code is expired")
3942
data object EmailFormatInvalidException : ClientException("VE0006", "Email is invalid format")
4043
data object EmailAlreadyVerifiedException : ClientException("VE0007", "This email is already verified.")
41-
data object TooManyVerificationRequestException : ClientException("VE0008", "You can request verification up to 3 in one day.")
44+
data object TooManyVerificationRequestException :
45+
ClientException("VE0008", "You can request verification up to 3 in one day.")
4246

4347
/**
4448
* Member error codes
4549
*/
4650
data object MemberNotFoundException : ClientException("ME0001", "Member not found")
47-
data class MemberRoleMismatchException(val role: String) : ClientException("ME0002", "Already registered as another role: $role")
51+
data class MemberRoleMismatchException(val role: String) :
52+
ClientException("ME0002", "Already registered as another role: $role")
53+
4854
data object ResearcherNotFoundException : ClientException("ME0003", "Researcher Not Found.")
4955
data object ParticipantNotFoundException : ClientException("ME0004", "Participant Not Found.")
5056
data object EmailNotValidateException : ClientException("ME0005", "You should validate your school email first")
51-
data object SignupOauthEmailDuplicateException : ClientException("ME0006", "You've already joined with requested OAuth email")
57+
data object SignupOauthEmailDuplicateException :
58+
ClientException("ME0006", "You've already joined with requested OAuth email")
59+
5260
data object ContactEmailDuplicateException : ClientException("ME0007", "This contact email is already in use.")
5361
data object MemberConsentNotFoundException : ClientException("ME0008", "Member Consent Not Found.")
54-
data object SignupUnivEmailDuplicateException : ClientException("ME0009", "You've already joined with requested university email")
62+
data object SignupUnivEmailDuplicateException :
63+
ClientException("ME0009", "You've already joined with requested university email")
5564

5665
/**
5766
* Experiment error codes
5867
*/
5968
data object ExperimentPostNotFoundException : ClientException("EP0001", "Experiment Post Not Found.")
60-
data object ExperimentAreaOverflowException : ClientException("EP0003", "You can only select up to 5 Area options in 1 Region.")
61-
data object ExperimentAreaInCorrectException : ClientException("EP0004", "Selected Area doesn't belong to correct Region.")
69+
data object ExperimentAreaOverflowException :
70+
ClientException("EP0003", "You can only select up to 5 Area options in 1 Region.")
71+
72+
data object ExperimentAreaInCorrectException :
73+
ClientException("EP0004", "Selected Area doesn't belong to correct Region.")
74+
6275
data object ExperimentPostImageSizeException : ClientException("EP0005", "Image can be uploaded maximum 3 images.")
63-
data object ExperimentPostRecruitStatusException : ClientException("EP0006", "You cannot update recruitStatus with closed recruitment post.")
64-
data object ExperimentPostUpdateDateException : ClientException("EP0007", "You cannot update startDate, endDate with closed recruitment post.")
65-
data object ExperimentPostInvalidOnlineRequestException : ClientException("EP0008", "place, region, area field value must be null when MatchType is online.")
76+
data object ExperimentPostRecruitStatusException :
77+
ClientException("EP0006", "You cannot update recruitStatus with closed recruitment post.")
78+
79+
data object ExperimentPostUpdateDateException :
80+
ClientException("EP0007", "You cannot update startDate, endDate with closed recruitment post.")
81+
82+
data object ExperimentPostInvalidOnlineRequestException :
83+
ClientException("EP0008", "place, region, area field value must be null when MatchType is online.")
84+
6685
data object ExperimentPostTitleException : ClientException("EP0009", "Title cannot be null.")
6786
data object ExperimentPostRewardException : ClientException("EP0010", "Reward cannot be null.")
6887
data object ExperimentPostContentException : ClientException("EP0011", "Content cannot be null.")
@@ -74,6 +93,7 @@ data object ExperimentPostLeadResearcherException : ClientException("EP0013", "L
7493
* - Database or infrastructure failures
7594
* - Unexpected backend processing issues
7695
*/
77-
sealed class ServerException(code: String, message: String, cause: Throwable? = null) : DobbyException(code, message, cause)
96+
sealed class ServerException(code: String, message: String, cause: Throwable? = null) :
97+
DobbyException(code, message, cause)
7898

7999
data object UnknownServerErrorException : ServerException("DB0001", "An unknown error has occurred")

domain/src/main/kotlin/com/dobby/gateway/auth/GoogleAuthGateway.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import com.dobby.model.auth.GoogleToken
44
import com.dobby.model.auth.GoogleUserInfo
55

66
interface GoogleAuthGateway {
7-
fun getAccessToken(code: String): GoogleToken
7+
8+
fun getAccessToken(
9+
code: String,
10+
redirectUri: String
11+
): GoogleToken
12+
813
fun getUserInfo(accessToken: String): GoogleUserInfo
914
}

infrastructure/src/main/kotlin/com/dobby/external/gateway/auth/GoogleAuthGatewayImpl.kt

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@ class GoogleAuthGatewayImpl(
1515
private val googleUserInfoFeignClient: GoogleUserInfoFeginClient
1616
) : GoogleAuthGateway {
1717

18-
override fun getAccessToken(code: String): GoogleToken {
19-
return googleAuthFeignClient.getAccessToken(
20-
clientId = googleAuthProperties.clientId,
21-
redirectUri = googleAuthProperties.redirectUri,
22-
code = code,
23-
clientSecret = googleAuthProperties.clientSecret,
24-
grantType = googleAuthProperties.grantType
25-
).toDomain()
26-
}
18+
override fun getAccessToken(
19+
code: String,
20+
redirectUri: String
21+
): GoogleToken = googleAuthFeignClient.getAccessToken(
22+
clientId = googleAuthProperties.clientId,
23+
redirectUri = redirectUri,
24+
code = code,
25+
clientSecret = googleAuthProperties.clientSecret,
26+
grantType = googleAuthProperties.grantType
27+
).toDomain()
2728

2829
override fun getUserInfo(accessToken: String): GoogleUserInfo {
2930
return googleUserInfoFeignClient.getUserInfo("Bearer $accessToken")

infrastructure/src/main/resources/application.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,8 @@ cors:
8787

8888
swagger:
8989
server-url: http://localhost:8080
90+
91+
oauth:
92+
google:
93+
redirectUris:
94+
- "http://localhost:8080/oauth2/callback/google"

infrastructure/src/main/resources/template-application-local.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,8 @@ cors:
9090

9191
swagger:
9292
server-url: http://localhost:8080
93+
94+
oauth:
95+
google:
96+
redirectUris:
97+
- "http://localhost:8080/oauth2/callback/google"

0 commit comments

Comments
 (0)