Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Google OAuth2 Token 검증 기능 개발 #43

Merged
merged 19 commits into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
de603c5
Feat(#42): @Authenticated 적용을 위한 Resolver 작성
sjhjack Nov 3, 2024
6f1c859
Rename(#42): Member, SocialMember 위치 변경
sjhjack Nov 3, 2024
adc2dfd
Feat(#42): Google Token 유효성 검사 요청 API 작성
sjhjack Nov 3, 2024
1dbbd83
Feat(#42): application에서 google client 사용하기 위한 interface와 구현 클래스 추가
sjhjack Nov 3, 2024
5ae2c46
Feat(#42): 토큰 유효성 검사 응답 레코드 추가
sjhjack Nov 3, 2024
b7f44fd
Feat(#42): Member 도메인 Repository 추가
sjhjack Nov 3, 2024
1c6d817
Feat(#42): Member 도메인 Exception 추가
sjhjack Nov 3, 2024
c4889d5
Feat(#42): 토큰 유효성 관련 Exception 추가
sjhjack Nov 3, 2024
89577ae
Refactor(#42): AuthenticatedArgumentResolver의 파라미터 추가
sjhjack Nov 3, 2024
67dc676
Refactor(#42): ScheduleInfoMapper의 toJpaScheduleInfo 오버로딩
sjhjack Nov 3, 2024
d2df374
Refactor(#42): ReviewController의 AuthMember, Authenticated import 위치 변경
sjhjack Nov 3, 2024
c2eaa52
Fix(#42): getForObject의 쿼리 파라미터가 세팅되지 않는 현상 해결
sjhjack Nov 6, 2024
5dfc5e3
Feat(#42): google-oauth-client에 Jacoco 추가
sjhjack Nov 6, 2024
c928b4f
Feat(#42): 토큰 검증 테스트 작성
sjhjack Nov 6, 2024
bbef5ac
Refactor(#42): 토큰 유효성 검증 uri 상수화
sjhjack Nov 9, 2024
6080154
Move(#42): AuthMemberService 패키지 변경
sjhjack Nov 9, 2024
f76186a
Move(#42): GoogleOAuthProperties 패키지 변경
sjhjack Nov 9, 2024
460de87
Move(#42): GoogleOAuthClient import 변경
sjhjack Nov 9, 2024
5aeff39
Refactor(#42): VALIDATION_URI을 static으로 변경
sjhjack Nov 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.wypl.wyplcore.auth;

import java.util.Optional;

import org.springframework.stereotype.Component;

import com.wypl.googleoauthclient.GoogleOAuthClient;
import com.wypl.googleoauthclient.service.AuthMemberService;
import com.wypl.googleoauthclient.data.response.GoogleTokenValidationResponse;
import com.wypl.googleoauthclient.domain.AuthMember;
import com.wypl.jpamemberdomain.member.OauthProvider;
import com.wypl.jpamemberdomain.member.domain.SocialMember;
import com.wypl.jpamemberdomain.member.SocialMemberRepository;
import com.wypl.jpamemberdomain.member.exception.MemberErrorCode;
import com.wypl.jpamemberdomain.member.exception.MemberException;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class AuthMemberServiceImpl implements AuthMemberService {
private final GoogleOAuthClient googleOAuthClient;
private final SocialMemberRepository socialMemberRepository;

@Override
public AuthMember getValidatedMemberId(String accessToken) {
GoogleTokenValidationResponse response = googleOAuthClient.validateToken(accessToken);
Optional<SocialMember> optionalSocialMember
= socialMemberRepository.findByOauthProviderAndOauthId(OauthProvider.GOOGLE, response.userId());

if (optionalSocialMember.isEmpty()) {
throw new MemberException(MemberErrorCode.NO_SUCH_MEMBER);
}
return AuthMember.of(optionalSocialMember.get().getId(), accessToken);
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package com.wypl.wyplcore.global.config;
package com.wypl.wyplcore.config;

import java.util.List;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.wypl.wyplcore.auth.utils.AuthenticatedArgumentResolver;
import com.wypl.googleoauthclient.service.AuthMemberService;
import com.wypl.googleoauthclient.utils.AuthenticatedArgumentResolver;

import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final AuthMemberService authMemberService;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new AuthenticatedArgumentResolver());
resolvers.add(new AuthenticatedArgumentResolver(authMemberService));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
import org.springframework.web.bind.annotation.RestController;

import com.wypl.applicationcommon.WyplResponseEntity;
import com.wypl.wyplcore.auth.annotation.Authenticated;
import com.wypl.wyplcore.auth.domain.AuthMember;
import com.wypl.googleoauthclient.annotation.Authenticated;
import com.wypl.googleoauthclient.domain.AuthMember;
import com.wypl.wyplcore.review.data.request.ReviewCreateRequest;
import com.wypl.wyplcore.review.data.request.ReviewType;
import com.wypl.wyplcore.review.data.request.ReviewUpdateRequest;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import org.springframework.transaction.annotation.Transactional;

import com.wypl.jpacalendardomain.calendar.domain.Schedule;
import com.wypl.jpamemberdomain.member.Member;
import com.wypl.jpamemberdomain.member.domain.Member;
import com.wypl.jpamongoreviewdomain.review.domain.Review;
import com.wypl.jpamongoreviewdomain.review.repository.ReviewRepository;
import com.wypl.jpamongoreviewdomain.reviewcontents.domain.BlockType;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.wypl.wyplcore.review.utils;

import com.wypl.common.exception.CallConstructorException;
import com.wypl.jpamemberdomain.member.Member;
import com.wypl.jpamemberdomain.member.domain.Member;
import com.wypl.jpamongoreviewdomain.review.domain.Review;
import com.wypl.jpamongoreviewdomain.review.repository.ReviewRepository;
import com.wypl.wyplcore.review.exception.ReviewErrorCode;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package com.wypl.wyplcore.schedule.controller;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.wypl.applicationcommon.WyplResponseEntity;
import com.wypl.wyplcore.auth.annotation.Authenticated;
import com.wypl.wyplcore.auth.domain.AuthMember;
import com.wypl.googleoauthclient.annotation.Authenticated;
import com.wypl.googleoauthclient.domain.AuthMember;
import com.wypl.wyplcore.schedule.data.request.ScheduleCreateRequest;
import com.wypl.wyplcore.schedule.data.response.ScheduleInfoCreateResponse;
import com.wypl.wyplcore.schedule.service.ScheduleService;

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
package com.wypl.wyplcore.schedule.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.wypl.googleoauthclient.domain.AuthMember;
import com.wypl.jpacalendardomain.calendar.domain.Calendar;
import com.wypl.jpacalendardomain.calendar.domain.Schedule;
import com.wypl.jpacalendardomain.calendar.domain.ScheduleInfo;
import com.wypl.jpacalendardomain.calendar.mapper.ScheduleInfoMapper;
import com.wypl.jpacalendardomain.calendar.mapper.ScheduleMapper;
import com.wypl.jpacalendardomain.calendar.repository.ScheduleInfoRepository;
import com.wypl.jpacalendardomain.calendar.repository.ScheduleRepository;
import com.wypl.jpamemberdomain.member.Member;
import com.wypl.wyplcore.auth.domain.AuthMember;
import com.wypl.jpamemberdomain.member.domain.Member;
import com.wypl.wyplcore.schedule.data.request.ScheduleCreateRequest;
import com.wypl.wyplcore.schedule.data.response.ScheduleInfoCreateResponse;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


@Service
Expand All @@ -29,7 +31,7 @@ public ScheduleInfoCreateResponse createSchedule(AuthMember authMember, Schedule
Calendar foundCalendar = null; // FIXME: scheduleInfoRequest의 calendarId로 찾는다. foundCalendar 엔티티 검증 필요.
Member foundMember = null; // FIXME: member 엔티티 검증 필요.

ScheduleInfo scheduleInfo = ScheduleInfoMapper.toJpaScheduleInfo(foundCalendar, authMember);
ScheduleInfo scheduleInfo = ScheduleInfoMapper.toJpaScheduleInfo(foundCalendar, authMember.id());
Schedule schedule = ScheduleMapper.toJpaSchedule(scheduleCreateRequest, scheduleInfo);

ScheduleInfo savedScheduleInfo = scheduleInfoRepository.save(scheduleInfo);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.wypl.wyplcore.auth;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.BDDMockito.*;

import java.util.Optional;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.wypl.googleoauthclient.GoogleOAuthClient;
import com.wypl.googleoauthclient.data.response.GoogleTokenValidationResponse;
import com.wypl.googleoauthclient.domain.AuthMember;
import com.wypl.jpamemberdomain.member.OauthProvider;
import com.wypl.jpamemberdomain.member.SocialMemberRepository;
import com.wypl.jpamemberdomain.member.domain.SocialMember;
import com.wypl.jpamemberdomain.member.exception.MemberErrorCode;
import com.wypl.jpamemberdomain.member.exception.MemberException;

@ExtendWith(MockitoExtension.class)
class AuthMemberServiceImplTest {
@InjectMocks
private AuthMemberServiceImpl authMemberService;
@Mock
private GoogleOAuthClient googleOAuthClient;
@Mock
private SocialMemberRepository socialMemberRepository;

@DisplayName("현재의 토큰을 검증한다.")
@Nested
class GetValidatedMemberId {

private static final String OAUTH_ID = "OAUTH_ID";

@BeforeEach
void setUp() {
given(googleOAuthClient.validateToken(anyString()))
.willReturn(new GoogleTokenValidationResponse(OAUTH_ID, "[email protected]"));
}

@DisplayName("AuthMember 정상적으로 생성된다.")
@Test
void success() {
// Given
String accessToken = "accessToken";
// Todo : Fixture 만들어서 분리하기!
SocialMember mockSocialMember = SocialMember.builder()
.id(1L)
.oauthProvider(OauthProvider.GOOGLE)
.OauthId(OAUTH_ID)
.build();

given(socialMemberRepository.findByOauthProviderAndOauthId(any(OauthProvider.class), anyString()))
.willReturn(Optional.of(mockSocialMember));

// When
AuthMember authMember = authMemberService.getValidatedMemberId(accessToken);

// Then
assertThat(authMember).isNotNull();
assertThat(authMember.id()).isEqualTo(1L);
assertThat(authMember.accessToken()).isEqualTo(accessToken);
}

@DisplayName("토큰으로 멤버 정보를 조회하지 못한다.")
@Test
void socialMemberEmptyTest() {
// Given
given(socialMemberRepository.findByOauthProviderAndOauthId(any(OauthProvider.class), anyString()))
.willReturn(Optional.empty());

// When & Then
assertThatThrownBy(() -> authMemberService.getValidatedMemberId(anyString()))
.isInstanceOf(MemberException.class)
.hasMessageContaining(MemberErrorCode.NO_SUCH_MEMBER.getMessage());
}
}
}
46 changes: 45 additions & 1 deletion client/google-oauth-client/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,48 @@ dependencies {

implementation('org.springframework.boot:spring-boot-starter-web')
implementation('org.springframework.boot:spring-boot-starter')
}
}

/* Jacoco Start */
tasks.withType(JacocoReport).configureEach {
reports {
html.required.set(true)
xml.required.set(true)
html.outputLocation.set(file("reports/jacoco/index.xml"))
xml.outputLocation.set(file("reports/jacoco/test/jacocoTestReport.xml"))
}

classDirectories.setFrom(
files(classDirectories.files.collect {
fileTree(it) {
exclude(
"**/annotation/**",
"**/data/**",
"**/exception/**",
)
}
})
)
}

tasks.jacocoTestCoverageVerification {
violationRules {
rule {
enabled = true
element = 'CLASS'

limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.80D
}

excludes = [
"**/annotation/**",
"**/data/**",
"**/exception/**",
]
}
}
}
/* Jacoco End */
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.wypl.googleoauthclient;

import java.util.HashMap;
import java.util.Map;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
Expand All @@ -11,9 +14,12 @@

import com.wypl.common.exception.GlobalErrorCode;
import com.wypl.common.exception.WyplException;
import com.wypl.googleoauthclient.config.GoogleOAuthProperties;
import com.wypl.googleoauthclient.data.response.GoogleTokenResponse;
import com.wypl.googleoauthclient.data.response.GoogleTokenValidationResponse;
import com.wypl.googleoauthclient.exception.GoogleOAuthErrorCode;
import com.wypl.googleoauthclient.exception.GoogleOAuthException;
import com.wypl.googleoauthclient.utils.GoogleOAuthParamFactory;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -23,6 +29,7 @@
@RequiredArgsConstructor
@Component
public class GoogleOAuthClient {
private static final String VALIDATION_URI = "https://www.googleapis.com/oauth2/v1/tokeninfo";

private final GoogleOAuthProperties googleOAuthProperties;
private final RestTemplate restTemplate;
Expand All @@ -46,6 +53,19 @@ public GoogleTokenResponse fetchRefreshGoogleOAuthToken(String refreshToken) {
return requestToken(params);
}

public GoogleTokenValidationResponse validateToken(String accessToken) {
Map<String, String> params = new HashMap<>();
params.put("access_token", accessToken);

try {
return restTemplate.getForObject(VALIDATION_URI + "?access_token={access_token}"
, GoogleTokenValidationResponse.class
, params);
} catch (HttpClientErrorException e) {
throw new GoogleOAuthException(GoogleOAuthErrorCode.INVALID_TOKEN);
}
}

private GoogleTokenResponse requestToken(MultiValueMap<String, String> params) {
HttpEntity<MultiValueMap<String, String>> formEntity = getHttpEntity(params);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.wypl.wyplcore.auth.annotation;
package com.wypl.googleoauthclient.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
Expand Down
Loading
Loading