From f1b501d853b052c5953bee44d45817018d551ed1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A3=BC=EC=84=B1?= <32183520@dankook.ac.kr> Date: Sat, 13 Jan 2024 13:17:59 +0900 Subject: [PATCH 01/34] =?UTF-8?q?[BE]=20refactor(#31):=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=ED=98=95=EC=8B=9D=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/backend/auth/api/service/oauth/OAuthService.java | 4 ++-- .../auth/api/service/oauth/adapter/github/GithubAdapter.java | 4 ++-- .../api/service/oauth/builder/github/GithubURLBuilder.java | 2 +- .../backend/external/config/ExternalClientsPostProcessor.java | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/src/main/java/com/example/backend/auth/api/service/oauth/OAuthService.java b/backend/src/main/java/com/example/backend/auth/api/service/oauth/OAuthService.java index f9094d817..c8aa826c8 100644 --- a/backend/src/main/java/com/example/backend/auth/api/service/oauth/OAuthService.java +++ b/backend/src/main/java/com/example/backend/auth/api/service/oauth/OAuthService.java @@ -63,7 +63,7 @@ public OAuthResponse login(UserPlatformType platformType, String code, String st OAuthURLBuilder urlBuilder = factory.getOAuthURLBuilder(); OAuthAdapter adapter = factory.getOAuthAdapter(); - log.info(">>>> {} Login Start", platformType); + log.info(">>>> [ {} Login Start ] <<<<", platformType); // code, state를 이용해 Access Token 요청 URL 생성 String tokenUrl = urlBuilder.token(code, state); @@ -73,7 +73,7 @@ public OAuthResponse login(UserPlatformType platformType, String code, String st // 사용자 프로필 조회 OAuthResponse userInfo = adapter.getProfile(accessToken); - log.info(">>>> {} Login Success", platformType); + log.info(">>>> [ {} Login Success ] <<<<", platformType); return userInfo; } diff --git a/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/github/GithubAdapter.java b/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/github/GithubAdapter.java index a70b84fcc..99b3b6d73 100644 --- a/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/github/GithubAdapter.java +++ b/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/github/GithubAdapter.java @@ -35,7 +35,7 @@ public String getToken(String tokenURL) { return token.getAccess_token(); } catch (RuntimeException e) { - log.error(">>>> [ Github Oauth 인증 에러 발생: {}", ExceptionMessage.OAUTH_INVALID_TOKEN_URL.getText()); + log.error(">>>> [ Github Oauth 인증 에러 발생: {} ] <<<<", ExceptionMessage.OAUTH_INVALID_TOKEN_URL.getText()); throw new OAuthException(ExceptionMessage.OAUTH_INVALID_TOKEN_URL); } } @@ -53,7 +53,7 @@ public OAuthResponse getProfile(String accessToken) { .profileImageUrl(profile.getAvatar_url()) .build(); } catch (RuntimeException e) { - log.error(">>>> [ Github Oauth 인증 에러 발생: {}", ExceptionMessage.OAUTH_INVALID_ACCESS_TOKEN.getText()); + log.error(">>>> [ Github Oauth 인증 에러 발생: {} ] <<<<", ExceptionMessage.OAUTH_INVALID_ACCESS_TOKEN.getText()); throw new OAuthException(ExceptionMessage.OAUTH_INVALID_ACCESS_TOKEN); } } diff --git a/backend/src/main/java/com/example/backend/auth/api/service/oauth/builder/github/GithubURLBuilder.java b/backend/src/main/java/com/example/backend/auth/api/service/oauth/builder/github/GithubURLBuilder.java index f861050eb..df17520ff 100644 --- a/backend/src/main/java/com/example/backend/auth/api/service/oauth/builder/github/GithubURLBuilder.java +++ b/backend/src/main/java/com/example/backend/auth/api/service/oauth/builder/github/GithubURLBuilder.java @@ -33,7 +33,7 @@ public GithubURLBuilder(OAuthProperties oAuthProperties) { this.profileUri = githubProvider.profileUri(); } catch (NullPointerException e) { - log.error(">>>> OAuthProperties NullPointerException 발생: {}", ExceptionMessage.OAUTH_CONFIG_NULL); + log.error(">>>> [ OAuthProperties NullPointerException 발생: {} ] <<<<", ExceptionMessage.OAUTH_CONFIG_NULL); throw new OAuthException(ExceptionMessage.OAUTH_CONFIG_NULL); } } diff --git a/backend/src/main/java/com/example/backend/external/config/ExternalClientsPostProcessor.java b/backend/src/main/java/com/example/backend/external/config/ExternalClientsPostProcessor.java index 092b18bcd..785eddd15 100644 --- a/backend/src/main/java/com/example/backend/external/config/ExternalClientsPostProcessor.java +++ b/backend/src/main/java/com/example/backend/external/config/ExternalClientsPostProcessor.java @@ -57,7 +57,7 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) Object externalClientsBean = factory.createClient(clazz); beanFactory.registerSingleton(clazz.getSimpleName(), externalClientsBean); - log.info(">>>> Success External Clients : {}, baseUrl : {}", clazz.getSimpleName(), baseUrl); + log.info(">>>> [ Success External Clients : {}, baseUrl : {} ] <<<<", clazz.getSimpleName(), baseUrl); } } From c2a94411135d87a9f1c7bf0b0df13e16421f26ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A3=BC=EC=84=B1?= <32183520@dankook.ac.kr> Date: Sat, 13 Jan 2024 14:23:49 +0900 Subject: [PATCH 02/34] =?UTF-8?q?[BE]=20feat(#31):=20JWT=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EB=B0=9C=EA=B8=89=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/controller/auth/AuthController.java | 11 ++- .../auth/api/service/auth/AuthService.java | 70 ++++++++++++++++--- .../auth/api/service/jwt/JwtToken.java | 19 +++++ 3 files changed, 84 insertions(+), 16 deletions(-) create mode 100644 backend/src/main/java/com/example/backend/auth/api/service/jwt/JwtToken.java diff --git a/backend/src/main/java/com/example/backend/auth/api/controller/auth/AuthController.java b/backend/src/main/java/com/example/backend/auth/api/controller/auth/AuthController.java index 434ee570e..74463ae5e 100644 --- a/backend/src/main/java/com/example/backend/auth/api/controller/auth/AuthController.java +++ b/backend/src/main/java/com/example/backend/auth/api/controller/auth/AuthController.java @@ -1,6 +1,7 @@ package com.example.backend.auth.api.controller.auth; import com.example.backend.auth.api.controller.auth.response.AuthLoginPageResponse; +import com.example.backend.auth.api.controller.auth.response.AuthLoginResponse; import com.example.backend.auth.api.service.auth.AuthService; import com.example.backend.auth.api.service.oauth.OAuthService; import com.example.backend.common.response.JsonResult; @@ -33,7 +34,7 @@ public JsonResult> loginPage() { } @GetMapping("/{platformType}/login") - public JsonResult login( + public JsonResult login( @PathVariable("platformType") UserPlatformType platformType, @RequestParam("code") String code, @RequestParam("state") String loginState) { @@ -41,13 +42,9 @@ public JsonResult login( TODO : state 값이 유효한지 검증하는 로직이 필요합니다. */ - /* - TODO : 로그인 API는 최종적으로 JWT 토큰이 담긴 AuthLoginResponse DTO를 반환해줘야 합니다. - */ - - authService.login(platformType, code, loginState); + AuthLoginResponse loginResponse = authService.login(platformType, code, loginState); - return JsonResult.successOf("로그인에 성공하였습니다."); + return JsonResult.successOf(loginResponse); } /* diff --git a/backend/src/main/java/com/example/backend/auth/api/service/auth/AuthService.java b/backend/src/main/java/com/example/backend/auth/api/service/auth/AuthService.java index 91b79a59f..84f318822 100644 --- a/backend/src/main/java/com/example/backend/auth/api/service/auth/AuthService.java +++ b/backend/src/main/java/com/example/backend/auth/api/service/auth/AuthService.java @@ -1,15 +1,20 @@ package com.example.backend.auth.api.service.auth; import com.example.backend.auth.api.controller.auth.response.AuthLoginResponse; +import com.example.backend.auth.api.service.jwt.JwtService; +import com.example.backend.auth.api.service.jwt.JwtToken; import com.example.backend.auth.api.service.oauth.OAuthService; import com.example.backend.auth.api.service.oauth.response.OAuthResponse; +import com.example.backend.domain.define.user.User; import com.example.backend.domain.define.user.constant.UserPlatformType; +import com.example.backend.domain.define.user.constant.UserRole; import com.example.backend.domain.define.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.HashMap; import java.util.List; @Slf4j @@ -17,32 +22,79 @@ @Transactional(readOnly = true) @RequiredArgsConstructor public class AuthService { + private static final String ROLE_CLAIM = "role"; + private static final String NAME_CLAIM = "name"; + private static final String PROFILE_IMAGE_CLAIM = "profileImageUrl"; private final UserRepository userRepository; private final OAuthService oAuthService; + private final JwtService jwtService; + @Transactional public AuthLoginResponse login(UserPlatformType platformType, String code, String state) { OAuthResponse loginResponse = oAuthService.login(platformType, code, state); - log.info(">>>> {}님이 로그인하셨습니다.", loginResponse.getName()); + String name = loginResponse.getName(); + String profileImageUrl = loginResponse.getProfileImageUrl(); + String email = loginResponse.getEmail(); + + log.info(">>>> [ {}님이 로그인하셨습니다 ] <<<<", name); /* - TODO : OAuth 로그인 인증을 마쳤으니 우리 애플리케이션의 DB에도 존재하는 사용자인지 확인해야 합니다. - * 회원이 아닐 경우, 즉 회원가입이 필요한 신규 사용자의 경우 OAuthResponse를 바탕으로 DB에 등록해줍니다. + * OAuth 로그인 인증을 마쳤으니 우리 애플리케이션의 DB에도 존재하는 사용자인지 확인한다. + * 회원이 아닐 경우, 즉 회원가입이 필요한 신규 사용자의 경우 OAuthResponse를 바탕으로 DB에 등록해준다. */ + User findUser = userRepository.findByEmail(email) + .orElseGet(() -> { + User saveUser = User.builder() + .platformId(loginResponse.getPlatformId()) + .platformType(loginResponse.getPlatformType()) + .role(UserRole.UNAUTH) + .name(name) + .email(email) + .profileImageUrl(profileImageUrl) + .build(); + + log.info(">>>> [ UNAUTH 권한으로 사용자를 DB에 등록합니다. 이후 회원가입이 필요합니다 ] <<<<"); + return userRepository.save(saveUser); + }); + + // 기존 사용자의 경우 OAuth 사용자 정보(이름, 사진)가 변경되었으면 업데이트해준다. + findUser.updateProfile(name, profileImageUrl); /* TODO : 기존 사용자의 경우 OAuth 사용자 정보가 변경되었을 수 있으므로 변경 사항을 업데이트해주는 로직이 필요합니다. */ + // JWT 토큰과 권한 정보를 담아 반환 + return AuthLoginResponse.builder() + .accessToken(jwtToken.getAccessToken()) + .refreshToken(jwtToken.getRefreshToken()) + .role(findUser.getRole()) + .build(); + } + + private JwtToken generateJwtToken(User user) { + // JWT 토큰 생성을 위한 claims 생성 + HashMap claims = new HashMap<>(); + claims.put(ROLE_CLAIM, user.getRole().name()); + claims.put(NAME_CLAIM, user.getName()); + claims.put(PROFILE_IMAGE_CLAIM, user.getProfileImageUrl()); + + // Access Token 생성 + final String jwtAccessToken = jwtService.generateAccessToken(claims, user); + // 임시로 Refresh Token 생성 + final String jwtRefreshToken = "jwt-refresh-token"; + log.info(">>>> [ 사용자 {}님의 JWT 토큰이 발급되었습니다 ] <<<<", user.getName()); + /* - TODO : DB에 저장된 사용자 정보를 기반으로 JWT 토큰을 발급해주는 로직이 필요합니다. - * JWT 토큰을 요청시에 담아 인증된 사용자임을 알립니다. - * JWT 토큰 인증 필터에서 Security에 인증된 사용자로 등록될 것입니다. - * JWT 재발급을 위한 Refresh 토큰은 Redis에서 관리할 예정입니다. + TODO : Refresh Token 생성, 저장 로직이 필요합니다. + * Redis DB를 연동해 Refresh Token을 저장, 관리할 예정입니다. */ - // TODO : JWT 토큰을 담아 최종적으로 AuthLoginResponse를 반환해줍니다. - return null; + return JwtToken.builder() + .accessToken(jwtAccessToken) + .refreshToken(jwtRefreshToken) + .build(); } /* diff --git a/backend/src/main/java/com/example/backend/auth/api/service/jwt/JwtToken.java b/backend/src/main/java/com/example/backend/auth/api/service/jwt/JwtToken.java new file mode 100644 index 000000000..be0f005bd --- /dev/null +++ b/backend/src/main/java/com/example/backend/auth/api/service/jwt/JwtToken.java @@ -0,0 +1,19 @@ +package com.example.backend.auth.api.service.jwt; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class JwtToken { + private String accessToken; + private String refreshToken; + + @Builder + public JwtToken(String accessToken, String refreshToken) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } +} From 095555064331c83140d9e47a710b2adb480351fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A3=BC=EC=84=B1?= <32183520@dankook.ac.kr> Date: Sat, 13 Jan 2024 14:24:44 +0900 Subject: [PATCH 03/34] =?UTF-8?q?[BE]=20feat(#31):=20OAuth=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EC=A0=95=EB=B3=B4=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 사용자 정보가 변경했을 경우 DB에 업데이트한다. --- .../example/backend/auth/api/service/auth/AuthService.java | 5 ++++- .../java/com/example/backend/domain/define/user/User.java | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/com/example/backend/auth/api/service/auth/AuthService.java b/backend/src/main/java/com/example/backend/auth/api/service/auth/AuthService.java index 84f318822..bee9caec3 100644 --- a/backend/src/main/java/com/example/backend/auth/api/service/auth/AuthService.java +++ b/backend/src/main/java/com/example/backend/auth/api/service/auth/AuthService.java @@ -62,8 +62,11 @@ public AuthLoginResponse login(UserPlatformType platformType, String code, Strin findUser.updateProfile(name, profileImageUrl); /* - TODO : 기존 사용자의 경우 OAuth 사용자 정보가 변경되었을 수 있으므로 변경 사항을 업데이트해주는 로직이 필요합니다. + DB에 저장된 사용자 정보를 기반으로 JWT 토큰을 발급 + * JWT 토큰을 요청시에 담아 보내면 JWT 토큰 인증 필터에서 Security Context에 인증된 사용자로 등록 + TODO : JWT 재발급을 위한 Refresh 토큰은 Redis에서 관리할 예정입니다. */ + JwtToken jwtToken = generateJwtToken(findUser); // JWT 토큰과 권한 정보를 담아 반환 return AuthLoginResponse.builder() diff --git a/backend/src/main/java/com/example/backend/domain/define/user/User.java b/backend/src/main/java/com/example/backend/domain/define/user/User.java index 964989c2e..4670ddc7f 100644 --- a/backend/src/main/java/com/example/backend/domain/define/user/User.java +++ b/backend/src/main/java/com/example/backend/domain/define/user/User.java @@ -72,6 +72,11 @@ private User(String platformId, this.pushAlarmYn = pushAlarmYn; } + public void updateProfile(String name, String profileImageUrl) { + this.profileImageUrl = profileImageUrl; + this.name = name; + } + // Spring Security UserDetails Area @Override public Collection getAuthorities() { From 26b389ad13fb32516b71e0c99bfb425678578102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A3=BC=EC=84=B1?= <32183520@dankook.ac.kr> Date: Sat, 13 Jan 2024 16:06:03 +0900 Subject: [PATCH 04/34] =?UTF-8?q?[BE]=20test(#31):=20JWT=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EB=B0=9C=EA=B8=89=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/backend/auth/TestConfig.java | 13 ++ .../api/service/auth/AuthServiceTest.java | 135 ++++++++++++++++++ .../api/service/oauth/OAuthServiceTest.java | 12 +- 3 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 backend/src/test/java/com/example/backend/auth/api/service/auth/AuthServiceTest.java diff --git a/backend/src/test/java/com/example/backend/auth/TestConfig.java b/backend/src/test/java/com/example/backend/auth/TestConfig.java index 214e27c72..18a75776b 100644 --- a/backend/src/test/java/com/example/backend/auth/TestConfig.java +++ b/backend/src/test/java/com/example/backend/auth/TestConfig.java @@ -16,6 +16,7 @@ */ +import com.example.backend.auth.api.service.oauth.response.OAuthResponse; import com.example.backend.domain.define.user.User; import com.example.backend.domain.define.user.constant.UserPlatformType; import com.example.backend.domain.define.user.constant.UserRole; @@ -23,6 +24,8 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; +import static com.example.backend.domain.define.user.constant.UserPlatformType.GITHUB; + @SpringBootTest @ActiveProfiles("test") @AutoConfigureMockMvc @@ -41,5 +44,15 @@ public static User generateUser() { .pushAlarmYn(true) .build(); } + + public static OAuthResponse generateOauthResponse() { + return OAuthResponse.builder() + .platformId("1") + .platformType(GITHUB) + .email("32183520@dankook.ac.kr") + .name("jusung-c") + .profileImageUrl("http://www.naver.com") + .build(); + } } diff --git a/backend/src/test/java/com/example/backend/auth/api/service/auth/AuthServiceTest.java b/backend/src/test/java/com/example/backend/auth/api/service/auth/AuthServiceTest.java new file mode 100644 index 000000000..5dbb0f589 --- /dev/null +++ b/backend/src/test/java/com/example/backend/auth/api/service/auth/AuthServiceTest.java @@ -0,0 +1,135 @@ +package com.example.backend.auth.api.service.auth; + +import com.example.backend.auth.TestConfig; +import com.example.backend.auth.api.controller.auth.response.AuthLoginResponse; +import com.example.backend.auth.api.service.jwt.JwtService; +import com.example.backend.auth.api.service.oauth.OAuthService; +import com.example.backend.auth.api.service.oauth.response.OAuthResponse; +import com.example.backend.domain.define.user.User; +import com.example.backend.domain.define.user.constant.UserPlatformType; +import com.example.backend.domain.define.user.constant.UserRole; +import com.example.backend.domain.define.user.repository.UserRepository; +import io.jsonwebtoken.Claims; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; + +import static com.example.backend.domain.define.user.constant.UserPlatformType.GITHUB; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +class AuthServiceTest extends TestConfig { + + @MockBean + private OAuthService oAuthService; + + @Autowired + private AuthService authService; + + @Autowired + private JwtService jwtService; + + @Autowired + private UserRepository userRepository; + + @AfterEach + void tearDown() { + userRepository.deleteAllInBatch(); + } + + @Test + @DisplayName("신규 사용자의 경우 UNAUTH 권한으로 DB에 저장된다.") + void registerUnauthUser() { + // given + OAuthResponse oAuthResponse = generateOauthResponse(); + when(oAuthService.login(any(UserPlatformType.class), any(String.class), any(String.class))) + .thenReturn(oAuthResponse); + + // when + authService.login(GITHUB, "code", "state"); + + User user = userRepository.findByEmail(oAuthResponse.getEmail()).get(); + + // then + assertThat(user.getRole().name()).isEqualTo(UserRole.UNAUTH.name()); + } + + @Test + @DisplayName("OAuth 사용자 정보 변경시 DB에 업데이트되어야 한다.") + void loginUserProfileUpdate() { + // given + OAuthResponse oAuthResponse = generateOauthResponse(); + when(oAuthService.login(any(UserPlatformType.class), any(String.class), any(String.class))) + .thenReturn(oAuthResponse); + authService.login(GITHUB, "code", "state"); + + oAuthResponse = OAuthResponse.builder() + .platformId("1") + .platformType(GITHUB) + .email("32183520@dankook.ac.kr") + .name("testName") + .profileImageUrl("www.test.com") + .build(); + + when(oAuthService.login(any(UserPlatformType.class), any(String.class), any(String.class))) + .thenReturn(oAuthResponse); + + // when + authService.login(GITHUB, "code", "state"); + User findUser = userRepository.findByEmail(oAuthResponse.getEmail()).get(); + + // then + assertThat(findUser.getName()).isEqualTo("testName"); + assertThat(findUser.getProfileImageUrl()).isEqualTo("www.test.com"); + + + } + + @Test + @DisplayName("OAuth 로그인 인증 완료 후 JWT 토큰이 정상적으로 발급된다.") + void loginJwtTokenGenerate() { + // given + OAuthResponse oAuthResponse = generateOauthResponse(); + when(oAuthService.login(any(UserPlatformType.class), any(String.class), any(String.class))) + .thenReturn(oAuthResponse); + + // when + AuthLoginResponse loginResponse = authService.login(GITHUB, "code", "state"); +// System.out.println("Access Token: " + loginResponse.getAccessToken()); +// System.out.println("Refresh Token: " + loginResponse.getRefreshToken()); + + // then + assertThat(loginResponse).isNotNull(); + assertThat(loginResponse.getAccessToken()).isNotBlank(); + assertThat(loginResponse.getRefreshToken()).isNotBlank(); + } + + @Test + @DisplayName("OAuth 로그인 인증이 완료된 사용자의 JWT 토큰은 알맞은 Claims가 들어있어야 한다.") + void loginJwtTokenValidClaims() { + // given + String role = "role"; + String name = "name"; + String profileImageUrl = "profileImageUrl"; + + OAuthResponse oAuthResponse = generateOauthResponse(); + when(oAuthService.login(any(UserPlatformType.class), any(String.class), any(String.class))) + .thenReturn(oAuthResponse); + + // when + AuthLoginResponse loginResponse = authService.login(GITHUB, "code", "state"); + String atk = loginResponse.getAccessToken(); + Claims claims = jwtService.extractAllClaims(atk); + + // then + assertAll( + () -> assertThat(claims.get(role)).isEqualTo("UNAUTH"), + () -> assertThat(claims.get(name)).isEqualTo("jusung-c"), + () -> assertThat(claims.get(profileImageUrl)).isEqualTo("http://www.naver.com") + ); + } +} \ No newline at end of file diff --git a/backend/src/test/java/com/example/backend/auth/api/service/oauth/OAuthServiceTest.java b/backend/src/test/java/com/example/backend/auth/api/service/oauth/OAuthServiceTest.java index 3229c070b..c8ef76368 100644 --- a/backend/src/test/java/com/example/backend/auth/api/service/oauth/OAuthServiceTest.java +++ b/backend/src/test/java/com/example/backend/auth/api/service/oauth/OAuthServiceTest.java @@ -2,10 +2,14 @@ import com.example.backend.auth.TestConfig; import com.example.backend.auth.api.controller.auth.response.AuthLoginPageResponse; +import com.example.backend.auth.api.service.jwt.JwtService; import com.example.backend.auth.api.service.oauth.adapter.github.GithubAdapter; import com.example.backend.auth.api.service.oauth.builder.github.GithubURLBuilder; import com.example.backend.auth.api.service.oauth.response.OAuthResponse; import com.example.backend.common.exception.oauth.OAuthException; +import com.example.backend.domain.define.user.constant.UserPlatformType; +import com.example.backend.domain.define.user.repository.UserRepository; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -50,13 +54,7 @@ void githubLoginSuccess() { String code = "valid-code"; String state = "valid-state"; - OAuthResponse response = OAuthResponse.builder() - .platformId("1") - .platformType(GITHUB) - .email("32183520@dankook.ac.kr") - .name("jusung-c") - .profileImageUrl("http://www.naver.com") - .build(); + OAuthResponse response = generateOauthResponse(); // when // when 사용시 Mockito 패키지 사용 From f4a98721d059d1d12fcef3a954fcb0e06c263929 Mon Sep 17 00:00:00 2001 From: j-ra1n Date: Mon, 15 Jan 2024 22:56:03 +0900 Subject: [PATCH 05/34] =?UTF-8?q?[BE]=20feat(#30):=20OAuth=20Google=20URL?= =?UTF-8?q?=20Builder=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Google OAuth 인증을 위한 URL 생성 * access token을 요청하는 URL 생성 * 사용자 프로필 정보 URL 생성 --- .../auth/api/service/oauth/OAuthService.java | 11 ++- .../builder/google/GoogleURLBuilder.java | 74 +++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 backend/src/main/java/com/example/backend/auth/api/service/oauth/builder/google/GoogleURLBuilder.java diff --git a/backend/src/main/java/com/example/backend/auth/api/service/oauth/OAuthService.java b/backend/src/main/java/com/example/backend/auth/api/service/oauth/OAuthService.java index f9094d817..2bdc4289b 100644 --- a/backend/src/main/java/com/example/backend/auth/api/service/oauth/OAuthService.java +++ b/backend/src/main/java/com/example/backend/auth/api/service/oauth/OAuthService.java @@ -3,8 +3,10 @@ import com.example.backend.auth.api.controller.auth.response.AuthLoginPageResponse; import com.example.backend.auth.api.service.oauth.adapter.OAuthAdapter; import com.example.backend.auth.api.service.oauth.adapter.github.GithubAdapter; +import com.example.backend.auth.api.service.oauth.adapter.google.GoogleAdapter; import com.example.backend.auth.api.service.oauth.builder.OAuthURLBuilder; import com.example.backend.auth.api.service.oauth.builder.github.GithubURLBuilder; +import com.example.backend.auth.api.service.oauth.builder.google.GoogleURLBuilder; import com.example.backend.auth.api.service.oauth.response.OAuthResponse; import com.example.backend.domain.define.user.constant.UserPlatformType; import lombok.extern.slf4j.Slf4j; @@ -17,6 +19,7 @@ import java.util.stream.Collectors; import static com.example.backend.domain.define.user.constant.UserPlatformType.GITHUB; +import static com.example.backend.domain.define.user.constant.UserPlatformType.GOOGLE; @Slf4j @Service @@ -25,16 +28,20 @@ public class OAuthService { private Map adapterMap; // 플랫폼별 Adapter, URLBuilder 등록 - public OAuthService(GithubAdapter githubAdapter, GithubURLBuilder githubURLBuilder) { + public OAuthService(GithubAdapter githubAdapter, GithubURLBuilder githubURLBuilder, GoogleAdapter googleAdapter, GoogleURLBuilder googleURLBuilder) { this.adapterMap = new HashMap<>() {{ // 깃허브 플랫폼 추가 put(GITHUB, OAuthFactory.builder() .oAuthAdapter(githubAdapter) .oAuthURLBuilder(githubURLBuilder) .build()); + // 구글 플랫폼 추가 + put(GOOGLE, OAuthFactory.builder() + .oAuthAdapter(googleAdapter) + .oAuthURLBuilder(googleURLBuilder) + .build()); }}; } - // OAuth 2.0 로그인 페이지 생성 public List loginPage(String state) { // 지원하는 모든 플랫폼의 로그인 페이지를 생성해 반환한다. diff --git a/backend/src/main/java/com/example/backend/auth/api/service/oauth/builder/google/GoogleURLBuilder.java b/backend/src/main/java/com/example/backend/auth/api/service/oauth/builder/google/GoogleURLBuilder.java new file mode 100644 index 000000000..b17abcb8b --- /dev/null +++ b/backend/src/main/java/com/example/backend/auth/api/service/oauth/builder/google/GoogleURLBuilder.java @@ -0,0 +1,74 @@ +package com.example.backend.auth.api.service.oauth.builder.google; + +import com.example.backend.auth.api.service.oauth.builder.OAuthURLBuilder; +import com.example.backend.auth.config.oauth.OAuthProperties; +import com.example.backend.common.exception.ExceptionMessage; +import com.example.backend.common.exception.oauth.OAuthException; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.stereotype.Component; + + +@Slf4j +@Component +public class GoogleURLBuilder implements OAuthURLBuilder { + + private static final String PLATFORM = "google"; + private final String authorizationUri; + private final String clientId; + private final String redirectUri; + private final String tokenUri; + private final String clientSecret; + private final String profileUri; + + + public GoogleURLBuilder(OAuthProperties oAuthProperties) { + try { + // 플랫폼(google)의 client, provider Map 획득 + OAuthProperties.Client googleClient = oAuthProperties.getClient().get(PLATFORM); + OAuthProperties.Provider googleProvider = oAuthProperties.getProvider().get(PLATFORM); + + this.authorizationUri = googleProvider.authorizationUri(); + this.clientId = googleClient.clientId(); + this.redirectUri = googleClient.redirectUri(); + this.tokenUri = googleProvider.tokenUri(); + this.clientSecret = googleClient.clientSecret(); + this.profileUri = googleProvider.profileUri(); + + } catch (NullPointerException e) { + log.error(">>>> OAuthProperties NullPointerException 발생: {}", ExceptionMessage.OAUTH_CONFIG_NULL); + throw new OAuthException(ExceptionMessage.OAUTH_CONFIG_NULL); + } + } + + // "https://accounts.google.com/o/oauth2/v2/auth?..." + @Override + public String authorize(String state) { + return authorizationUri + + "?response_type=code" // OAuth 인증 코드 그랜트 유형: code로 고정 + + "&client_id=" + clientId // 클라이언트 ID + + "&redirect_uri=" + redirectUri // 리다이렉트 URI + + "&state=" + state // CSRF 방지 + + "&scope=email+profile"; // Google의 경우 openid가 아닌 email+profile로 추가해야함 + } + + // "https://oauth2.googleapis.com/token?..." + @Override + public String token(String code, String state) { + return tokenUri + + "?grant_type=authorization_code" // OAuth 인증 코드 그랜트 유형: code로 고정 + + "&client_id=" + clientId // 클라이언트 ID + + "&client_secret=" + clientSecret // 클라이언트 Secret + + "&redirect_uri=" + redirectUri // 리다이렉트 URI + + "&code=" + code; // authorize() 요청으로 얻은 인가 코드 + } + + + + + // "https://www.googleapis.com/oauth2/v3/userinfo" + @Override + public String profile() { + return profileUri; + } +} From 47880e38ffc831384bc328d8b281dbd2594f6ce5 Mon Sep 17 00:00:00 2001 From: j-ra1n Date: Mon, 15 Jan 2024 23:29:23 +0900 Subject: [PATCH 06/34] =?UTF-8?q?[BE]=20refactor(#30):=20OAuth=20Google=20?= =?UTF-8?q?URL=20Builder=20=EC=A3=BC=EC=84=9D=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 주석추가 --- .../api/service/oauth/builder/google/GoogleURLBuilder.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/com/example/backend/auth/api/service/oauth/builder/google/GoogleURLBuilder.java b/backend/src/main/java/com/example/backend/auth/api/service/oauth/builder/google/GoogleURLBuilder.java index b17abcb8b..4911655c7 100644 --- a/backend/src/main/java/com/example/backend/auth/api/service/oauth/builder/google/GoogleURLBuilder.java +++ b/backend/src/main/java/com/example/backend/auth/api/service/oauth/builder/google/GoogleURLBuilder.java @@ -42,7 +42,7 @@ public GoogleURLBuilder(OAuthProperties oAuthProperties) { } // "https://accounts.google.com/o/oauth2/v2/auth?..." - @Override + @Override // Google OAuth 인증을 위한 URL 생성 public String authorize(String state) { return authorizationUri + "?response_type=code" // OAuth 인증 코드 그랜트 유형: code로 고정 @@ -53,7 +53,7 @@ public String authorize(String state) { } // "https://oauth2.googleapis.com/token?..." - @Override + @Override // access token을 요청하는 URL 생성 public String token(String code, String state) { return tokenUri + "?grant_type=authorization_code" // OAuth 인증 코드 그랜트 유형: code로 고정 @@ -67,7 +67,7 @@ public String token(String code, String state) { // "https://www.googleapis.com/oauth2/v3/userinfo" - @Override + @Override // 사용자 프로필 정보 요청하는 URL반환 public String profile() { return profileUri; } From 0ac99c45651c82a4c3b02888cc6134dbd6d34dc8 Mon Sep 17 00:00:00 2001 From: j-ra1n Date: Tue, 16 Jan 2024 00:18:33 +0900 Subject: [PATCH 07/34] [BE] feat(#30): OAuth Google URL Builder Test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 인가 코드 요청 URL 생성 테스트 * access token 요청 URL 생성 테스트 * 사용자 정보 요청 URL 생성 테스 --- .../api/service/oauth/OAuthServiceTest.java | 67 +++++++++++++++- .../builder/google/GoogleURLBuilderTest.java | 77 +++++++++++++++++++ 2 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 backend/src/test/java/com/example/backend/auth/api/service/oauth/builder/google/GoogleURLBuilderTest.java diff --git a/backend/src/test/java/com/example/backend/auth/api/service/oauth/OAuthServiceTest.java b/backend/src/test/java/com/example/backend/auth/api/service/oauth/OAuthServiceTest.java index 3229c070b..c54bc6ac2 100644 --- a/backend/src/test/java/com/example/backend/auth/api/service/oauth/OAuthServiceTest.java +++ b/backend/src/test/java/com/example/backend/auth/api/service/oauth/OAuthServiceTest.java @@ -4,6 +4,8 @@ import com.example.backend.auth.api.controller.auth.response.AuthLoginPageResponse; import com.example.backend.auth.api.service.oauth.adapter.github.GithubAdapter; import com.example.backend.auth.api.service.oauth.builder.github.GithubURLBuilder; +import com.example.backend.auth.api.service.oauth.adapter.google.GoogleAdapter; +import com.example.backend.auth.api.service.oauth.builder.google.GoogleURLBuilder; import com.example.backend.auth.api.service.oauth.response.OAuthResponse; import com.example.backend.common.exception.oauth.OAuthException; import org.junit.jupiter.api.DisplayName; @@ -14,6 +16,7 @@ import java.util.List; import static com.example.backend.domain.define.user.constant.UserPlatformType.GITHUB; +import static com.example.backend.domain.define.user.constant.UserPlatformType.GOOGLE; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -29,6 +32,12 @@ class OAuthServiceTest extends TestConfig { @MockBean private GithubAdapter githubAdapter; + @Autowired + private GoogleURLBuilder googleurlBuilder; + + @MockBean + private GoogleAdapter googleAdapter; + @Test @DisplayName("모든 플랫폼의 로그인 페이지를 성공적으로 반환한다.") void allUrlBuilderSuccess() { @@ -38,9 +47,23 @@ void allUrlBuilderSuccess() { // when List loginPages = oAuthService.loginPage(state); String authorizeURL = urlBuilder.authorize(state); + String authorizeURLgoogle = googleurlBuilder.authorize(state); // then - assertThat(loginPages.get(0).getUrl()).isEqualTo(authorizeURL); + //assertThat(loginPages.get(0).getUrl()).isEqualTo(authorizeURL); + + assertThat(loginPages).hasSize(2); // 리스트 크기 확인 + + // 각 플랫폼별 URL인지 확인 + boolean containsGithub = loginPages.stream() + .anyMatch(page -> page.getPlatformType().equals(GITHUB) && + page.getUrl().equals(authorizeURL)); + boolean containsGoogle = loginPages.stream() + .anyMatch(page -> page.getPlatformType().equals(GOOGLE) && + page.getUrl().equals(authorizeURLgoogle)); + + assertThat(containsGithub).isTrue(); + assertThat(containsGoogle).isTrue(); } @Test @@ -83,6 +106,48 @@ void githubLoginFail() { when(githubAdapter.getToken(any(String.class))).thenThrow(OAuthException.class); assertThrows(OAuthException.class, () -> oAuthService.login(GITHUB, code, state)); + } + + @Test + @DisplayName("구글 로그인에 성공하면 OAuthResponse 객체를 반환한다.") + void googleLoginSuccess() { + // given + String code = "valid-code"; + String state = "valid-state"; + + OAuthResponse response = OAuthResponse.builder() + .platformId("102514823309503386675") + .platformType(GOOGLE) + .email("xw21yog@dankook.ac.kr") + .name("이정우") + .profileImageUrl("https://lh3.googleusercontent.com/a/ACg8ocLrP_GLo-fUjSmnUZedPZbbL7ifImYTnelh108XkgOx=s96-c") + .build(); + + // when + // when 사용시 Mockito 패키지 사용 + when(googleAdapter.getToken(any(String.class))).thenReturn("access-token"); + when(googleAdapter.getProfile(any(String.class))).thenReturn(response); + OAuthResponse profile = oAuthService.login(GOOGLE, code, state); + + // then + assertThat(profile) + .extracting("platformId", "platformType", "email", "name", "profileImageUrl") + .contains("102514823309503386675", GOOGLE, "xw21yog@dankook.ac.kr", "이정우", "https://lh3.googleusercontent.com/a/ACg8ocLrP_GLo-fUjSmnUZedPZbbL7ifImYTnelh108XkgOx=s96-c"); + + + } + + @Test + @DisplayName("구글 로그인에 실하면 OAuthException 예외가 발생한다.") + void googleLoginFail() { + // given + String code = "invalid-code"; + String state = "invalid-state"; + + // when + when(googleAdapter.getToken(any(String.class))).thenThrow(OAuthException.class); + assertThrows(OAuthException.class, + () -> oAuthService.login(GOOGLE, code, state)); } } \ No newline at end of file diff --git a/backend/src/test/java/com/example/backend/auth/api/service/oauth/builder/google/GoogleURLBuilderTest.java b/backend/src/test/java/com/example/backend/auth/api/service/oauth/builder/google/GoogleURLBuilderTest.java new file mode 100644 index 000000000..0ca1ead67 --- /dev/null +++ b/backend/src/test/java/com/example/backend/auth/api/service/oauth/builder/google/GoogleURLBuilderTest.java @@ -0,0 +1,77 @@ +package com.example.backend.auth.api.service.oauth.builder.google; + +import com.example.backend.auth.TestConfig; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GoogleURLBuilderTest extends TestConfig { + + @Autowired + private GoogleURLBuilder urlBuilder; + + @Value("${oauth2.client.google.client-id}") private String clientId; + @Value("${oauth2.client.google.client-secret}") private String clientSecret; + @Value("${oauth2.client.google.redirect-uri}") private String redirectUri; + + @Value("${oauth2.provider.google.authorization-uri}") String authorizationUri; + @Value("${oauth2.provider.google.token-uri}") private String tokenUri; + @Value("${oauth2.provider.google.profile-uri}") private String profileUri; + + @Test + @DisplayName("authorize(인가 코드 요청) URL을 성공적으로 생성한다.") + void authorizeURIBuildSuccess() { + // given + String state = "testState"; + + // when + String authorizeURL = urlBuilder.authorize(state); + + // then + System.out.println("authorize URL : " + authorizeURL); + assertThat(authorizeURL).isEqualTo(authorizationUri + + "?response_type=code" + + "&client_id=" + clientId + + "&redirect_uri=" + redirectUri + + "&state=" + state + + "&scope=email+profile"); // openid가아닌 email+profile 변경 + } + + @Test + @DisplayName("token(Access Token 요청) URL을 성공적으로 생성한다.") + void tokenURIBuildSuccess() { + // given + String code = "testCode"; + String state = "testState"; + + // when + String tokenURL = urlBuilder.token(code, state); + + // then + System.out.println("tokenURL : " + tokenURL); + assertThat(tokenURL).isEqualTo(tokenUri + + "?grant_type=authorization_code" + + "&client_id=" + clientId + + "&client_secret=" + clientSecret + + "&redirect_uri=" + redirectUri + + "&code=" + code); + } + + @Test + @DisplayName("profile(사용자 정보 요청) URL을 성공적으로 생성한다.") + void profileURIBuildSuccess() { + // given + + // when + String profileURL = urlBuilder.profile(); + + // then + System.out.println("profileURL : " + profileURL); + assertThat(profileURL).isEqualTo(profileUri); + + } + +} From 064cfd56f8e19ef69bc4e4ada12fa300aae066cd Mon Sep 17 00:00:00 2001 From: j-ra1n Date: Tue, 16 Jan 2024 00:25:16 +0900 Subject: [PATCH 08/34] =?UTF-8?q?[BE]=20feat(#30):=20Google=20Adapter=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Google OAuth 인증 처리 --- .../oauth/adapter/google/GoogleAdapter.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapter.java diff --git a/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapter.java b/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapter.java new file mode 100644 index 000000000..f88490bbf --- /dev/null +++ b/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapter.java @@ -0,0 +1,60 @@ +package com.example.backend.auth.api.service.oauth.adapter.google; + +import com.example.backend.auth.api.service.oauth.adapter.OAuthAdapter; +import com.example.backend.auth.api.service.oauth.response.OAuthResponse; +import com.example.backend.common.exception.ExceptionMessage; +import com.example.backend.common.exception.oauth.OAuthException; +import com.example.backend.external.clients.oauth.google.GoogleProfileClients; +import com.example.backend.external.clients.oauth.google.GoogleTokenClients; +import com.example.backend.external.clients.oauth.google.response.GoogleProfileResponse; +import com.example.backend.external.clients.oauth.google.response.GoogleTokenResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import java.net.URI; +import static com.example.backend.domain.define.user.constant.UserPlatformType.GOOGLE; + +@Slf4j +@Component +@RequiredArgsConstructor +public class GoogleAdapter implements OAuthAdapter { + + private final GoogleTokenClients googleTokenClients; + private final GoogleProfileClients googleProfileClients; + + @Override + public String getToken(String tokenURL) { + try { + GoogleTokenResponse token = googleTokenClients.getToken(URI.create(tokenURL)); + // URL로 액세스 토큰을 요청 + + // 만약 token이 null일 경우 예외처리 + if (token.getAccess_token() == null) { + throw new OAuthException(ExceptionMessage.OAUTH_INVALID_TOKEN_URL); + } + return token.getAccess_token(); + } catch (RuntimeException e) { + log.error(">>>> [ Google Oauth 인증 에러 발생: {}", ExceptionMessage.OAUTH_INVALID_TOKEN_URL.getText()); + throw new OAuthException(ExceptionMessage.OAUTH_INVALID_TOKEN_URL); + } + } + + @Override + public OAuthResponse getProfile(String accessToken) { + try { + GoogleProfileResponse profile = googleProfileClients.getProfile("Bearer " + accessToken); + + // 액세스 토큰을 사용하여 프로필 정보 요청 + return OAuthResponse.builder() + .platformId(profile.getSub()) + .platformType(GOOGLE) + .email(profile.getEmail()) + .name(profile.getName()) + .profileImageUrl(profile.getPicture()) + .build(); + } catch (RuntimeException e) { + log.error(">>>> [ Google Oauth 인증 에러 발생: {}", ExceptionMessage.OAUTH_INVALID_ACCESS_TOKEN.getText()); + throw new OAuthException(ExceptionMessage.OAUTH_INVALID_ACCESS_TOKEN); + } + } +} From 90e3f8bca660aba4a13614f227b6c1f72b7d4ead Mon Sep 17 00:00:00 2001 From: j-ra1n Date: Tue, 16 Jan 2024 00:18:33 +0900 Subject: [PATCH 09/34] [BE] test(#30): OAuth Google URL Builder Test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 인가 코드 요청 URL 생성 테스트 * access token 요청 URL 생성 테스트 * 사용자 정보 요청 URL 생성 테스 --- .../api/service/oauth/OAuthServiceTest.java | 67 +++++++++++++++- .../builder/google/GoogleURLBuilderTest.java | 77 +++++++++++++++++++ 2 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 backend/src/test/java/com/example/backend/auth/api/service/oauth/builder/google/GoogleURLBuilderTest.java diff --git a/backend/src/test/java/com/example/backend/auth/api/service/oauth/OAuthServiceTest.java b/backend/src/test/java/com/example/backend/auth/api/service/oauth/OAuthServiceTest.java index 3229c070b..c54bc6ac2 100644 --- a/backend/src/test/java/com/example/backend/auth/api/service/oauth/OAuthServiceTest.java +++ b/backend/src/test/java/com/example/backend/auth/api/service/oauth/OAuthServiceTest.java @@ -4,6 +4,8 @@ import com.example.backend.auth.api.controller.auth.response.AuthLoginPageResponse; import com.example.backend.auth.api.service.oauth.adapter.github.GithubAdapter; import com.example.backend.auth.api.service.oauth.builder.github.GithubURLBuilder; +import com.example.backend.auth.api.service.oauth.adapter.google.GoogleAdapter; +import com.example.backend.auth.api.service.oauth.builder.google.GoogleURLBuilder; import com.example.backend.auth.api.service.oauth.response.OAuthResponse; import com.example.backend.common.exception.oauth.OAuthException; import org.junit.jupiter.api.DisplayName; @@ -14,6 +16,7 @@ import java.util.List; import static com.example.backend.domain.define.user.constant.UserPlatformType.GITHUB; +import static com.example.backend.domain.define.user.constant.UserPlatformType.GOOGLE; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -29,6 +32,12 @@ class OAuthServiceTest extends TestConfig { @MockBean private GithubAdapter githubAdapter; + @Autowired + private GoogleURLBuilder googleurlBuilder; + + @MockBean + private GoogleAdapter googleAdapter; + @Test @DisplayName("모든 플랫폼의 로그인 페이지를 성공적으로 반환한다.") void allUrlBuilderSuccess() { @@ -38,9 +47,23 @@ void allUrlBuilderSuccess() { // when List loginPages = oAuthService.loginPage(state); String authorizeURL = urlBuilder.authorize(state); + String authorizeURLgoogle = googleurlBuilder.authorize(state); // then - assertThat(loginPages.get(0).getUrl()).isEqualTo(authorizeURL); + //assertThat(loginPages.get(0).getUrl()).isEqualTo(authorizeURL); + + assertThat(loginPages).hasSize(2); // 리스트 크기 확인 + + // 각 플랫폼별 URL인지 확인 + boolean containsGithub = loginPages.stream() + .anyMatch(page -> page.getPlatformType().equals(GITHUB) && + page.getUrl().equals(authorizeURL)); + boolean containsGoogle = loginPages.stream() + .anyMatch(page -> page.getPlatformType().equals(GOOGLE) && + page.getUrl().equals(authorizeURLgoogle)); + + assertThat(containsGithub).isTrue(); + assertThat(containsGoogle).isTrue(); } @Test @@ -83,6 +106,48 @@ void githubLoginFail() { when(githubAdapter.getToken(any(String.class))).thenThrow(OAuthException.class); assertThrows(OAuthException.class, () -> oAuthService.login(GITHUB, code, state)); + } + + @Test + @DisplayName("구글 로그인에 성공하면 OAuthResponse 객체를 반환한다.") + void googleLoginSuccess() { + // given + String code = "valid-code"; + String state = "valid-state"; + + OAuthResponse response = OAuthResponse.builder() + .platformId("102514823309503386675") + .platformType(GOOGLE) + .email("xw21yog@dankook.ac.kr") + .name("이정우") + .profileImageUrl("https://lh3.googleusercontent.com/a/ACg8ocLrP_GLo-fUjSmnUZedPZbbL7ifImYTnelh108XkgOx=s96-c") + .build(); + + // when + // when 사용시 Mockito 패키지 사용 + when(googleAdapter.getToken(any(String.class))).thenReturn("access-token"); + when(googleAdapter.getProfile(any(String.class))).thenReturn(response); + OAuthResponse profile = oAuthService.login(GOOGLE, code, state); + + // then + assertThat(profile) + .extracting("platformId", "platformType", "email", "name", "profileImageUrl") + .contains("102514823309503386675", GOOGLE, "xw21yog@dankook.ac.kr", "이정우", "https://lh3.googleusercontent.com/a/ACg8ocLrP_GLo-fUjSmnUZedPZbbL7ifImYTnelh108XkgOx=s96-c"); + + + } + + @Test + @DisplayName("구글 로그인에 실하면 OAuthException 예외가 발생한다.") + void googleLoginFail() { + // given + String code = "invalid-code"; + String state = "invalid-state"; + + // when + when(googleAdapter.getToken(any(String.class))).thenThrow(OAuthException.class); + assertThrows(OAuthException.class, + () -> oAuthService.login(GOOGLE, code, state)); } } \ No newline at end of file diff --git a/backend/src/test/java/com/example/backend/auth/api/service/oauth/builder/google/GoogleURLBuilderTest.java b/backend/src/test/java/com/example/backend/auth/api/service/oauth/builder/google/GoogleURLBuilderTest.java new file mode 100644 index 000000000..0ca1ead67 --- /dev/null +++ b/backend/src/test/java/com/example/backend/auth/api/service/oauth/builder/google/GoogleURLBuilderTest.java @@ -0,0 +1,77 @@ +package com.example.backend.auth.api.service.oauth.builder.google; + +import com.example.backend.auth.TestConfig; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GoogleURLBuilderTest extends TestConfig { + + @Autowired + private GoogleURLBuilder urlBuilder; + + @Value("${oauth2.client.google.client-id}") private String clientId; + @Value("${oauth2.client.google.client-secret}") private String clientSecret; + @Value("${oauth2.client.google.redirect-uri}") private String redirectUri; + + @Value("${oauth2.provider.google.authorization-uri}") String authorizationUri; + @Value("${oauth2.provider.google.token-uri}") private String tokenUri; + @Value("${oauth2.provider.google.profile-uri}") private String profileUri; + + @Test + @DisplayName("authorize(인가 코드 요청) URL을 성공적으로 생성한다.") + void authorizeURIBuildSuccess() { + // given + String state = "testState"; + + // when + String authorizeURL = urlBuilder.authorize(state); + + // then + System.out.println("authorize URL : " + authorizeURL); + assertThat(authorizeURL).isEqualTo(authorizationUri + + "?response_type=code" + + "&client_id=" + clientId + + "&redirect_uri=" + redirectUri + + "&state=" + state + + "&scope=email+profile"); // openid가아닌 email+profile 변경 + } + + @Test + @DisplayName("token(Access Token 요청) URL을 성공적으로 생성한다.") + void tokenURIBuildSuccess() { + // given + String code = "testCode"; + String state = "testState"; + + // when + String tokenURL = urlBuilder.token(code, state); + + // then + System.out.println("tokenURL : " + tokenURL); + assertThat(tokenURL).isEqualTo(tokenUri + + "?grant_type=authorization_code" + + "&client_id=" + clientId + + "&client_secret=" + clientSecret + + "&redirect_uri=" + redirectUri + + "&code=" + code); + } + + @Test + @DisplayName("profile(사용자 정보 요청) URL을 성공적으로 생성한다.") + void profileURIBuildSuccess() { + // given + + // when + String profileURL = urlBuilder.profile(); + + // then + System.out.println("profileURL : " + profileURL); + assertThat(profileURL).isEqualTo(profileUri); + + } + +} From a54104482eaf23cefb00dfed2001e0d1881085e1 Mon Sep 17 00:00:00 2001 From: j-ra1n Date: Tue, 16 Jan 2024 00:25:16 +0900 Subject: [PATCH 10/34] =?UTF-8?q?[BE]=20feat(#30):=20Google=20Adapter=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Google OAuth 인증 처리 --- .../oauth/adapter/google/GoogleAdapter.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapter.java diff --git a/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapter.java b/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapter.java new file mode 100644 index 000000000..f88490bbf --- /dev/null +++ b/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapter.java @@ -0,0 +1,60 @@ +package com.example.backend.auth.api.service.oauth.adapter.google; + +import com.example.backend.auth.api.service.oauth.adapter.OAuthAdapter; +import com.example.backend.auth.api.service.oauth.response.OAuthResponse; +import com.example.backend.common.exception.ExceptionMessage; +import com.example.backend.common.exception.oauth.OAuthException; +import com.example.backend.external.clients.oauth.google.GoogleProfileClients; +import com.example.backend.external.clients.oauth.google.GoogleTokenClients; +import com.example.backend.external.clients.oauth.google.response.GoogleProfileResponse; +import com.example.backend.external.clients.oauth.google.response.GoogleTokenResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import java.net.URI; +import static com.example.backend.domain.define.user.constant.UserPlatformType.GOOGLE; + +@Slf4j +@Component +@RequiredArgsConstructor +public class GoogleAdapter implements OAuthAdapter { + + private final GoogleTokenClients googleTokenClients; + private final GoogleProfileClients googleProfileClients; + + @Override + public String getToken(String tokenURL) { + try { + GoogleTokenResponse token = googleTokenClients.getToken(URI.create(tokenURL)); + // URL로 액세스 토큰을 요청 + + // 만약 token이 null일 경우 예외처리 + if (token.getAccess_token() == null) { + throw new OAuthException(ExceptionMessage.OAUTH_INVALID_TOKEN_URL); + } + return token.getAccess_token(); + } catch (RuntimeException e) { + log.error(">>>> [ Google Oauth 인증 에러 발생: {}", ExceptionMessage.OAUTH_INVALID_TOKEN_URL.getText()); + throw new OAuthException(ExceptionMessage.OAUTH_INVALID_TOKEN_URL); + } + } + + @Override + public OAuthResponse getProfile(String accessToken) { + try { + GoogleProfileResponse profile = googleProfileClients.getProfile("Bearer " + accessToken); + + // 액세스 토큰을 사용하여 프로필 정보 요청 + return OAuthResponse.builder() + .platformId(profile.getSub()) + .platformType(GOOGLE) + .email(profile.getEmail()) + .name(profile.getName()) + .profileImageUrl(profile.getPicture()) + .build(); + } catch (RuntimeException e) { + log.error(">>>> [ Google Oauth 인증 에러 발생: {}", ExceptionMessage.OAUTH_INVALID_ACCESS_TOKEN.getText()); + throw new OAuthException(ExceptionMessage.OAUTH_INVALID_ACCESS_TOKEN); + } + } +} From 04eb53c2f53c39c40206817827c72c2c14f73004 Mon Sep 17 00:00:00 2001 From: j-ra1n Date: Tue, 16 Jan 2024 00:34:06 +0900 Subject: [PATCH 11/34] [BE] test(#30): Google Adapter Test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 토큰 요청 테스트 * 프로필 요청 테스트 --- .../adapter/google/GoogleAdapterTest.java | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapterTest.java diff --git a/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapterTest.java b/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapterTest.java new file mode 100644 index 000000000..40b8cac2d --- /dev/null +++ b/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapterTest.java @@ -0,0 +1,119 @@ +package com.example.backend.auth.api.service.oauth.adapter.google; + + +import com.example.backend.auth.TestConfig; +import com.example.backend.auth.api.service.oauth.builder.google.GoogleURLBuilder; +import com.example.backend.auth.api.service.oauth.response.OAuthResponse; +import com.example.backend.common.exception.ExceptionMessage; +import com.example.backend.common.exception.oauth.OAuthException; + +import com.example.backend.external.clients.oauth.google.GoogleProfileClients; +import com.example.backend.external.clients.oauth.google.GoogleTokenClients; +import com.example.backend.external.clients.oauth.google.response.GoogleProfileResponse; +import com.example.backend.external.clients.oauth.google.response.GoogleTokenResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.net.URI; + +import static com.example.backend.domain.define.user.constant.UserPlatformType.GOOGLE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class GoogleAdapterTest extends TestConfig { + + @Autowired + private GoogleAdapter googleAdapter; + + @Autowired + private GoogleURLBuilder googleURLBuilder; + + @Test + @DisplayName("google 토큰 요청 API에 정상적인 요청을 보내면, access_token이 발행된다.") + void googleAdapterGetTokenSuccess() { + // given + GoogleAdapterTest.MockGoogleTokenClients mockGoogleTokenClients = new GoogleAdapterTest.MockGoogleTokenClients(); + GoogleAdapterTest.MockGoogleProfileClients mockGoogleProfileClients = new GoogleAdapterTest.MockGoogleProfileClients(); + GoogleAdapter googleAdapter = new GoogleAdapter(mockGoogleTokenClients, mockGoogleProfileClients); + + // when + String accessToken = googleAdapter.getToken("tokenUrl"); + + // then + System.out.println("accessToken = " + accessToken); + assertThat(accessToken).isEqualTo("access-token"); + + } + + @Test + @DisplayName("google 토큰 요청 중 예외가 발생하면, OAUTH_INVALID_TOKEN_URL 에외가 발생한다.") + void googleAdapterGetTokenFail() { + // given + String tokenURL = googleURLBuilder.token("error-token", "state"); + + // when + OAuthException exception = assertThrows(OAuthException.class, + () -> googleAdapter.getToken(tokenURL)); + + // then + assertThat(exception.getMessage()).isEqualTo(ExceptionMessage.OAUTH_INVALID_TOKEN_URL.getText()); + + } + + @Test + @DisplayName("google 프로필 요청 API에 정상적인 요청을 보내면, 사용자 프로필이 반환된다.") + void googleAdapterGetProfileSuccess() { + // given + GoogleAdapterTest.MockGoogleTokenClients mockGoogleTokenClients = new GoogleAdapterTest.MockGoogleTokenClients(); + GoogleAdapterTest.MockGoogleProfileClients mockGoogleProfileClients = new GoogleAdapterTest.MockGoogleProfileClients(); + GoogleAdapter googleAdapter = new GoogleAdapter(mockGoogleTokenClients, mockGoogleProfileClients); + + // when + OAuthResponse profile = googleAdapter.getProfile("access-token"); + + // then + assertAll( + () -> assertThat(profile.getPlatformId()).isEqualTo("102514823309503386675"), // google은 sub + () -> assertThat(profile.getEmail()).isEqualTo("xw21yog@dankook.ac.kr"), + () -> assertThat(profile.getProfileImageUrl()).isEqualTo("https://lh3.googleusercontent.com/a/ACg8ocLrP_GLo-fUjSmnUZedPZbbL7ifImYTnelh108XkgOx=s96-c"), + () -> assertThat(profile.getName()).isEqualTo("이정우"), + () -> assertThat(profile.getPlatformType()).isEqualTo(GOOGLE) + ); + } + + @Test + @DisplayName("google 프로필 요청 중 예외가 발생하면, OAUTH_INVALID_ACCESS_TOKEN 예외가 발생한다.") + void googleAdapterGetProfileFail() { + + // when + OAuthException exception = assertThrows(OAuthException.class, + () -> googleAdapter.getProfile("error-token")); + + // then + assertThat(exception.getMessage()).isEqualTo(ExceptionMessage.OAUTH_INVALID_ACCESS_TOKEN.getText()); + + } + + + + static class MockGoogleTokenClients implements GoogleTokenClients { + + @Override + public GoogleTokenResponse getToken(URI uri) { + return new GoogleTokenResponse("access-token"); + } + } + + static class MockGoogleProfileClients implements GoogleProfileClients { + + @Override + public GoogleProfileResponse getProfile(String header) { + return new GoogleProfileResponse("102514823309503386675", + "이정우", + "xw21yog@dankook.ac.kr", + "https://lh3.googleusercontent.com/a/ACg8ocLrP_GLo-fUjSmnUZedPZbbL7ifImYTnelh108XkgOx=s96-c"); + } + } +} From 407e466634a323355d4e7563e5a52cca7186cfa7 Mon Sep 17 00:00:00 2001 From: j-ra1n Date: Tue, 16 Jan 2024 10:31:00 +0900 Subject: [PATCH 12/34] =?UTF-8?q?[BE]=20feat(#30):=20Google=20Token=20Clie?= =?UTF-8?q?nts,=20Response=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Token uri로 요청 구현 * Token을 요청 후 반환받을 DTO 구현 --- .../oauth/google/GoogleTokenClients.java | 20 +++++++++++++++++++ .../google/response/GoogleTokenResponse.java | 20 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 backend/src/main/java/com/example/backend/external/clients/oauth/google/GoogleTokenClients.java create mode 100644 backend/src/main/java/com/example/backend/external/clients/oauth/google/response/GoogleTokenResponse.java diff --git a/backend/src/main/java/com/example/backend/external/clients/oauth/google/GoogleTokenClients.java b/backend/src/main/java/com/example/backend/external/clients/oauth/google/GoogleTokenClients.java new file mode 100644 index 000000000..3e9044773 --- /dev/null +++ b/backend/src/main/java/com/example/backend/external/clients/oauth/google/GoogleTokenClients.java @@ -0,0 +1,20 @@ +package com.example.backend.external.clients.oauth.google; + + +import com.example.backend.external.annotation.ExternalClients; +import com.example.backend.external.clients.oauth.google.response.GoogleTokenResponse; +import org.springframework.http.MediaType; +import org.springframework.web.service.annotation.PostExchange; + +import java.net.URI; + +/* + AccessToken을 얻기 위해 설정한 token URI로 POST 요청을 보낸다. + */ +@ExternalClients(baseUrl = "oauth2.provider.google.token-uri") +public interface GoogleTokenClients { + + // 요청의 content Type: FORM_URLENCODED 형식 ex) key1=value1&key2=value2 + @PostExchange(contentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + public GoogleTokenResponse getToken(URI uri); +} diff --git a/backend/src/main/java/com/example/backend/external/clients/oauth/google/response/GoogleTokenResponse.java b/backend/src/main/java/com/example/backend/external/clients/oauth/google/response/GoogleTokenResponse.java new file mode 100644 index 000000000..d07231574 --- /dev/null +++ b/backend/src/main/java/com/example/backend/external/clients/oauth/google/response/GoogleTokenResponse.java @@ -0,0 +1,20 @@ +package com.example.backend.external.clients.oauth.google.response; + + +import lombok.Getter; +import lombok.NoArgsConstructor; + +/* + Google에 Access Token을 요청 후 반환받을 DTO + */ +@Getter +@NoArgsConstructor +public class GoogleTokenResponse { + private String access_token; + + + public GoogleTokenResponse(String access_token) { + this.access_token = access_token; + } + +} \ No newline at end of file From 614e2d9dc9429d1001b9bbba271a1f7b6de14cd2 Mon Sep 17 00:00:00 2001 From: j-ra1n Date: Tue, 16 Jan 2024 10:32:39 +0900 Subject: [PATCH 13/34] =?UTF-8?q?[BE]=20feat(#30):=20Google=20Profile=20Cl?= =?UTF-8?q?ients,=20Response=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 프로필 uri로 요청 구현 * 프로필을 요청 후 반환받을 DTO 구현 --- .../oauth/google/GoogleProfileClients.java | 18 +++++++++++++ .../response/GoogleProfileResponse.java | 26 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 backend/src/main/java/com/example/backend/external/clients/oauth/google/GoogleProfileClients.java create mode 100644 backend/src/main/java/com/example/backend/external/clients/oauth/google/response/GoogleProfileResponse.java diff --git a/backend/src/main/java/com/example/backend/external/clients/oauth/google/GoogleProfileClients.java b/backend/src/main/java/com/example/backend/external/clients/oauth/google/GoogleProfileClients.java new file mode 100644 index 000000000..d5a431273 --- /dev/null +++ b/backend/src/main/java/com/example/backend/external/clients/oauth/google/GoogleProfileClients.java @@ -0,0 +1,18 @@ +package com.example.backend.external.clients.oauth.google; + + +import com.example.backend.external.annotation.ExternalClients; +import com.example.backend.external.clients.oauth.google.response.GoogleProfileResponse; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.service.annotation.GetExchange; + +/* + 사용자 정보를 얻기 위해 설정한 profile 요청 URI로 GET 요청을 보낸다. + */ +@ExternalClients(baseUrl = "oauth2.provider.google.profile-uri") +public interface GoogleProfileClients { + + @GetExchange + public GoogleProfileResponse getProfile(@RequestHeader(value = "Authorization") String header); + +} diff --git a/backend/src/main/java/com/example/backend/external/clients/oauth/google/response/GoogleProfileResponse.java b/backend/src/main/java/com/example/backend/external/clients/oauth/google/response/GoogleProfileResponse.java new file mode 100644 index 000000000..18417f9e9 --- /dev/null +++ b/backend/src/main/java/com/example/backend/external/clients/oauth/google/response/GoogleProfileResponse.java @@ -0,0 +1,26 @@ +package com.example.backend.external.clients.oauth.google.response; + + +import lombok.Getter; +import lombok.NoArgsConstructor; + + +/* + Google에 사용자 정보를 요청 후 반환받을 DTO + */ +@Getter +@NoArgsConstructor // 사용자 프로필 응답 데이터 +public class GoogleProfileResponse { + private String sub; // Google에서 사용하는 사용자의 고유 식별자 (subject) + private String name; + private String email; + private String picture; + + + public GoogleProfileResponse(String sub, String name, String email, String picture) { + this.sub = sub; + this.name = name; + this.email = email; + this.picture = picture; + } +} From 444489e873988c664fc2f72d80215adb69b280fd Mon Sep 17 00:00:00 2001 From: kooyeongmin Date: Tue, 16 Jan 2024 17:23:43 +0900 Subject: [PATCH 14/34] =?UTF-8?q?[BE]=20test(#29):=20Kakao=20Adapter=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Google OAuth 인증 처리 --- .../oauth/adapter/kakao/KakaoAdapter.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapter.java diff --git a/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapter.java b/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapter.java new file mode 100644 index 000000000..073e900d1 --- /dev/null +++ b/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapter.java @@ -0,0 +1,60 @@ +package com.example.backend.auth.api.service.oauth.adapter.kakao; + +import com.example.backend.auth.api.service.oauth.adapter.OAuthAdapter; +import com.example.backend.auth.api.service.oauth.response.OAuthResponse; +import com.example.backend.common.exception.ExceptionMessage; +import com.example.backend.common.exception.oauth.OAuthException; +import com.example.backend.external.clients.oauth.kakao.KakaoProfileClients; +import com.example.backend.external.clients.oauth.kakao.KakaoTokenClients; +import com.example.backend.external.clients.oauth.kakao.response.KakaoProfileResponse; +import com.example.backend.external.clients.oauth.kakao.response.KakaoTokenResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.net.URI; + +import static com.example.backend.domain.define.user.constant.UserPlatformType.KAKAO; + +@Slf4j +@Component +@RequiredArgsConstructor +public class KakaoAdapter implements OAuthAdapter { + private final KakaoTokenClients kakaoTokenClients; + private final KakaoProfileClients kakaoProfileClients; + + @Override + public String getToken(String tokenURL) { + try { + KakaoTokenResponse token = kakaoTokenClients.getToken(URI.create(tokenURL)); + + // 받아온 token이 null일 경우 예외 발생 + if (token.getAccess_token() == null) { + throw new OAuthException(ExceptionMessage.OAUTH_INVALID_TOKEN_URL); + } + + return token.getAccess_token(); + } catch (RuntimeException e) { + log.error(">>>> [ Kakao Oauth 인증 에러 발생: {}", ExceptionMessage.OAUTH_INVALID_TOKEN_URL.getText()); + throw new OAuthException(ExceptionMessage.OAUTH_INVALID_TOKEN_URL); + } + } + + @Override + public OAuthResponse getProfile(String accessToken) { + try { + KakaoProfileResponse profile = kakaoProfileClients.getProfile("Bearer " + accessToken); + + return OAuthResponse.builder() + .platformId(profile.getId().toString()) + .platformType(KAKAO) + .email(profile.getEmail()) + .name(profile.getProperties().getNickname()) + .profileImageUrl(profile.getProperties().getProfile_image()) + .build(); + } catch (RuntimeException e) { + log.error(">>>> [ Kakao Oauth 인증 에러 발생: {}", ExceptionMessage.OAUTH_INVALID_ACCESS_TOKEN.getText()); + throw new OAuthException(ExceptionMessage.OAUTH_INVALID_ACCESS_TOKEN); + } + } +} From 881407675522f4450fa3a6235dd11ba4203ddc5a Mon Sep 17 00:00:00 2001 From: kooyeongmin Date: Tue, 16 Jan 2024 17:28:52 +0900 Subject: [PATCH 15/34] =?UTF-8?q?[BE]=20feat(#29)=20KakaoURLBuilder=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Kakao OAuth 인증을 위한 URL 생성 * access token을 요청하는 URL 생성 * 사용자 프로필 정보 URL 생성 --- .../oauth/builder/kakao/KakaoURLBuilder.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 backend/src/main/java/com/example/backend/auth/api/service/oauth/builder/kakao/KakaoURLBuilder.java diff --git a/backend/src/main/java/com/example/backend/auth/api/service/oauth/builder/kakao/KakaoURLBuilder.java b/backend/src/main/java/com/example/backend/auth/api/service/oauth/builder/kakao/KakaoURLBuilder.java new file mode 100644 index 000000000..04a4b35a1 --- /dev/null +++ b/backend/src/main/java/com/example/backend/auth/api/service/oauth/builder/kakao/KakaoURLBuilder.java @@ -0,0 +1,66 @@ +package com.example.backend.auth.api.service.oauth.builder.kakao; + +import com.example.backend.auth.api.service.oauth.builder.OAuthURLBuilder; +import com.example.backend.auth.config.oauth.OAuthProperties; +import com.example.backend.common.exception.ExceptionMessage; +import com.example.backend.common.exception.oauth.OAuthException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class KakaoURLBuilder implements OAuthURLBuilder { + private static final String PLATFORM = "kakao"; + private final String authorizationUri; + private final String clientId; + private final String redirectUri; + private final String tokenUri; + private final String clientSecret; + private final String profileUri; + + + // 속성에서 읽어온 객체를 주입 + public KakaoURLBuilder(OAuthProperties oAuthProperties) { + try { + // 플랫폼(kakao)의 client, provider Map 획득 + OAuthProperties.Client kakaoClient = oAuthProperties.getClient().get(PLATFORM); + OAuthProperties.Provider kakaoProvider = oAuthProperties.getProvider().get(PLATFORM); + + this.authorizationUri = kakaoProvider.authorizationUri(); + this.clientId = kakaoClient.clientId(); + this.redirectUri = kakaoClient.redirectUri(); + this.tokenUri = kakaoProvider.tokenUri(); + this.clientSecret = kakaoClient.clientSecret(); + this.profileUri = kakaoProvider.profileUri(); + + } catch (NullPointerException e) { + log.error(">>>> OAuthProperties NullPointerException 발생: {}", ExceptionMessage.OAUTH_CONFIG_NULL); + throw new OAuthException(ExceptionMessage.OAUTH_CONFIG_NULL); + } + } + // "https://kauth.kakao.com/oauth/authorize?..." + @Override + public String authorize(String state) { + return authorizationUri + + "?response_type=code" // OAuth 인증 코드 그랜트 유형: code로 고정 + + "&client_id=" + clientId // 클라이언트 ID + + "&redirect_uri=" + redirectUri // 리다이렉트 URI + + "&state=" + state // CSRF 방지 + + "&scope=openid"; // 리소스 접근 범위: openid로 고정 + } + + // "https://kauth.kakao.com/oauth/token?..." + @Override + public String token(String code, String state) { + return tokenUri + + "?grant_type=authorization_code" // OAuth 인증 코드 그랜트 유형: code로 고정 + + "&client_id=" + clientId // 클라이언트 ID + + "&client_secret=" + clientSecret // 클라이언트 Secret + + "&redirect_uri=" + redirectUri // 리다이렉트 URI + + "&code=" + code; // authorize() 요청으로 얻은 인가 코드 + + } + // "https://kapi.kakao.com/v2/user/me" + @Override + public String profile() { return profileUri; } +} From b43cbdf95a9ab62e36266cb9292069a9b4fba6a3 Mon Sep 17 00:00:00 2001 From: kooyeongmin Date: Tue, 16 Jan 2024 17:23:43 +0900 Subject: [PATCH 16/34] =?UTF-8?q?[BE]=20feat(#29):=20Kakao=20Adapter=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Kakao OAuth 인증 처리 --- .../oauth/adapter/kakao/KakaoAdapter.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapter.java diff --git a/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapter.java b/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapter.java new file mode 100644 index 000000000..073e900d1 --- /dev/null +++ b/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapter.java @@ -0,0 +1,60 @@ +package com.example.backend.auth.api.service.oauth.adapter.kakao; + +import com.example.backend.auth.api.service.oauth.adapter.OAuthAdapter; +import com.example.backend.auth.api.service.oauth.response.OAuthResponse; +import com.example.backend.common.exception.ExceptionMessage; +import com.example.backend.common.exception.oauth.OAuthException; +import com.example.backend.external.clients.oauth.kakao.KakaoProfileClients; +import com.example.backend.external.clients.oauth.kakao.KakaoTokenClients; +import com.example.backend.external.clients.oauth.kakao.response.KakaoProfileResponse; +import com.example.backend.external.clients.oauth.kakao.response.KakaoTokenResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.net.URI; + +import static com.example.backend.domain.define.user.constant.UserPlatformType.KAKAO; + +@Slf4j +@Component +@RequiredArgsConstructor +public class KakaoAdapter implements OAuthAdapter { + private final KakaoTokenClients kakaoTokenClients; + private final KakaoProfileClients kakaoProfileClients; + + @Override + public String getToken(String tokenURL) { + try { + KakaoTokenResponse token = kakaoTokenClients.getToken(URI.create(tokenURL)); + + // 받아온 token이 null일 경우 예외 발생 + if (token.getAccess_token() == null) { + throw new OAuthException(ExceptionMessage.OAUTH_INVALID_TOKEN_URL); + } + + return token.getAccess_token(); + } catch (RuntimeException e) { + log.error(">>>> [ Kakao Oauth 인증 에러 발생: {}", ExceptionMessage.OAUTH_INVALID_TOKEN_URL.getText()); + throw new OAuthException(ExceptionMessage.OAUTH_INVALID_TOKEN_URL); + } + } + + @Override + public OAuthResponse getProfile(String accessToken) { + try { + KakaoProfileResponse profile = kakaoProfileClients.getProfile("Bearer " + accessToken); + + return OAuthResponse.builder() + .platformId(profile.getId().toString()) + .platformType(KAKAO) + .email(profile.getEmail()) + .name(profile.getProperties().getNickname()) + .profileImageUrl(profile.getProperties().getProfile_image()) + .build(); + } catch (RuntimeException e) { + log.error(">>>> [ Kakao Oauth 인증 에러 발생: {}", ExceptionMessage.OAUTH_INVALID_ACCESS_TOKEN.getText()); + throw new OAuthException(ExceptionMessage.OAUTH_INVALID_ACCESS_TOKEN); + } + } +} From eb2ce335131641e09611afc2cb821f01fbd815f4 Mon Sep 17 00:00:00 2001 From: kooyeongmin Date: Tue, 16 Jan 2024 17:28:52 +0900 Subject: [PATCH 17/34] =?UTF-8?q?[BE]=20feat(#29):=20KakaoURLBuilder=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Kakao OAuth 인증을 위한 URL 생성 * access token을 요청하는 URL 생성 * 사용자 프로필 정보 URL 생성 --- .../oauth/builder/kakao/KakaoURLBuilder.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 backend/src/main/java/com/example/backend/auth/api/service/oauth/builder/kakao/KakaoURLBuilder.java diff --git a/backend/src/main/java/com/example/backend/auth/api/service/oauth/builder/kakao/KakaoURLBuilder.java b/backend/src/main/java/com/example/backend/auth/api/service/oauth/builder/kakao/KakaoURLBuilder.java new file mode 100644 index 000000000..04a4b35a1 --- /dev/null +++ b/backend/src/main/java/com/example/backend/auth/api/service/oauth/builder/kakao/KakaoURLBuilder.java @@ -0,0 +1,66 @@ +package com.example.backend.auth.api.service.oauth.builder.kakao; + +import com.example.backend.auth.api.service.oauth.builder.OAuthURLBuilder; +import com.example.backend.auth.config.oauth.OAuthProperties; +import com.example.backend.common.exception.ExceptionMessage; +import com.example.backend.common.exception.oauth.OAuthException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class KakaoURLBuilder implements OAuthURLBuilder { + private static final String PLATFORM = "kakao"; + private final String authorizationUri; + private final String clientId; + private final String redirectUri; + private final String tokenUri; + private final String clientSecret; + private final String profileUri; + + + // 속성에서 읽어온 객체를 주입 + public KakaoURLBuilder(OAuthProperties oAuthProperties) { + try { + // 플랫폼(kakao)의 client, provider Map 획득 + OAuthProperties.Client kakaoClient = oAuthProperties.getClient().get(PLATFORM); + OAuthProperties.Provider kakaoProvider = oAuthProperties.getProvider().get(PLATFORM); + + this.authorizationUri = kakaoProvider.authorizationUri(); + this.clientId = kakaoClient.clientId(); + this.redirectUri = kakaoClient.redirectUri(); + this.tokenUri = kakaoProvider.tokenUri(); + this.clientSecret = kakaoClient.clientSecret(); + this.profileUri = kakaoProvider.profileUri(); + + } catch (NullPointerException e) { + log.error(">>>> OAuthProperties NullPointerException 발생: {}", ExceptionMessage.OAUTH_CONFIG_NULL); + throw new OAuthException(ExceptionMessage.OAUTH_CONFIG_NULL); + } + } + // "https://kauth.kakao.com/oauth/authorize?..." + @Override + public String authorize(String state) { + return authorizationUri + + "?response_type=code" // OAuth 인증 코드 그랜트 유형: code로 고정 + + "&client_id=" + clientId // 클라이언트 ID + + "&redirect_uri=" + redirectUri // 리다이렉트 URI + + "&state=" + state // CSRF 방지 + + "&scope=openid"; // 리소스 접근 범위: openid로 고정 + } + + // "https://kauth.kakao.com/oauth/token?..." + @Override + public String token(String code, String state) { + return tokenUri + + "?grant_type=authorization_code" // OAuth 인증 코드 그랜트 유형: code로 고정 + + "&client_id=" + clientId // 클라이언트 ID + + "&client_secret=" + clientSecret // 클라이언트 Secret + + "&redirect_uri=" + redirectUri // 리다이렉트 URI + + "&code=" + code; // authorize() 요청으로 얻은 인가 코드 + + } + // "https://kapi.kakao.com/v2/user/me" + @Override + public String profile() { return profileUri; } +} From 215149e8efd3f33d699b5c42fe96118bc20f1f3f Mon Sep 17 00:00:00 2001 From: kooyeongmin Date: Tue, 16 Jan 2024 17:36:05 +0900 Subject: [PATCH 18/34] [BE] feat(#29): OAuthService MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 카카오 플랫폼 추가 --- .../backend/auth/api/service/oauth/OAuthService.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/example/backend/auth/api/service/oauth/OAuthService.java b/backend/src/main/java/com/example/backend/auth/api/service/oauth/OAuthService.java index f9094d817..78a347279 100644 --- a/backend/src/main/java/com/example/backend/auth/api/service/oauth/OAuthService.java +++ b/backend/src/main/java/com/example/backend/auth/api/service/oauth/OAuthService.java @@ -3,8 +3,10 @@ import com.example.backend.auth.api.controller.auth.response.AuthLoginPageResponse; import com.example.backend.auth.api.service.oauth.adapter.OAuthAdapter; import com.example.backend.auth.api.service.oauth.adapter.github.GithubAdapter; +import com.example.backend.auth.api.service.oauth.adapter.kakao.KakaoAdapter; import com.example.backend.auth.api.service.oauth.builder.OAuthURLBuilder; import com.example.backend.auth.api.service.oauth.builder.github.GithubURLBuilder; +import com.example.backend.auth.api.service.oauth.builder.kakao.KakaoURLBuilder; import com.example.backend.auth.api.service.oauth.response.OAuthResponse; import com.example.backend.domain.define.user.constant.UserPlatformType; import lombok.extern.slf4j.Slf4j; @@ -17,6 +19,7 @@ import java.util.stream.Collectors; import static com.example.backend.domain.define.user.constant.UserPlatformType.GITHUB; +import static com.example.backend.domain.define.user.constant.UserPlatformType.KAKAO; @Slf4j @Service @@ -25,13 +28,19 @@ public class OAuthService { private Map adapterMap; // 플랫폼별 Adapter, URLBuilder 등록 - public OAuthService(GithubAdapter githubAdapter, GithubURLBuilder githubURLBuilder) { + public OAuthService(GithubAdapter githubAdapter, GithubURLBuilder githubURLBuilder , KakaoAdapter kakaoAdapter, KakaoURLBuilder kakaoURLBuilder) { this.adapterMap = new HashMap<>() {{ // 깃허브 플랫폼 추가 put(GITHUB, OAuthFactory.builder() .oAuthAdapter(githubAdapter) .oAuthURLBuilder(githubURLBuilder) .build()); + // 카카오 플랫폼 추가 + put(KAKAO, OAuthFactory.builder() + .oAuthAdapter(kakaoAdapter) + .oAuthURLBuilder(kakaoURLBuilder) + .build()); + }}; } @@ -70,7 +79,6 @@ public OAuthResponse login(UserPlatformType platformType, String code, String st // Access Token 획득 String accessToken = adapter.getToken(tokenUrl); - // 사용자 프로필 조회 OAuthResponse userInfo = adapter.getProfile(accessToken); log.info(">>>> {} Login Success", platformType); From 73168819c19e3cb7cb9af357c6ff517926fe7f29 Mon Sep 17 00:00:00 2001 From: kooyeongmin Date: Tue, 16 Jan 2024 17:42:33 +0900 Subject: [PATCH 19/34] =?UTF-8?q?[BE]=20feat(#29):=20Kakao=20Token=20Clien?= =?UTF-8?q?ts,=20Response=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Token uri로 요청 구현 * Token을 요청 후 반환받을 DTO 구현 --- .../oauth/kakao/KakaoTokenClients.java | 20 +++++++++++++++++++ .../kakao/response/KakaoTokenResponse.java | 19 ++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 backend/src/main/java/com/example/backend/external/clients/oauth/kakao/KakaoTokenClients.java create mode 100644 backend/src/main/java/com/example/backend/external/clients/oauth/kakao/response/KakaoTokenResponse.java diff --git a/backend/src/main/java/com/example/backend/external/clients/oauth/kakao/KakaoTokenClients.java b/backend/src/main/java/com/example/backend/external/clients/oauth/kakao/KakaoTokenClients.java new file mode 100644 index 000000000..5ab731e37 --- /dev/null +++ b/backend/src/main/java/com/example/backend/external/clients/oauth/kakao/KakaoTokenClients.java @@ -0,0 +1,20 @@ +package com.example.backend.external.clients.oauth.kakao; + + +import com.example.backend.external.annotation.ExternalClients; +import com.example.backend.external.clients.oauth.kakao.response.KakaoTokenResponse; +import org.springframework.http.MediaType; +import org.springframework.web.service.annotation.PostExchange; + +import java.net.URI; + +/* + AccessToken을 얻기 위해 설정한 token URI로 POST 요청을 보낸다. + */ +@ExternalClients(baseUrl = "oauth2.provider.kakao.token-uri") +public interface KakaoTokenClients { + + // 요청의 content Type: FORM_URLENCODED 형식 ex) key1=value1&key2=value2 + @PostExchange(contentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + public KakaoTokenResponse getToken(URI uri); +} diff --git a/backend/src/main/java/com/example/backend/external/clients/oauth/kakao/response/KakaoTokenResponse.java b/backend/src/main/java/com/example/backend/external/clients/oauth/kakao/response/KakaoTokenResponse.java new file mode 100644 index 000000000..be3e7b344 --- /dev/null +++ b/backend/src/main/java/com/example/backend/external/clients/oauth/kakao/response/KakaoTokenResponse.java @@ -0,0 +1,19 @@ +package com.example.backend.external.clients.oauth.kakao.response; + + +import lombok.Getter; +import lombok.NoArgsConstructor; + +/* + Kakao에 Access Token을 요청 후 반환받을 DTO + */ +@Getter +@NoArgsConstructor +public class KakaoTokenResponse { + private String access_token; + + public KakaoTokenResponse(String access_token) { + this.access_token = access_token; + } +} + From 7dcead9be62e1ab03cc4779056773da8394b49ae Mon Sep 17 00:00:00 2001 From: kooyeongmin Date: Tue, 16 Jan 2024 17:46:03 +0900 Subject: [PATCH 20/34] =?UTF-8?q?[BE]=20feat(#29):=20Kakao=20Profile=20Cli?= =?UTF-8?q?ents,=20Response=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 프로필 uri로 요청 구현 * 프로필을 요청 후 반환받을 DTO 구현 * 카카오에선 email을 제공하지 않음 * response 받는 형식을 다르게 함 --- .../oauth/kakao/KakaoProfileClients.java | 17 +++++++++ .../kakao/response/KakaoProfileResponse.java | 35 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 backend/src/main/java/com/example/backend/external/clients/oauth/kakao/KakaoProfileClients.java create mode 100644 backend/src/main/java/com/example/backend/external/clients/oauth/kakao/response/KakaoProfileResponse.java diff --git a/backend/src/main/java/com/example/backend/external/clients/oauth/kakao/KakaoProfileClients.java b/backend/src/main/java/com/example/backend/external/clients/oauth/kakao/KakaoProfileClients.java new file mode 100644 index 000000000..d0c756c44 --- /dev/null +++ b/backend/src/main/java/com/example/backend/external/clients/oauth/kakao/KakaoProfileClients.java @@ -0,0 +1,17 @@ +package com.example.backend.external.clients.oauth.kakao; + +import com.example.backend.external.annotation.ExternalClients; +import com.example.backend.external.clients.oauth.kakao.response.KakaoProfileResponse; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.service.annotation.GetExchange; + +/* + 사용자 정보를 얻기 위해 설정한 profile 요청 URI로 GET 요청을 보낸다. + */ +@ExternalClients(baseUrl = "https://kapi.kakao.com/v2/user/me") +public interface KakaoProfileClients { + + @GetExchange + public KakaoProfileResponse getProfile(@RequestHeader(value = "Authorization") String header); + +} diff --git a/backend/src/main/java/com/example/backend/external/clients/oauth/kakao/response/KakaoProfileResponse.java b/backend/src/main/java/com/example/backend/external/clients/oauth/kakao/response/KakaoProfileResponse.java new file mode 100644 index 000000000..e5abbd2e6 --- /dev/null +++ b/backend/src/main/java/com/example/backend/external/clients/oauth/kakao/response/KakaoProfileResponse.java @@ -0,0 +1,35 @@ +package com.example.backend.external.clients.oauth.kakao.response; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class KakaoProfileResponse { + private Long id; + private String name; + private String email; + + private Properties properties; + + public KakaoProfileResponse(Long id, String name, String email, Properties properties) { + this.id = id; + this.name = name; + this.email = email; + this.properties = properties; + } + + @Getter + @NoArgsConstructor + public static class Properties { + private String nickname; + private String profile_image; + private String thumbnail_image; + + public Properties(String nickname, String profile_image, String thumbnail_image) { + this.nickname = nickname; + this.profile_image = profile_image; + this.thumbnail_image = thumbnail_image; + } + } +} \ No newline at end of file From 5eecf8c9712521b6e6fab26abcc972f3227e1577 Mon Sep 17 00:00:00 2001 From: kooyeongmin Date: Tue, 16 Jan 2024 17:52:24 +0900 Subject: [PATCH 21/34] [BE] test(#29) Kakao Adapter Test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 토큰 요청 테스트 * 프로필 요쳥 테스트 --- .../oauth/adapter/kakao/KakaoAdapterTest.java | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapterTest.java diff --git a/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapterTest.java b/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapterTest.java new file mode 100644 index 000000000..c00cf1951 --- /dev/null +++ b/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapterTest.java @@ -0,0 +1,125 @@ +package com.example.backend.auth.api.service.oauth.adapter.kakao; + + +import com.example.backend.auth.TestConfig; +import com.example.backend.auth.api.service.oauth.builder.kakao.KakaoURLBuilder; + +import com.example.backend.auth.api.service.oauth.response.OAuthResponse; +import com.example.backend.common.exception.ExceptionMessage; +import com.example.backend.common.exception.oauth.OAuthException; +import com.example.backend.external.clients.oauth.kakao.KakaoProfileClients; +import com.example.backend.external.clients.oauth.kakao.KakaoTokenClients; +import com.example.backend.external.clients.oauth.kakao.response.KakaoProfileResponse; +import com.example.backend.external.clients.oauth.kakao.response.KakaoTokenResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.net.URI; + +import static com.example.backend.domain.define.user.constant.UserPlatformType.KAKAO; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class KakaoAdapterTest extends TestConfig { + @Autowired + private KakaoAdapter kakaoAdapter; + + @Autowired + private KakaoURLBuilder kakaoURLBuilder; + @Test + @DisplayName("kakao 토큰 요청 API에 정상적인 요청을 보내면, access_token이 발행된다.") + void kakaoAdapterGetTokenSuccess() { + // given + KakaoAdapterTest.MockKakaoTokenClients mockKakaoTokenClients = new KakaoAdapterTest.MockKakaoTokenClients(); + KakaoAdapterTest.MockKakaoProfileClients mockKakaoProfileClients = new KakaoAdapterTest.MockKakaoProfileClients(); + KakaoAdapter kakaoAdapter = new KakaoAdapter(mockKakaoTokenClients, mockKakaoProfileClients); + + // when + String accessToken = kakaoAdapter.getToken("tokenUrl"); + + // then + System.out.println("accessToken = " + accessToken); + assertThat(accessToken).isEqualTo("access-token"); + + } + + + @Test + @DisplayName("kakao 토큰 요청 중 예외가 발생하면, OAUTH_INVALID_TOKEN_URL 에외가 발생한다.") + void kakaoAdapterGetTokenFail() { + // given + String tokenURL = kakaoURLBuilder.token("error-token", "state"); + + // when + OAuthException exception = assertThrows(OAuthException.class, + () -> kakaoAdapter.getToken(tokenURL)); + + // then + assertThat(exception.getMessage()).isEqualTo(ExceptionMessage.OAUTH_INVALID_TOKEN_URL.getText()); + + } + + + + + @Test + @DisplayName("kakao 프로필 요청 API에 정상적인 요청을 보내면, 사용자 프로필이 반환된다.") + void kakaoAdapterGetProfileSuccess() { + // given + KakaoAdapterTest.MockKakaoTokenClients mockKakaoTokenClients = new KakaoAdapterTest.MockKakaoTokenClients(); + KakaoAdapterTest.MockKakaoProfileClients mockKakaoProfileClients = new KakaoAdapterTest.MockKakaoProfileClients(); + KakaoAdapter kakaoAdapter = new KakaoAdapter(mockKakaoTokenClients, mockKakaoProfileClients); + + // when + OAuthResponse profile = kakaoAdapter.getProfile("access-token"); + + // then + assertAll( + () -> assertThat(profile.getPlatformId()).isEqualTo("1"), + () -> assertThat(profile.getEmail()).isEqualTo("이메일 없음"), + () -> assertThat(profile.getProfileImageUrl()).isEqualTo("http://k.kakaocdn.net/dn/1G9kp/btsAot8liOn/8CWudi3uy07rvFNUkk3ER0/img_640x640.jpg"), + () -> assertThat(profile.getName()).isEqualTo("구영민"), + () -> assertThat(profile.getPlatformType()).isEqualTo(KAKAO) + ); + } + + + @Test // X + @DisplayName("kakao 프로필 요청 중 예외가 발생하면, OAUTH_INVALID_ACCESS_TOKEN 예외가 발생한다.") + void kakaoAdapterGetProfileFail() { + // when + OAuthException exception = assertThrows(OAuthException.class, + () -> kakaoAdapter.getProfile("error-token")); + + // then + assertThat(exception.getMessage()).isEqualTo(ExceptionMessage.OAUTH_INVALID_ACCESS_TOKEN.getText()); + + } + + + + + + static class MockKakaoTokenClients implements KakaoTokenClients { + + @Override + public KakaoTokenResponse getToken(URI uri) { + return new KakaoTokenResponse("access-token"); + } + } + static class MockKakaoProfileClients implements KakaoProfileClients { + + @Override + public KakaoProfileResponse getProfile(String header) { + return new KakaoProfileResponse(1L, + "구영민", + "이메일 없음", + new KakaoProfileResponse.Properties("구영민", + "http://k.kakaocdn.net/dn/1G9kp/btsAot8liOn/8CWudi3uy07rvFNUkk3ER0/img_640x640.jpg", + "http://k.kakaocdn.net/dn/1G9kp/btsAot8liOn/8CWudi3uy07rvFNUkk3ER0/img_110x110.jpg")) + ; + } + } +} From 00c4f5d3862749508741ede19eb7f6c62178a78d Mon Sep 17 00:00:00 2001 From: kooyeongmin Date: Tue, 16 Jan 2024 17:53:37 +0900 Subject: [PATCH 22/34] [BE] test(#29): OAuth Kakao URL Builder Test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 인가 코드 요청 URL 생성 테스트 * access token 요청 URL 생성 테스트 * 사용자 정보 요청 URL 생성 테스트 --- .../builder/kakao/KakaoURLBuilderTest.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 backend/src/test/java/com/example/backend/auth/api/service/oauth/builder/kakao/KakaoURLBuilderTest.java diff --git a/backend/src/test/java/com/example/backend/auth/api/service/oauth/builder/kakao/KakaoURLBuilderTest.java b/backend/src/test/java/com/example/backend/auth/api/service/oauth/builder/kakao/KakaoURLBuilderTest.java new file mode 100644 index 000000000..0b2009fd0 --- /dev/null +++ b/backend/src/test/java/com/example/backend/auth/api/service/oauth/builder/kakao/KakaoURLBuilderTest.java @@ -0,0 +1,76 @@ +package com.example.backend.auth.api.service.oauth.builder.kakao; + +import com.example.backend.auth.TestConfig; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +import static org.assertj.core.api.Assertions.assertThat; + +public class KakaoURLBuilderTest extends TestConfig { + @Autowired + private KakaoURLBuilder urlBuilder; + + @Value("${oauth2.client.kakao.client-id}") private String clientId; + @Value("${oauth2.client.kakao.client-secret}") private String clientSecret; + @Value("${oauth2.client.kakao.redirect-uri}") private String redirectUri; + + @Value("${oauth2.provider.kakao.authorization-uri}") String authorizationUri; + @Value("${oauth2.provider.kakao.token-uri}") private String tokenUri; + @Value("${oauth2.provider.kakao.profile-uri}") private String profileUri; + + @Test + @DisplayName("authorize(인가 코드 요청) URL을 성공적으로 생성한다.") + void authorizeURIBuildSuccess() { + // given + String state = "testState"; + + // when + String authorizeURL = urlBuilder.authorize(state); + + // then + System.out.println("authorize URL : " + authorizeURL); + assertThat(authorizeURL).isEqualTo(authorizationUri + + "?response_type=code" + + "&client_id=" + clientId + + "&redirect_uri=" + redirectUri + + "&state=" + state + + "&scope=openid"); + } + + + @Test + @DisplayName("token(Access Token 요청) URL을 성공적으로 생성한다.") + void tokenURIBuildSuccess() { + // given + String code = "testCode"; + String state = "testState"; + + // when + String tokenURL = urlBuilder.token(code, state); + + // then + System.out.println("tokenURL : " + tokenURL); + assertThat(tokenURL).isEqualTo(tokenUri + + "?grant_type=authorization_code" + + "&client_id=" + clientId + + "&client_secret=" + clientSecret + + "&redirect_uri=" + redirectUri + + "&code=" + code); + } + + @Test + @DisplayName("profile(사용자 정보 요청) URL을 성공적으로 생성한다.") + void profileURIBuildSuccess() { + // given + + // when + String profileURL = urlBuilder.profile(); + + // then + System.out.println("profileURL : " + profileURL); + assertThat(profileURL).isEqualTo(profileUri); + + } +} From ff90cd9a3f084b19349fe7857280066eae26c8b0 Mon Sep 17 00:00:00 2001 From: kooyeongmin Date: Tue, 16 Jan 2024 17:56:04 +0900 Subject: [PATCH 23/34] [BE] test(#29): OAuth Service Test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 모든 플랫폼 로그인 페이지 반환 test에 카카오 추가 * 카카오 로그인 성공, 실패 테스트 추가 --- .../api/service/oauth/OAuthServiceTest.java | 66 ++++++++++++++++++- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/backend/src/test/java/com/example/backend/auth/api/service/oauth/OAuthServiceTest.java b/backend/src/test/java/com/example/backend/auth/api/service/oauth/OAuthServiceTest.java index 3229c070b..4d866b336 100644 --- a/backend/src/test/java/com/example/backend/auth/api/service/oauth/OAuthServiceTest.java +++ b/backend/src/test/java/com/example/backend/auth/api/service/oauth/OAuthServiceTest.java @@ -3,7 +3,9 @@ import com.example.backend.auth.TestConfig; import com.example.backend.auth.api.controller.auth.response.AuthLoginPageResponse; import com.example.backend.auth.api.service.oauth.adapter.github.GithubAdapter; +import com.example.backend.auth.api.service.oauth.adapter.kakao.KakaoAdapter; import com.example.backend.auth.api.service.oauth.builder.github.GithubURLBuilder; +import com.example.backend.auth.api.service.oauth.builder.kakao.KakaoURLBuilder; import com.example.backend.auth.api.service.oauth.response.OAuthResponse; import com.example.backend.common.exception.oauth.OAuthException; import org.junit.jupiter.api.DisplayName; @@ -14,6 +16,7 @@ import java.util.List; import static com.example.backend.domain.define.user.constant.UserPlatformType.GITHUB; +import static com.example.backend.domain.define.user.constant.UserPlatformType.KAKAO; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -29,6 +32,10 @@ class OAuthServiceTest extends TestConfig { @MockBean private GithubAdapter githubAdapter; + @Autowired + private KakaoURLBuilder kakaoUrlBuilder; + @MockBean + private KakaoAdapter kakaoAdapter; @Test @DisplayName("모든 플랫폼의 로그인 페이지를 성공적으로 반환한다.") void allUrlBuilderSuccess() { @@ -38,9 +45,20 @@ void allUrlBuilderSuccess() { // when List loginPages = oAuthService.loginPage(state); String authorizeURL = urlBuilder.authorize(state); - + String authorizeURLkakao = kakaoUrlBuilder.authorize(state); // then - assertThat(loginPages.get(0).getUrl()).isEqualTo(authorizeURL); + assertThat(loginPages).hasSize(2); // 리스트 크기 확인 + + // 각 플랫폼별 URL인지 확인 + boolean containsGithub = loginPages.stream() + .anyMatch(page -> page.getPlatformType().equals(GITHUB) && + page.getUrl().equals(authorizeURL)); + boolean containsKakao = loginPages.stream() + .anyMatch(page -> page.getPlatformType().equals(KAKAO) && + page.getUrl().equals(authorizeURLkakao)); + + assertThat(containsGithub).isTrue(); + assertThat(containsKakao).isTrue(); } @Test @@ -73,7 +91,35 @@ void githubLoginSuccess() { } @Test - @DisplayName("깃허브 로그인에 실하면 OAuthException 예외가 발생한다.") + @DisplayName("카카오 로그인에 성공하면 OAuthResponse 객체를 반환한다.") + void kakaoLoginSuccess() { + // given + String code = "valid-code"; + String state = "valid-state"; + + OAuthResponse response = OAuthResponse.builder() + .platformId("1") + .platformType(KAKAO) + .email("이메일 없음") + .name("구영민") + .profileImageUrl("http://k.kakaocdn.net/dn/1G9kp/btsAot8liOn/8CWudi3uy07rvFNUkk3ER0/img_640x640.jpg") + .build(); + + // when + // when 사용시 Mockito 패키지 사용 + when(kakaoAdapter.getToken(any(String.class))).thenReturn("access-token"); + when(kakaoAdapter.getProfile(any(String.class))).thenReturn(response); + OAuthResponse profile = oAuthService.login(KAKAO, code, state); + + // then + assertThat(profile) + .extracting("platformId", "platformType", "email", "name", "profileImageUrl") + .contains("1", KAKAO, "이메일 없음", "구영민", "http://k.kakaocdn.net/dn/1G9kp/btsAot8liOn/8CWudi3uy07rvFNUkk3ER0/img_640x640.jpg"); + + + } + @Test + @DisplayName("깃허브 로그인에 실패하면 OAuthException 예외가 발생한다.") void githubLoginFail() { // given String code = "invalid-code"; @@ -85,4 +131,18 @@ void githubLoginFail() { () -> oAuthService.login(GITHUB, code, state)); } + + @Test + @DisplayName("카카오 로그인에 실패하면 OAuthException 예외가 발생한다.") + void kakaoLoginFail() { + // given + String code = "invalid-code"; + String state = "invalid-state"; + + // when + when(kakaoAdapter.getToken(any(String.class))).thenThrow(OAuthException.class); + assertThrows(OAuthException.class, + () -> oAuthService.login(KAKAO, code, state)); + + } } \ No newline at end of file From 19536eb524a5dc1eaadf5900494d36f8ac0cd3f6 Mon Sep 17 00:00:00 2001 From: kooyeongmin Date: Tue, 16 Jan 2024 17:57:45 +0900 Subject: [PATCH 24/34] =?UTF-8?q?[BE]=20test(#29):=20Kakao=20Client=20Test?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 추후에 구현 예정 --- .../clients/oauth/kakao/KakaoClientTest.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 backend/src/test/java/com/example/backend/external/clients/oauth/kakao/KakaoClientTest.java diff --git a/backend/src/test/java/com/example/backend/external/clients/oauth/kakao/KakaoClientTest.java b/backend/src/test/java/com/example/backend/external/clients/oauth/kakao/KakaoClientTest.java new file mode 100644 index 000000000..d0abdfc1a --- /dev/null +++ b/backend/src/test/java/com/example/backend/external/clients/oauth/kakao/KakaoClientTest.java @@ -0,0 +1,55 @@ +package com.example.backend.external.clients.oauth.kakao; + +import com.example.backend.auth.TestConfig; +import com.example.backend.external.clients.oauth.kakao.response.KakaoProfileResponse; +import com.example.backend.external.clients.oauth.kakao.response.KakaoTokenResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.net.URI; + +import static org.junit.jupiter.api.Assertions.assertNull; + +public class KakaoClientTest extends TestConfig { + @Autowired + private KakaoTokenClients kakaoTokenClients; + + @Autowired + private KakaoProfileClients kakaoProfileClients; + + @Test + @DisplayName("인가 code를 URL에 담아 요청해 access_token을 성공적으로 받환받는다.") + void kakaoTokenRequestTest() { + // given + //String uri = "https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id=1d8513aae332ebe7462f429d67f3cacc&client_secret=T2dW4O4bFTPYz7PKflnIlqqfYXNbr2U6&redirect_uri=http://localhost:8080/auth/KAKAO/login&code="; + + + // when + //KakaoTokenResponse token = kakaoTokenClients.getToken(URI.create(uri)); + //System.out.println("token = " + token.getAccess_token()); + + // then + // 현재는 동적인 테스트 불가하므로 null. OauthService 구현 후 동적으로 테스트할 예정 + //assertNull(token.getAccess_token()); + } + + @Test + @DisplayName("Access Token을 URL에 담아 요청해 사용자 정보를 성공적으로 받환받는다.") + void githubProfileRequestTest() { + // given + String accessToken = ""; + + /*KakaoProfileResponse profile = kakaoProfileClients.getProfile("Bearer " + accessToken); + + System.out.println("login = " + profile.getLogin()); + System.out.println("email = " + profile.getEmail()); + System.out.println("name = " + profile.getName()); + + assertAll( + () -> assertThat(profile.getLogin()).isEqualTo("jusung-c"), + () -> assertThat(profile.getEmail()).isEqualTo("anaooauc1236@naver.com"), + () -> assertThat(profile.getName()).isEqualTo("이주성") + );*/ + } +} From 81c7785ee813aa687459961a1dfee4a587a6c464 Mon Sep 17 00:00:00 2001 From: j-ra1n Date: Wed, 17 Jan 2024 21:13:00 +0900 Subject: [PATCH 25/34] =?UTF-8?q?[BE]=20refactor(#30):=20Email=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * email 정보 삭제 --- .../auth/api/service/oauth/adapter/google/GoogleAdapter.java | 1 - .../clients/oauth/google/response/GoogleProfileResponse.java | 4 +--- .../api/service/oauth/adapter/google/GoogleAdapterTest.java | 2 -- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapter.java b/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapter.java index f88490bbf..2d553be4a 100644 --- a/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapter.java +++ b/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapter.java @@ -48,7 +48,6 @@ public OAuthResponse getProfile(String accessToken) { return OAuthResponse.builder() .platformId(profile.getSub()) .platformType(GOOGLE) - .email(profile.getEmail()) .name(profile.getName()) .profileImageUrl(profile.getPicture()) .build(); diff --git a/backend/src/main/java/com/example/backend/external/clients/oauth/google/response/GoogleProfileResponse.java b/backend/src/main/java/com/example/backend/external/clients/oauth/google/response/GoogleProfileResponse.java index 18417f9e9..90a3c66db 100644 --- a/backend/src/main/java/com/example/backend/external/clients/oauth/google/response/GoogleProfileResponse.java +++ b/backend/src/main/java/com/example/backend/external/clients/oauth/google/response/GoogleProfileResponse.java @@ -13,14 +13,12 @@ public class GoogleProfileResponse { private String sub; // Google에서 사용하는 사용자의 고유 식별자 (subject) private String name; - private String email; private String picture; - public GoogleProfileResponse(String sub, String name, String email, String picture) { + public GoogleProfileResponse(String sub, String name, String picture) { this.sub = sub; this.name = name; - this.email = email; this.picture = picture; } } diff --git a/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapterTest.java b/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapterTest.java index 40b8cac2d..17c0bda9e 100644 --- a/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapterTest.java +++ b/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapterTest.java @@ -76,7 +76,6 @@ void googleAdapterGetProfileSuccess() { // then assertAll( () -> assertThat(profile.getPlatformId()).isEqualTo("102514823309503386675"), // google은 sub - () -> assertThat(profile.getEmail()).isEqualTo("xw21yog@dankook.ac.kr"), () -> assertThat(profile.getProfileImageUrl()).isEqualTo("https://lh3.googleusercontent.com/a/ACg8ocLrP_GLo-fUjSmnUZedPZbbL7ifImYTnelh108XkgOx=s96-c"), () -> assertThat(profile.getName()).isEqualTo("이정우"), () -> assertThat(profile.getPlatformType()).isEqualTo(GOOGLE) @@ -112,7 +111,6 @@ static class MockGoogleProfileClients implements GoogleProfileClients { public GoogleProfileResponse getProfile(String header) { return new GoogleProfileResponse("102514823309503386675", "이정우", - "xw21yog@dankook.ac.kr", "https://lh3.googleusercontent.com/a/ACg8ocLrP_GLo-fUjSmnUZedPZbbL7ifImYTnelh108XkgOx=s96-c"); } } From 0f942162ec6c9273937cc5329b8ff563e872b0bf Mon Sep 17 00:00:00 2001 From: kooyeongmin Date: Wed, 17 Jan 2024 21:21:16 +0900 Subject: [PATCH 26/34] =?UTF-8?q?[BE]=20refactor(#29):=20Email=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Email 관련 모두 삭제 --- .../auth/api/service/oauth/adapter/kakao/KakaoAdapter.java | 1 - .../clients/oauth/kakao/response/KakaoProfileResponse.java | 4 +--- .../api/service/oauth/adapter/kakao/KakaoAdapterTest.java | 2 -- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapter.java b/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapter.java index 073e900d1..702ead502 100644 --- a/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapter.java +++ b/backend/src/main/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapter.java @@ -48,7 +48,6 @@ public OAuthResponse getProfile(String accessToken) { return OAuthResponse.builder() .platformId(profile.getId().toString()) .platformType(KAKAO) - .email(profile.getEmail()) .name(profile.getProperties().getNickname()) .profileImageUrl(profile.getProperties().getProfile_image()) .build(); diff --git a/backend/src/main/java/com/example/backend/external/clients/oauth/kakao/response/KakaoProfileResponse.java b/backend/src/main/java/com/example/backend/external/clients/oauth/kakao/response/KakaoProfileResponse.java index e5abbd2e6..8a20f5d08 100644 --- a/backend/src/main/java/com/example/backend/external/clients/oauth/kakao/response/KakaoProfileResponse.java +++ b/backend/src/main/java/com/example/backend/external/clients/oauth/kakao/response/KakaoProfileResponse.java @@ -8,14 +8,12 @@ public class KakaoProfileResponse { private Long id; private String name; - private String email; private Properties properties; - public KakaoProfileResponse(Long id, String name, String email, Properties properties) { + public KakaoProfileResponse(Long id, String name, Properties properties) { this.id = id; this.name = name; - this.email = email; this.properties = properties; } diff --git a/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapterTest.java b/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapterTest.java index c00cf1951..2c52e8264 100644 --- a/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapterTest.java +++ b/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapterTest.java @@ -78,7 +78,6 @@ void kakaoAdapterGetProfileSuccess() { // then assertAll( () -> assertThat(profile.getPlatformId()).isEqualTo("1"), - () -> assertThat(profile.getEmail()).isEqualTo("이메일 없음"), () -> assertThat(profile.getProfileImageUrl()).isEqualTo("http://k.kakaocdn.net/dn/1G9kp/btsAot8liOn/8CWudi3uy07rvFNUkk3ER0/img_640x640.jpg"), () -> assertThat(profile.getName()).isEqualTo("구영민"), () -> assertThat(profile.getPlatformType()).isEqualTo(KAKAO) @@ -115,7 +114,6 @@ static class MockKakaoProfileClients implements KakaoProfileClients { public KakaoProfileResponse getProfile(String header) { return new KakaoProfileResponse(1L, "구영민", - "이메일 없음", new KakaoProfileResponse.Properties("구영민", "http://k.kakaocdn.net/dn/1G9kp/btsAot8liOn/8CWudi3uy07rvFNUkk3ER0/img_640x640.jpg", "http://k.kakaocdn.net/dn/1G9kp/btsAot8liOn/8CWudi3uy07rvFNUkk3ER0/img_110x110.jpg")) From e8511f87c906ab348d61c2cb80abe40bf6f6f9b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=8D=EC=B0=AC=ED=9D=AC?= Date: Thu, 18 Jan 2024 08:03:14 +0900 Subject: [PATCH 27/34] chore: dev pull request CI --- .github/workflows/CI_dev_pull_request | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/CI_dev_pull_request diff --git a/.github/workflows/CI_dev_pull_request b/.github/workflows/CI_dev_pull_request new file mode 100644 index 000000000..440d1daf7 --- /dev/null +++ b/.github/workflows/CI_dev_pull_request @@ -0,0 +1,37 @@ +name: Spring Boot & Gradle CI Jobs (With. dev branches pull_request) + +on: + pull_request: + branches: [ dev ] + +jobs: + build: + # 실행 환경 (Git Runners 개인 서버) + runs-on: self-hosted + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'adopt' + + # application.yml 파일 설정 + - name: resources 폴더 생성 + run: | + mkdir -p ./backend/src/main/resources + + - name: yml 파일 생성 + run: | + echo "${{ secrets.APPLICATION_DEFAULT }}" > ./backend/src/main/resources/application.yml + echo "${{ secrets.APPLICATION_LOCAL }}" > ./backend/src/main/resources/application-local.yml + echo "${{ secrets.APPLICATION_TEST }}" > ./backend/src/main/resources/application-test.yml + + # gradlew를 실행시키기 위해 권한 부여 + - name: Gradlew에게 실행권한 부여 + run: chmod +x gradlew + + # 멀티모듈 빌드하기 + - name: 멀티모듈 전체 빌드 + run: ./gradlew clean build -x test From ce1610177925a4cf3aedbd07449bfb66c3ccefa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=8D=EC=B0=AC=ED=9D=AC?= Date: Thu, 18 Jan 2024 08:11:15 +0900 Subject: [PATCH 28/34] =?UTF-8?q?chore:=20=ED=8C=8C=EC=9D=BC=EB=AA=85=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflows/{CI_dev_pull_request => CI_dev_pull_request.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{CI_dev_pull_request => CI_dev_pull_request.yml} (100%) diff --git a/.github/workflows/CI_dev_pull_request b/.github/workflows/CI_dev_pull_request.yml similarity index 100% rename from .github/workflows/CI_dev_pull_request rename to .github/workflows/CI_dev_pull_request.yml From ff417815c008d627c169d5a24095e9824bc648d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=8D=EC=B0=AC=ED=9D=AC?= Date: Thu, 18 Jan 2024 08:13:22 +0900 Subject: [PATCH 29/34] =?UTF-8?q?chore:=20gradlew=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CI_dev_pull_request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI_dev_pull_request.yml b/.github/workflows/CI_dev_pull_request.yml index 440d1daf7..616039982 100644 --- a/.github/workflows/CI_dev_pull_request.yml +++ b/.github/workflows/CI_dev_pull_request.yml @@ -30,8 +30,8 @@ jobs: # gradlew를 실행시키기 위해 권한 부여 - name: Gradlew에게 실행권한 부여 - run: chmod +x gradlew + run: chmod +x ./backend/gradlew # 멀티모듈 빌드하기 - name: 멀티모듈 전체 빌드 - run: ./gradlew clean build -x test + run: ./backend/gradlew clean build -x test From b3eee01aefd39ff1876fb752f609e58ee895b36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=8D=EC=B0=AC=ED=9D=AC?= Date: Thu, 18 Jan 2024 08:23:32 +0900 Subject: [PATCH 30/34] =?UTF-8?q?chore:=20CI=20=ED=8C=8C=EC=9D=B4=ED=94=84?= =?UTF-8?q?=EB=9D=BC=EC=9D=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ull_request.yml => CI_dev_be_pull_request.yml} | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) rename .github/workflows/{CI_dev_pull_request.yml => CI_dev_be_pull_request.yml} (60%) diff --git a/.github/workflows/CI_dev_pull_request.yml b/.github/workflows/CI_dev_be_pull_request.yml similarity index 60% rename from .github/workflows/CI_dev_pull_request.yml rename to .github/workflows/CI_dev_be_pull_request.yml index 616039982..9b4135dd5 100644 --- a/.github/workflows/CI_dev_pull_request.yml +++ b/.github/workflows/CI_dev_be_pull_request.yml @@ -17,21 +17,24 @@ jobs: java-version: '17' distribution: 'adopt' + - name: backend 폴더로 이동 + run: cd ./backend + # application.yml 파일 설정 - name: resources 폴더 생성 run: | - mkdir -p ./backend/src/main/resources + mkdir -p ./src/main/resources - name: yml 파일 생성 run: | - echo "${{ secrets.APPLICATION_DEFAULT }}" > ./backend/src/main/resources/application.yml - echo "${{ secrets.APPLICATION_LOCAL }}" > ./backend/src/main/resources/application-local.yml - echo "${{ secrets.APPLICATION_TEST }}" > ./backend/src/main/resources/application-test.yml + echo "${{ secrets.APPLICATION_DEFAULT }}" > ./src/main/resources/application.yml + echo "${{ secrets.APPLICATION_LOCAL }}" > ./src/main/resources/application-local.yml + echo "${{ secrets.APPLICATION_TEST }}" > ./src/main/resources/application-test.yml # gradlew를 실행시키기 위해 권한 부여 - name: Gradlew에게 실행권한 부여 - run: chmod +x ./backend/gradlew + run: chmod +x ./gradlew # 멀티모듈 빌드하기 - name: 멀티모듈 전체 빌드 - run: ./backend/gradlew clean build -x test + run: ./gradlew clean build -x test From 456152128c87e426d18c41c4f2e980d46e949647 Mon Sep 17 00:00:00 2001 From: j-ra1n Date: Thu, 18 Jan 2024 10:34:37 +0900 Subject: [PATCH 31/34] =?UTF-8?q?[BE]=20refactor(#30):=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * println 주석 처리 * 검증시 검증할 값 변수로 수정 --- .../oauth/adapter/google/GoogleAdapterTest.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapterTest.java b/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapterTest.java index 17c0bda9e..b3f1dbeed 100644 --- a/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapterTest.java +++ b/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapterTest.java @@ -42,7 +42,7 @@ void googleAdapterGetTokenSuccess() { String accessToken = googleAdapter.getToken("tokenUrl"); // then - System.out.println("accessToken = " + accessToken); + //System.out.println("accessToken = " + accessToken); assertThat(accessToken).isEqualTo("access-token"); } @@ -66,6 +66,11 @@ void googleAdapterGetTokenFail() { @DisplayName("google 프로필 요청 API에 정상적인 요청을 보내면, 사용자 프로필이 반환된다.") void googleAdapterGetProfileSuccess() { // given + + String expectedPlatformId = "102514823309503386675"; // google은 sub + String expectedProfileImageUrl = "https://lh3.googleusercontent.com/a/ACg8ocLrP_GLo-fUjSmnUZedPZbbL7ifImYTnelh108XkgOx=s96-c"; + String expectedName = "이정우"; + GoogleAdapterTest.MockGoogleTokenClients mockGoogleTokenClients = new GoogleAdapterTest.MockGoogleTokenClients(); GoogleAdapterTest.MockGoogleProfileClients mockGoogleProfileClients = new GoogleAdapterTest.MockGoogleProfileClients(); GoogleAdapter googleAdapter = new GoogleAdapter(mockGoogleTokenClients, mockGoogleProfileClients); @@ -75,9 +80,9 @@ void googleAdapterGetProfileSuccess() { // then assertAll( - () -> assertThat(profile.getPlatformId()).isEqualTo("102514823309503386675"), // google은 sub - () -> assertThat(profile.getProfileImageUrl()).isEqualTo("https://lh3.googleusercontent.com/a/ACg8ocLrP_GLo-fUjSmnUZedPZbbL7ifImYTnelh108XkgOx=s96-c"), - () -> assertThat(profile.getName()).isEqualTo("이정우"), + () -> assertThat(profile.getPlatformId()).isEqualTo(expectedPlatformId), // google은 sub + () -> assertThat(profile.getProfileImageUrl()).isEqualTo(expectedProfileImageUrl), + () -> assertThat(profile.getName()).isEqualTo(expectedName), () -> assertThat(profile.getPlatformType()).isEqualTo(GOOGLE) ); } From 29240fc6667ff2d797f637dcd61ce50b0ea9dfd2 Mon Sep 17 00:00:00 2001 From: j-ra1n Date: Thu, 18 Jan 2024 11:59:37 +0900 Subject: [PATCH 32/34] =?UTF-8?q?[BE]=20refactor(#30):=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 코드 재수정 --- .../oauth/adapter/google/GoogleAdapterTest.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapterTest.java b/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapterTest.java index b3f1dbeed..2b6b075f4 100644 --- a/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapterTest.java +++ b/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/google/GoogleAdapterTest.java @@ -30,6 +30,10 @@ public class GoogleAdapterTest extends TestConfig { @Autowired private GoogleURLBuilder googleURLBuilder; + public static String expectedPlatformId = "102514823309503386675"; // google은 sub + public static String expectedProfileImageUrl = "https://lh3.googleusercontent.com/a/ACg8ocLrP_GLo-fUjSmnUZedPZbbL7ifImYTnelh108XkgOx=s96-c"; + public static String expectedName = "이정우"; + @Test @DisplayName("google 토큰 요청 API에 정상적인 요청을 보내면, access_token이 발행된다.") void googleAdapterGetTokenSuccess() { @@ -67,10 +71,6 @@ void googleAdapterGetTokenFail() { void googleAdapterGetProfileSuccess() { // given - String expectedPlatformId = "102514823309503386675"; // google은 sub - String expectedProfileImageUrl = "https://lh3.googleusercontent.com/a/ACg8ocLrP_GLo-fUjSmnUZedPZbbL7ifImYTnelh108XkgOx=s96-c"; - String expectedName = "이정우"; - GoogleAdapterTest.MockGoogleTokenClients mockGoogleTokenClients = new GoogleAdapterTest.MockGoogleTokenClients(); GoogleAdapterTest.MockGoogleProfileClients mockGoogleProfileClients = new GoogleAdapterTest.MockGoogleProfileClients(); GoogleAdapter googleAdapter = new GoogleAdapter(mockGoogleTokenClients, mockGoogleProfileClients); @@ -114,9 +114,9 @@ static class MockGoogleProfileClients implements GoogleProfileClients { @Override public GoogleProfileResponse getProfile(String header) { - return new GoogleProfileResponse("102514823309503386675", - "이정우", - "https://lh3.googleusercontent.com/a/ACg8ocLrP_GLo-fUjSmnUZedPZbbL7ifImYTnelh108XkgOx=s96-c"); + return new GoogleProfileResponse(GoogleAdapterTest.expectedPlatformId, + GoogleAdapterTest.expectedName, + GoogleAdapterTest.expectedProfileImageUrl); } } } From 7703355d97cfb12df5b39a31568dba6cf04a2b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=8D=EC=B0=AC=ED=9D=AC?= Date: Thu, 18 Jan 2024 12:15:56 +0900 Subject: [PATCH 33/34] =?UTF-8?q?chore:=20=EB=94=94=EB=A0=89=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=9D=B4=EB=8F=99=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CI_dev_be_pull_request.yml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/CI_dev_be_pull_request.yml b/.github/workflows/CI_dev_be_pull_request.yml index 9b4135dd5..033601916 100644 --- a/.github/workflows/CI_dev_be_pull_request.yml +++ b/.github/workflows/CI_dev_be_pull_request.yml @@ -17,24 +17,23 @@ jobs: java-version: '17' distribution: 'adopt' - - name: backend 폴더로 이동 - run: cd ./backend - # application.yml 파일 설정 - name: resources 폴더 생성 run: | - mkdir -p ./src/main/resources + mkdir -p ./backend/src/main/resources - name: yml 파일 생성 run: | - echo "${{ secrets.APPLICATION_DEFAULT }}" > ./src/main/resources/application.yml - echo "${{ secrets.APPLICATION_LOCAL }}" > ./src/main/resources/application-local.yml - echo "${{ secrets.APPLICATION_TEST }}" > ./src/main/resources/application-test.yml + echo "${{ secrets.APPLICATION_DEFAULT }}" > ./backend/src/main/resources/application.yml + echo "${{ secrets.APPLICATION_LOCAL }}" > ./backend/src/main/resources/application-local.yml + echo "${{ secrets.APPLICATION_TEST }}" > ./backend/src/main/resources/application-test.yml # gradlew를 실행시키기 위해 권한 부여 - name: Gradlew에게 실행권한 부여 - run: chmod +x ./gradlew + run: chmod +x ./backend/gradlew # 멀티모듈 빌드하기 - name: 멀티모듈 전체 빌드 - run: ./gradlew clean build -x test + run: | + cd ./backend + ./gradlew clean build -x test From fe8b36e145a8b12f708e3534000f46bf7025df5c Mon Sep 17 00:00:00 2001 From: kooyeongmin Date: Thu, 18 Jan 2024 12:24:57 +0900 Subject: [PATCH 34/34] =?UTF-8?q?[BE]=20refactor(#29)=20:=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * kakaoAdapterTest.kakaoAdapterGetProfileSuccess() 테스트 given, then 변수화 --- .../oauth/adapter/kakao/KakaoAdapterTest.java | 47 +++++++++++++++---- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapterTest.java b/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapterTest.java index 2c52e8264..432cf8444 100644 --- a/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapterTest.java +++ b/backend/src/test/java/com/example/backend/auth/api/service/oauth/adapter/kakao/KakaoAdapterTest.java @@ -68,8 +68,16 @@ void kakaoAdapterGetTokenFail() { @DisplayName("kakao 프로필 요청 API에 정상적인 요청을 보내면, 사용자 프로필이 반환된다.") void kakaoAdapterGetProfileSuccess() { // given + Long expetedId = 1L; + String expectedName = "구영민"; + String expectedNickName = "구영민"; + String expectedProfileImageUrl = "http://k.kakaocdn.net/dn/1G9kp/btsAot8liOn/8CWudi3uy07rvFNUkk3ER0/img_640x640.jpg"; + String expectedThumbnailImage = "http://k.kakaocdn.net/dn/1G9kp/btsAot8liOn/8CWudi3uy07rvFNUkk3ER0/img_110x110.jpg"; + KakaoAdapterTest.MockKakaoTokenClients mockKakaoTokenClients = new KakaoAdapterTest.MockKakaoTokenClients(); - KakaoAdapterTest.MockKakaoProfileClients mockKakaoProfileClients = new KakaoAdapterTest.MockKakaoProfileClients(); + KakaoAdapterTest.MockKakaoProfileClients mockKakaoProfileClients = new KakaoAdapterTest.MockKakaoProfileClients(expetedId, + expectedName, + new KakaoProfileResponse.Properties(expectedNickName, expectedProfileImageUrl, expectedThumbnailImage)); KakaoAdapter kakaoAdapter = new KakaoAdapter(mockKakaoTokenClients, mockKakaoProfileClients); // when @@ -77,9 +85,9 @@ void kakaoAdapterGetProfileSuccess() { // then assertAll( - () -> assertThat(profile.getPlatformId()).isEqualTo("1"), - () -> assertThat(profile.getProfileImageUrl()).isEqualTo("http://k.kakaocdn.net/dn/1G9kp/btsAot8liOn/8CWudi3uy07rvFNUkk3ER0/img_640x640.jpg"), - () -> assertThat(profile.getName()).isEqualTo("구영민"), + () -> assertThat(profile.getPlatformId()).isEqualTo(expetedId.toString()), + () -> assertThat(profile.getProfileImageUrl()).isEqualTo(expectedProfileImageUrl), + () -> assertThat(profile.getName()).isEqualTo(expectedName), () -> assertThat(profile.getPlatformType()).isEqualTo(KAKAO) ); } @@ -109,14 +117,33 @@ public KakaoTokenResponse getToken(URI uri) { } } static class MockKakaoProfileClients implements KakaoProfileClients { - + private Long id; + private String name; + private KakaoProfileResponse.Properties properties; + MockKakaoProfileClients(Long id, String name, KakaoProfileResponse.Properties properties){ + this.id = id; + this.name = name; + this.properties = properties; + } + MockKakaoProfileClients(){}; + public static class Properties { + private String nickname; + private String profile_image; + private String thumbnail_image; + + public Properties(String nickname, String profile_image, String thumbnail_image) { + this.nickname = nickname; + this.profile_image = profile_image; + this.thumbnail_image = thumbnail_image; + } + } @Override public KakaoProfileResponse getProfile(String header) { - return new KakaoProfileResponse(1L, - "구영민", - new KakaoProfileResponse.Properties("구영민", - "http://k.kakaocdn.net/dn/1G9kp/btsAot8liOn/8CWudi3uy07rvFNUkk3ER0/img_640x640.jpg", - "http://k.kakaocdn.net/dn/1G9kp/btsAot8liOn/8CWudi3uy07rvFNUkk3ER0/img_110x110.jpg")) + return new KakaoProfileResponse(id, + name, + new KakaoProfileResponse.Properties(properties.getNickname(), + properties.getProfile_image(), + properties.getThumbnail_image())) ; } }