Skip to content

Commit b76d045

Browse files
authored
[Fix] 요청한 클라이언트의 주소를 Origin 대신 Refer, Body 로 받도록 수정
2 parents a508457 + 016ba83 commit b76d045

File tree

11 files changed

+64
-56
lines changed

11 files changed

+64
-56
lines changed

src/main/java/timeeat/client/oauth/OauthClient.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public URI getOauthLoginUrl(String origin) {
3232

3333
return UriComponentsBuilder.fromUriString("https://kauth.kakao.com/oauth/authorize")
3434
.queryParam("client_id", properties.getClientId())
35-
.queryParam("redirect_uri", origin + properties.getRedirectPath())
35+
.queryParam("redirect_uri", createRedirectUri(origin))
3636
.queryParam("response_type", "code")
3737
.build()
3838
.toUri();
@@ -44,7 +44,7 @@ public OauthToken requestOauthToken(String code, String origin) {
4444
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
4545
body.add("grant_type", "authorization_code");
4646
body.add("client_id", properties.getClientId());
47-
body.add("redirect_uri", origin + properties.getRedirectPath());
47+
body.add("redirect_uri", createRedirectUri(origin));
4848
body.add("code", code);
4949

5050
return restClient.post()
@@ -61,6 +61,13 @@ private void validateOrigin(String origin) {
6161
}
6262
}
6363

64+
private String createRedirectUri(String origin) {
65+
return UriComponentsBuilder.fromUriString(origin)
66+
.path(properties.getRedirectPath())
67+
.build()
68+
.toString();
69+
}
70+
6471
public OauthMemberInformation requestMemberInformation(OauthToken token) {
6572
return restClient.get()
6673
.uri("https://kapi.kakao.com/v2/user/me")

src/main/java/timeeat/client/oauth/OauthProperties.java

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.util.List;
55
import lombok.Getter;
66
import org.springframework.boot.context.properties.ConfigurationProperties;
7+
import timeeat.exception.InitializeException;
78

89
@Getter
910
@ConfigurationProperties(prefix = "oauth")
@@ -25,22 +26,19 @@ public OauthProperties(String clientId, String redirectPath, List<String> allowe
2526

2627
private void validateClientId(String clientId) {
2728
if (clientId == null || clientId.isBlank()) {
28-
// TODO InitializeException 을 이용
29-
throw new RuntimeException("Client ID must not be null or empty");
29+
throw new InitializeException("Client ID must not be null or empty");
3030
}
3131
}
3232

3333
private void validateRedirectPath(String redirectPath) {
3434
if (redirectPath == null || !redirectPath.startsWith("/")) {
35-
// TODO InitializeException 을 이용
36-
throw new RuntimeException("Redirect path must not be null or start with '/'");
35+
throw new InitializeException("Redirect path must not be null or start with '/'");
3736
}
3837
}
3938

4039
private void validateOrigins(List<String> origins) {
4140
if (origins == null || origins.isEmpty()) {
42-
// TODO InitializeException 을 이용
43-
throw new RuntimeException("Allowed origins must not be null or empty");
41+
throw new InitializeException("Allowed origins must not be null or empty");
4442
}
4543
origins.forEach(this::validateOrigin);
4644
}
@@ -50,18 +48,21 @@ private void validateOrigin(String origin) {
5048
try {
5149
uri = new URI(origin);
5250
} catch (Exception e) {
53-
// TODO InitializeException 을 이용
54-
throw new RuntimeException("Allowed origin must be a valid origin form: " + origin, e);
51+
throw new InitializeException("Allowed origin must be a valid origin form: " + origin, e);
5552
}
5653

5754
if (uri.getScheme() == null || uri.getHost() == null || !uri.getPath().isBlank()) {
58-
// TODO InitializeException 을 이용
59-
throw new RuntimeException("Allowed origin must be a valid origin form: " + origin);
55+
throw new InitializeException("Allowed origin must be a valid origin form: " + origin);
6056
}
6157
}
6258

6359
public boolean isAllowedOrigin(String origin) {
6460
return allowedOrigins.stream()
65-
.anyMatch(allowedOrigin -> allowedOrigin.equalsIgnoreCase(origin.trim()));
61+
.anyMatch(allowedOrigin -> isMatchedOrigin(allowedOrigin, origin));
62+
}
63+
64+
private boolean isMatchedOrigin(String allowedOrigin, String origin) {
65+
return origin.trim().equals(allowedOrigin)
66+
|| origin.trim().equals(allowedOrigin + "/");
6667
}
6768
}

src/main/java/timeeat/controller/auth/AuthController.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public class AuthController {
2222
private final AuthService authService;
2323

2424
@GetMapping("/api/auth/login/oauth")
25-
public ResponseEntity<Void> redirectOauthLoginPage(@RequestHeader(HttpHeaders.ORIGIN) String origin) {
25+
public ResponseEntity<Void> redirectOauthLoginPage(@RequestHeader(HttpHeaders.REFERER) String origin) {
2626
URI oauthLoginUrl = authService.getOauthLoginUrl(origin);
2727

2828
return ResponseEntity
@@ -32,9 +32,8 @@ public ResponseEntity<Void> redirectOauthLoginPage(@RequestHeader(HttpHeaders.OR
3232
}
3333

3434
@PostMapping("/api/auth/login")
35-
public ResponseEntity<LoginResponse> login(@RequestHeader(HttpHeaders.ORIGIN) String origin,
36-
@RequestBody LoginRequest request) {
37-
MemberResponse member = authService.login(request, origin);
35+
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {
36+
MemberResponse member = authService.login(request);
3837
TokenResponse token = new TokenResponse(
3938
jwtManager.issueAccessToken(member.id()),
4039
jwtManager.issueRefreshToken(member.id()));
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
package timeeat.controller.auth;
22

3-
public record LoginRequest(String code) {
3+
public record LoginRequest(String code, String origin) {
44
}

src/main/java/timeeat/exception/InitializeException.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,8 @@ public class InitializeException extends RuntimeException {
55
public InitializeException(String message) {
66
super(message);
77
}
8+
9+
public InitializeException(String message, Throwable cause) {
10+
super(message, cause);
11+
}
812
}

src/main/java/timeeat/service/auth/AuthService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ public URI getOauthLoginUrl(String origin) {
2727
}
2828

2929
@Transactional
30-
public MemberResponse login(LoginRequest request, String origin) {
31-
OauthToken oauthToken = oauthClient.requestOauthToken(request.code(), origin);
30+
public MemberResponse login(LoginRequest request) {
31+
OauthToken oauthToken = oauthClient.requestOauthToken(request.code(), request.origin());
3232
OauthMemberInformation oauthInformation = oauthClient.requestMemberInformation(oauthToken);
3333

3434
Optional<Member> optionalMember = memberRepository.findBySocialId(Long.toString(oauthInformation.socialId()));

src/test/java/timeeat/client/oauth/OauthClientTest.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ class GetOauthLoginUrl {
4949
assertAll(
5050
() -> assertThat(uri.getHost()).isEqualTo("kauth.kakao.com"),
5151
() -> assertThat(uri.getPath()).isEqualTo("/oauth/authorize"),
52-
() -> assertThat(uri.getQuery()).contains("client_id=%s".formatted(properties.getClientId())),
53-
() -> assertThat(uri.getQuery()).contains("redirect_uri=%s".formatted(redirectUri)),
52+
() -> assertThat(uri.getQuery()).contains("client_id=%s&".formatted(properties.getClientId())),
53+
() -> assertThat(uri.getQuery()).contains("redirect_uri=%s&".formatted(redirectUri)),
5454
() -> assertThat(uri.getQuery()).contains("response_type=code")
5555
);
5656
}
@@ -64,6 +64,16 @@ class GetOauthLoginUrl {
6464

6565
assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.UNAUTHORIZED_ORIGIN);
6666
}
67+
68+
@Test
69+
void origin_뒤에_슬래시가_붙어도_정상적으로_처리된다() {
70+
String origin = properties.getAllowedOrigins().getFirst();
71+
String redirectUri = origin + properties.getRedirectPath();
72+
73+
URI uri = oauthClient.getOauthLoginUrl(origin + "/");
74+
75+
assertThat(uri.getQuery()).contains("redirect_uri=%s&".formatted(redirectUri));
76+
}
6777
}
6878

6979
@Nested

src/test/java/timeeat/client/oauth/OauthPropertiesTest.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55

66
import java.util.List;
77
import org.junit.jupiter.api.Nested;
8-
import org.junit.jupiter.api.Test;
98
import org.junit.jupiter.params.ParameterizedTest;
109
import org.junit.jupiter.params.provider.NullAndEmptySource;
1110
import org.junit.jupiter.params.provider.ValueSource;
11+
import timeeat.exception.InitializeException;
1212

1313
class OauthPropertiesTest {
1414

@@ -19,23 +19,23 @@ class Validate {
1919
@NullAndEmptySource
2020
void 클라이언트_아이디가_비어있는_경우_예외를_던진다(String clientId) {
2121
assertThatThrownBy(() -> new OauthProperties(clientId, "/path", List.of("http://localhost:8080")))
22-
.isInstanceOf(RuntimeException.class)
22+
.isInstanceOf(InitializeException.class)
2323
.hasMessage("Client ID must not be null or empty");
2424
}
2525

2626
@ParameterizedTest
2727
@ValueSource(strings = {"path", ".path", "path/", ""})
2828
void 리다이렉트_경로가_경로_형식이_아닌_경우_예외를_던진다(String redirectPath) {
2929
assertThatThrownBy(() -> new OauthProperties("client-id", redirectPath, List.of("http://localhost:8080")))
30-
.isInstanceOf(RuntimeException.class)
30+
.isInstanceOf(InitializeException.class)
3131
.hasMessage("Redirect path must not be null or start with '/'");
3232
}
3333

3434
@ParameterizedTest
3535
@ValueSource(strings = {"invalid-url", "http://", "http://:8080", "http://localhost:8080/path", " "})
3636
void 허용된_오리진이_유효하지_않은_URL인_경우_예외를_던진다(String origin) {
3737
assertThatThrownBy(() -> new OauthProperties("client-id", "/path", List.of(origin)))
38-
.isInstanceOf(RuntimeException.class)
38+
.isInstanceOf(InitializeException.class)
3939
.hasMessageContaining("Allowed origin must be a valid origin form");
4040
}
4141
}
@@ -44,7 +44,8 @@ class Validate {
4444
class IsAllowedOrigin {
4545

4646
@ParameterizedTest
47-
@ValueSource(strings = {"http://localhost:8080", " http://localhost:8080 ", "https://example.com"})
47+
@ValueSource(strings = {"http://localhost:8080", " http://localhost:8080 ",
48+
"https://example.com", "https://example.com/"})
4849
void 허용된_오리진인_경우_true를_반환한다(String allowedOrigin) {
4950
List<String> origins = List.of("http://localhost:8080", "https://example.com");
5051
OauthProperties oauthProperties = new OauthProperties("client-id", "/path", origins);
@@ -54,7 +55,8 @@ class IsAllowedOrigin {
5455
assertThat(isAllowed).isTrue();
5556
}
5657

57-
@Test
58+
@ParameterizedTest
59+
@ValueSource(strings = {"https://not-allowed.com", "http://localhost:8080/path", "http://localhost:8080nono"})
5860
void 허용되지_않은_오리진인_경우_false를_반환한다() {
5961
OauthProperties oauthProperties = new OauthProperties("client-id", "/path",
6062
List.of("http://localhost:8080"));

src/test/java/timeeat/controller/auth/AuthControllerTest.java

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,6 @@
1212

1313
class AuthControllerTest extends BaseControllerTest {
1414

15-
@Value("${oauth.client-id}")
16-
private String clientId;
17-
18-
@Value("${oauth.redirect-path}")
19-
private String redirectPath;
20-
2115
@Value("${oauth.allowed-origins[0]}")
2216
private String allowedOrigin;
2317

@@ -26,11 +20,8 @@ class RedirectOauthLoginPage {
2620

2721
@Test
2822
void Oauth_로그인_페이지로_리다이렉트_할_수_있다() {
29-
String origin = allowedOrigin;
30-
String expectedRedirectPath = origin + redirectPath;
31-
3223
String location = given()
33-
.header(HttpHeaders.ORIGIN, origin)
24+
.header(HttpHeaders.REFERER, allowedOrigin)
3425
.redirects().follow(false)
3526
.when()
3627
.get("/api/auth/login/oauth")
@@ -47,10 +38,9 @@ class Login {
4738

4839
@Test
4940
void 인가코드를_통해_회원가입할_수_있다() {
50-
LoginRequest request = new LoginRequest("auth-code");
41+
LoginRequest request = new LoginRequest("auth-code", allowedOrigin);
5142

5243
LoginResponse response = given().body(request)
53-
.header(HttpHeaders.ORIGIN, allowedOrigin)
5444
.contentType(ContentType.JSON)
5545
.when().post("/api/auth/login")
5646
.then()
@@ -67,10 +57,9 @@ class Login {
6757
@Test
6858
void 인가코드를_통해_로그인할_수_있다() {
6959
memberGenerator.generate(oauthLoginSocialId());
70-
LoginRequest request = new LoginRequest("auth-code");
60+
LoginRequest request = new LoginRequest("auth-code", allowedOrigin);
7161

7262
LoginResponse response = given().body(request)
73-
.header(HttpHeaders.ORIGIN, allowedOrigin)
7463
.contentType(ContentType.JSON)
7564
.when().post("/api/auth/login")
7665
.then()

src/test/java/timeeat/document/auth/AuthDocumentTest.java

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package timeeat.document.auth;
22

33
import static org.mockito.ArgumentMatchers.anyString;
4-
import static org.mockito.ArgumentMatchers.eq;
54
import static org.mockito.Mockito.doReturn;
65
import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName;
76
import static org.springframework.restdocs.payload.JsonFieldType.BOOLEAN;
@@ -37,7 +36,7 @@ class RedirectOauthLoginPage {
3736
.tag(Tag.AUTH_API)
3837
.summary("OAuth 로그인 페이지 리다이렉트")
3938
.requestHeader(
40-
headerWithName(HttpHeaders.ORIGIN).description("요청 Origin")
39+
headerWithName(HttpHeaders.REFERER).description("요청 Origin")
4140
);
4241

4342
RestDocsResponse responseDocument = response()
@@ -56,7 +55,7 @@ class RedirectOauthLoginPage {
5655

5756
given(document)
5857
.redirects().follow(false)
59-
.header(HttpHeaders.ORIGIN, origin)
58+
.header(HttpHeaders.REFERER, origin)
6059
.when()
6160
.get("/api/auth/login/oauth")
6261
.then()
@@ -70,11 +69,9 @@ class Login {
7069
RestDocsRequest requestDocument = request()
7170
.tag(Tag.AUTH_API)
7271
.summary("로그인")
73-
.requestHeader(
74-
headerWithName(HttpHeaders.ORIGIN).description("요청 Origin")
75-
)
7672
.requestBodyField(
77-
fieldWithPath("code").type(STRING).description("Oauth 인가 코드")
73+
fieldWithPath("code").type(STRING).description("Oauth 인가 코드"),
74+
fieldWithPath("origin").type(STRING).description("요청 Origin")
7875
);
7976

8077
RestDocsResponse responseDocument = response()
@@ -93,9 +90,9 @@ class Login {
9390

9491
@Test
9592
void 로그인_성공() {
96-
LoginRequest request = new LoginRequest("code");
93+
LoginRequest request = new LoginRequest("code", "http://localhost:3000");
9794
MemberResponse response = new MemberResponse(1L, true, "닉네임", null, null, null);
98-
doReturn(response).when(authService).login(eq(request), anyString());
95+
doReturn(response).when(authService).login(request);
9996

10097
var document = document("auth/login", 201)
10198
.request(requestDocument)
@@ -104,7 +101,6 @@ class Login {
104101

105102
given(document)
106103
.contentType(ContentType.JSON)
107-
.header(HttpHeaders.ORIGIN, origin)
108104
.body(request)
109105
.when().post("/api/auth/login")
110106
.then().statusCode(201);

0 commit comments

Comments
 (0)