Skip to content

Commit 35016b5

Browse files
authored
Merge pull request #40 from YAPP-Github/fix/PRODUCT-110
[Fix] Oauth Redirect URL을 여러 개를 사용할 수 있도록 수정
2 parents 6df3554 + 9b713db commit 35016b5

File tree

19 files changed

+260
-44
lines changed

19 files changed

+260
-44
lines changed

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

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import org.springframework.util.MultiValueMap;
1010
import org.springframework.web.client.RestClient;
1111
import org.springframework.web.util.UriComponentsBuilder;
12+
import timeeat.exception.BusinessErrorCode;
13+
import timeeat.exception.BusinessException;
1214

1315
@Component
1416
@EnableConfigurationProperties(OauthProperties.class)
@@ -17,27 +19,32 @@ public class OauthClient {
1719
private final RestClient restClient;
1820
private final OauthProperties properties;
1921

20-
public OauthClient(RestClient.Builder restClientBuilder, OauthProperties oauthProperties) {
22+
public OauthClient(RestClient.Builder restClientBuilder,
23+
OauthProperties oauthProperties) {
2124
this.restClient = restClientBuilder
2225
.defaultStatusHandler(HttpStatusCode::is5xxServerError, new OauthServerErrorHandler())
2326
.build();
2427
this.properties = oauthProperties;
2528
}
2629

27-
public URI getOauthLoginUrl() {
30+
public URI getOauthLoginUrl(String origin) {
31+
validateOrigin(origin);
32+
2833
return UriComponentsBuilder.fromUriString("https://kauth.kakao.com/oauth/authorize")
2934
.queryParam("client_id", properties.getClientId())
30-
.queryParam("redirect_uri", properties.getRedirectUri())
35+
.queryParam("redirect_uri", origin + properties.getRedirectPath())
3136
.queryParam("response_type", "code")
3237
.build()
3338
.toUri();
3439
}
3540

36-
public OauthToken requestOauthToken(String code) {
41+
public OauthToken requestOauthToken(String code, String origin) {
42+
validateOrigin(origin);
43+
3744
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
3845
body.add("grant_type", "authorization_code");
3946
body.add("client_id", properties.getClientId());
40-
body.add("redirect_uri", properties.getRedirectUri());
47+
body.add("redirect_uri", origin + properties.getRedirectPath());
4148
body.add("code", code);
4249

4350
return restClient.post()
@@ -48,6 +55,12 @@ public OauthToken requestOauthToken(String code) {
4855
.body(OauthToken.class);
4956
}
5057

58+
private void validateOrigin(String origin) {
59+
if (!properties.isAllowedOrigin(origin)) {
60+
throw new BusinessException(BusinessErrorCode.UNAUTHORIZED_ORIGIN);
61+
}
62+
}
63+
5164
public OauthMemberInformation requestMemberInformation(OauthToken token) {
5265
return restClient.get()
5366
.uri("https://kapi.kakao.com/v2/user/me")
Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,67 @@
11
package timeeat.client.oauth;
22

3+
import java.net.URI;
4+
import java.util.List;
35
import lombok.Getter;
4-
import lombok.RequiredArgsConstructor;
56
import org.springframework.boot.context.properties.ConfigurationProperties;
67

78
@Getter
89
@ConfigurationProperties(prefix = "oauth")
9-
@RequiredArgsConstructor
1010
public class OauthProperties {
1111

1212
private final String clientId;
13-
private final String redirectUri;
13+
private final String redirectPath;
14+
private final List<String> allowedOrigins;
15+
16+
public OauthProperties(String clientId, String redirectPath, List<String> allowedOrigins) {
17+
validateClientId(clientId);
18+
validateRedirectPath(redirectPath);
19+
validateOrigins(allowedOrigins);
20+
21+
this.clientId = clientId;
22+
this.redirectPath = redirectPath;
23+
this.allowedOrigins = allowedOrigins;
24+
}
25+
26+
private void validateClientId(String clientId) {
27+
if (clientId == null || clientId.isBlank()) {
28+
// TODO InitializeException 을 이용
29+
throw new RuntimeException("Client ID must not be null or empty");
30+
}
31+
}
32+
33+
private void validateRedirectPath(String redirectPath) {
34+
if (redirectPath == null || !redirectPath.startsWith("/")) {
35+
// TODO InitializeException 을 이용
36+
throw new RuntimeException("Redirect path must not be null or start with '/'");
37+
}
38+
}
39+
40+
private void validateOrigins(List<String> origins) {
41+
if (origins == null || origins.isEmpty()) {
42+
// TODO InitializeException 을 이용
43+
throw new RuntimeException("Allowed origins must not be null or empty");
44+
}
45+
origins.forEach(this::validateOrigin);
46+
}
47+
48+
private void validateOrigin(String origin) {
49+
URI uri;
50+
try {
51+
uri = new URI(origin);
52+
} catch (Exception e) {
53+
// TODO InitializeException 을 이용
54+
throw new RuntimeException("Allowed origin must be a valid origin form: " + origin, e);
55+
}
56+
57+
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);
60+
}
61+
}
62+
63+
public boolean isAllowedOrigin(String origin) {
64+
return allowedOrigins.stream()
65+
.anyMatch(allowedOrigin -> allowedOrigin.equalsIgnoreCase(origin.trim()));
66+
}
1467
}

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
import java.net.URI;
44
import lombok.RequiredArgsConstructor;
5+
import org.springframework.http.HttpHeaders;
56
import org.springframework.http.HttpStatus;
67
import org.springframework.http.ResponseEntity;
78
import org.springframework.web.bind.annotation.GetMapping;
89
import org.springframework.web.bind.annotation.PostMapping;
910
import org.springframework.web.bind.annotation.RequestBody;
11+
import org.springframework.web.bind.annotation.RequestHeader;
1012
import org.springframework.web.bind.annotation.RestController;
1113
import timeeat.controller.member.MemberResponse;
1214
import timeeat.controller.web.jwt.JwtManager;
@@ -20,18 +22,20 @@ public class AuthController {
2022
private final AuthService authService;
2123

2224
@GetMapping("/api/auth/login/oauth")
23-
public ResponseEntity<Void> redirectOauthLoginPage() {
24-
URI oauthLoginUrl = authService.getOauthLoginUrl();
25+
public ResponseEntity<Void> redirectOauthLoginPage(@RequestHeader(HttpHeaders.ORIGIN) String origin) {
26+
URI oauthLoginUrl = authService.getOauthLoginUrl(origin);
2527

2628
return ResponseEntity
2729
.status(HttpStatus.FOUND)
2830
.location(oauthLoginUrl)
2931
.build();
3032
}
3133

34+
// TODO : login() ControllerTest, DocumentTest 수정
3235
@PostMapping("/api/auth/login")
33-
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {
34-
MemberResponse member = authService.login(request);
36+
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request,
37+
@RequestHeader(HttpHeaders.ORIGIN) String origin) {
38+
MemberResponse member = authService.login(request, origin);
3539
TokenResponse token = new TokenResponse(
3640
jwtManager.issueAccessToken(member.id()),
3741
jwtManager.issueRefreshToken(member.id()));

src/main/java/timeeat/exception/BusinessErrorCode.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public enum BusinessErrorCode {
4444
// Auth
4545
UNAUTHORIZED_MEMBER("AUTH001", "인증되지 않은 회원입니다.", HttpStatus.UNAUTHORIZED),
4646
EXPIRED_TOKEN("AUTH002", "이미 만료된 토큰입니다.", HttpStatus.UNAUTHORIZED),
47+
UNAUTHORIZED_ORIGIN("AUTH003", "허용되지 않은 오리진입니다."),
4748
OAUTH_SERVER_ERROR("AUTH003", "OAuth 서버와의 통신 오류입니다.", HttpStatus.INTERNAL_SERVER_ERROR),
4849
;
4950

src/main/java/timeeat/exception/EtcErrorCode.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public enum EtcErrorCode {
1313
MEDIA_TYPE_NOT_SUPPORTED("CLIENT005", "허용되지 않은 미디어 타입입니다.", HttpStatus.UNSUPPORTED_MEDIA_TYPE),
1414
NO_RESOURCE_FOUND("CLIENT006", "요청한 리소스를 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
1515
NO_COOKIE_FOUND("CLIENT007", "필수 쿠키 값이 존재하지 않습니다.", HttpStatus.BAD_REQUEST),
16+
NO_HEADER_FOUND("CLIENT007", "필수 헤더 값이 존재하지 않습니다.", HttpStatus.BAD_REQUEST),
1617

1718
INTERNAL_SERVER_ERROR("SERVER001", "서버 내부 에러가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR),
1819
;

src/main/java/timeeat/exception/GlobalExceptionHandler.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.springframework.web.HttpMediaTypeNotSupportedException;
99
import org.springframework.web.HttpRequestMethodNotSupportedException;
1010
import org.springframework.web.bind.MissingRequestCookieException;
11+
import org.springframework.web.bind.MissingRequestHeaderException;
1112
import org.springframework.web.bind.annotation.ExceptionHandler;
1213
import org.springframework.web.bind.annotation.RestControllerAdvice;
1314
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
@@ -62,6 +63,11 @@ public ResponseEntity<ErrorResponse> handleMissingRequestCookieException(Missing
6263
return toErrorResponse(EtcErrorCode.NO_COOKIE_FOUND);
6364
}
6465

66+
@ExceptionHandler(MissingRequestHeaderException.class)
67+
public ResponseEntity<ErrorResponse> handleMissingRequestHeaderException(MissingRequestHeaderException exception) {
68+
return toErrorResponse(EtcErrorCode.NO_HEADER_FOUND);
69+
}
70+
6571
@ExceptionHandler(BusinessException.class)
6672
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException exception) {
6773
ErrorResponse response = new ErrorResponse(exception.getErrorCode());

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ public class AuthService {
2222
private final OauthClient oauthClient;
2323
private final MemberRepository memberRepository;
2424

25-
public URI getOauthLoginUrl() {
26-
return oauthClient.getOauthLoginUrl();
25+
public URI getOauthLoginUrl(String origin) {
26+
return oauthClient.getOauthLoginUrl(origin);
2727
}
2828

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

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

src/main/resources/application-dev.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,18 @@ spring:
2828
- classpath:db/seed/dev
2929

3030
jwt:
31+
access-token-expiration: 1h
32+
refresh-token-expiration: 14d
3133
secret-key: ${JWT_SECRET_KEY}
3234

3335
cors:
3436
origin:
3537
- "http://localhost:3000"
3638
- "https://api-dev.eatda.net"
39+
40+
oauth:
41+
client-id: ${OAUTH_CLIENT_ID}
42+
redirect-path: /login/callback
43+
allowed-origins:
44+
- "http://localhost:3000"
45+
- "https://dev.eatda.net"

src/main/resources/application-local.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,16 @@ spring:
2121
- classpath:db/seed/local
2222

2323
jwt:
24+
access-token-expiration: 1h
25+
refresh-token-expiration: 1d
2426
secret-key: ${LOCAL_JWT_SECRET_KEY}
2527

2628
cors:
2729
origin:
2830
- "http://localhost:3000"
31+
32+
oauth:
33+
client-id: ${OAUTH_CLIENT_ID}
34+
redirect-path: /login/callback
35+
allowed-origins:
36+
- "http://localhost:3000"

src/main/resources/application-prod.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,18 @@ spring:
2828
- classpath:db/seed/prod
2929

3030
jwt:
31+
access-token-expiration: 1h
32+
refresh-token-expiration: 14d
3133
secret-key: ${JWT_SECRET_KEY}
3234

3335
cors:
3436
origin:
3537
- "https://www.eatda.net"
3638
- "https://eatda.net"
39+
40+
oauth:
41+
client-id: ${OAUTH_CLIENT_ID}
42+
redirect-path: /login/callback
43+
allowed-origins:
44+
- "https://www.eatda.net"
45+
- "https://eatda.net"

0 commit comments

Comments
 (0)