Skip to content

Commit

Permalink
Github Social Login 4차 구현 (woowacourse-teams#164)
Browse files Browse the repository at this point in the history
* chore: HttpInterface 사용을 위한 의존성 추가

* feat: 소셜 타입 구분을 위한 OauthType 추가

* feat: AuthCode 를 받아오기 위한 인터페이스와 확장성을 고려한 컴포시트 패턴 추가

* feat: Oauth 정보를 받아올 인터페이스와 확장성을 위한 컴포시트 패턴 구현

* feat: 외부 변수를 받아올 GithubOauthConfig 구현 및 최상단 애플리케이션에 읽어올 수 있도록 스캔 어노테이션 적용

* feat: Github Oauth 요청을 위한 구현체 및 스프링 컨테이너에 빈 등록을 위한 HttpInterfaceConfig 설정 구현

* feat: Oauth 로그인을 받을 컨트롤러와 저장 및 조회를 위한 Oauth 레포지터리, Oauth 서비스 구현

* chore: gradle 에 auth0 jwt 의존성 추가

* feat: AccessToken dto 구현 추가 및 OauthInformation 내부 회사 정보 삭제

* feat: OauthType enum 컨버터 구현

* feat: Jwt 인코더, Jwt 디코더 구현

* feat: Oauth 전용 Runner, Supporter 레포지터리 구현

* feat: Runner, Supporter Principal 어노테이션 및 Runner, Supporter ArgumentResolver 구현

* feat: Oauth 서비스 Jwt 인코드 기능 및 Member, Runner, Supporter 신규 사용자 자동 회원가입 추가, Oauth 컨트롤러 Jwt 헤더 반환 구현

* test: WebMvcTest 시 bean 스캔 문제 해결을 위한 커스텀 MockMvcTest 어노테이션 구현 및 RestdocsConfig 수정

* test: Oauth Api 테스트 및 기존 Api Test 를 MockMvcTest 로 수정

* refactor: auth0 의존성을 삭제하고 jjwt 추가 후 HS256 를 이용하도록 JWT 설정 변경

* refactor: AccessToken JWT 내부 Claim 에서 삭제

* refactor: oauth 패키지 이동

* refactor: JWT 디코더 내부 검증 변경

* refactor: Oauth 사용자 정의 예외로 변경

* chore: 서브 모듈 최신화

* fix: 외부 환경 변수 자바 객체 주입을 문자열로 수정

* refactor: cors origin 외부 환경 변수 주입하도록 변경

* refactor: JWT Signature 클라이언트 요청 예외 에러 코드 변경

* refactor: Oauth Email 에러 코드 JWT 로 변경

* feat: Jwt 디코더 에러 코드 추가

* style: 문자열 상수화 및 메서드 파라미터 컨벤션 적용

* dev 환경 변수 수정 (woowacourse-teams#188)

fix: 환경 변수 수정

* refactor: AccessToken 명 SocialToken 으로 변경 및 Auth 관련 패키지 분리

---------

Co-authored-by: Jeonghoon Park <[email protected]>
  • Loading branch information
2 people authored and eunbii0213 committed Aug 13, 2023
1 parent a499ca4 commit feccc7b
Show file tree
Hide file tree
Showing 43 changed files with 1,066 additions and 11 deletions.
4 changes: 4 additions & 0 deletions backend/baton/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-mysql'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
Expand Down
2 changes: 2 additions & 0 deletions backend/baton/src/main/java/touch/baton/BatonApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@ConfigurationPropertiesScan
@SpringBootApplication
public class BatonApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package touch.baton.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import touch.baton.domain.oauth.controller.resolver.AuthRunnerPrincipalArgumentResolver;
import touch.baton.domain.oauth.controller.resolver.AuthSupporterPrincipalArgumentResolver;

import java.util.List;

@RequiredArgsConstructor
@Configuration
public class ArgumentResolverConfig extends WebMvcConfig {

private final AuthRunnerPrincipalArgumentResolver authRunnerPrincipalArgumentResolver;
private final AuthSupporterPrincipalArgumentResolver authSupporterPrincipalArgumentResolver;

@Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(authRunnerPrincipalArgumentResolver);
resolvers.add(authSupporterPrincipalArgumentResolver);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package touch.baton.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import touch.baton.infra.auth.oauth.github.http.GithubHttpInterface;

import static org.springframework.web.reactive.function.client.support.WebClientAdapter.forClient;

@Configuration
public class OauthHttpInterfaceConfig {

@Bean
public GithubHttpInterface githubHttpClient() {
return createHttpClient(GithubHttpInterface.class);
}

private <T> T createHttpClient(final Class<T> clazz) {
return HttpServiceProxyFactory
.builder(forClient(WebClient.create()))
.build()
.createClient(clazz);
}
}
12 changes: 9 additions & 3 deletions backend/baton/src/main/java/touch/baton/config/WebMvcConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import static org.springframework.http.HttpHeaders.AUTHORIZATION;
import static org.springframework.http.HttpHeaders.LOCATION;
import static org.springframework.http.HttpMethod.*;
import static org.springframework.http.HttpMethod.DELETE;
import static org.springframework.http.HttpMethod.GET;
import static org.springframework.http.HttpMethod.OPTIONS;
import static org.springframework.http.HttpMethod.PATCH;
import static org.springframework.http.HttpMethod.POST;
import static org.springframework.http.HttpMethod.PUT;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
Expand All @@ -18,9 +24,9 @@ public class WebMvcConfig implements WebMvcConfigurer {
public void addCorsMappings(final CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(allowedOrigin)
.allowCredentials(false)
.allowCredentials(true)
.allowedMethods(GET.name(), POST.name(), PUT.name(), PATCH.name(), DELETE.name(), OPTIONS.name())
.exposedHeaders(LOCATION)
.exposedHeaders(LOCATION, AUTHORIZATION)
.maxAge(3600);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class ConverterConfig implements WebMvcConfigurer {
@Override
public void addFormatters(final FormatterRegistry registry) {
registry.addConverter(new StringDateToLocalDateTimeConverter(DEFAULT_DATE_TIME_FORMAT, KOREA_TIME_ZONE));
registry.addConverter(new OauthTypeConverter());
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package touch.baton.config.converter;

import org.springframework.core.convert.converter.Converter;
import touch.baton.domain.oauth.OauthType;

public class OauthTypeConverter implements Converter<String, OauthType> {

@Override
public OauthType convert(final String source) {
return OauthType.from(source);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,16 @@ public enum ClientErrorCode {
CONTENTS_OVERFLOW(HttpStatus.BAD_REQUEST, "RP005", "내용은 1000자 까지 입력해주세요."),
PAST_DEADLINE(HttpStatus.BAD_REQUEST, "RP006", "마감일은 오늘보다 과거일 수 없습니다."),
CONTENTS_NOT_FOUND(HttpStatus.NOT_FOUND, "RP007", "존재하지 않는 게시물입니다."),
TAGS_ARE_NULL(HttpStatus.BAD_REQUEST, "RP008", "태그 목록을 빈 값이라도 입력해주세요.");
TAGS_ARE_NULL(HttpStatus.BAD_REQUEST, "RP008", "태그 목록을 빈 값이라도 입력해주세요."),
COMPANY_IS_NULL(HttpStatus.BAD_REQUEST, "OM001", "사용자의 회사 정보를 입력해주세요."),
OAUTH_REQUEST_URL_PROVIDER_IS_WRONG(HttpStatus.BAD_REQUEST, "OA001", "redirect 할 url 이 조회되지 않는 잘못된 소셜 타입입니다."),
OAUTH_INFORMATION_CLIENT_IS_WRONG(HttpStatus.BAD_REQUEST, "OA002", " 소셜 계정 정보를 조회할 수 없는 잘못된 소셜 타입입니다."),
OAUTH_AUTHORIZATION_VALUE_IS_NULL(HttpStatus.BAD_REQUEST, "OA003", "Authorization 값을 입력해주세요."),
OAUTH_AUTHORIZATION_BEARER_TYPE_NOT_FOUND(HttpStatus.BAD_REQUEST, "OA004", "Authorization 값을 Bearer 타입으로 입력해주세요."),
JWT_SIGNATURE_IS_WRONG(HttpStatus.BAD_REQUEST, "JW001", "시그니처가 다른 잘못된 JWT 입니다."),
JWT_FORM_IS_WRONG(HttpStatus.BAD_REQUEST, "JW002", "잘못 생성된 JWT 로 디코딩 할 수 없습니다."),
JWT_CLAIM_IS_WRONG(HttpStatus.BAD_REQUEST, "JW003", "JWT 에 기대한 정보를 모두 포함하고 있지 않습니다."),
JWT_CLAIM_EMAIL_IS_WRONG(HttpStatus.BAD_REQUEST, "JW004", "사용자의 잘못된 이메일 정보를 가진 JWT 입니다.");

private final HttpStatus httpStatus;
private final String errorCode;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package touch.baton.domain.oauth;


import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import touch.baton.domain.member.vo.Email;
import touch.baton.domain.member.vo.GithubUrl;
import touch.baton.domain.member.vo.ImageUrl;
import touch.baton.domain.member.vo.MemberName;
import touch.baton.domain.member.vo.OauthId;

import static lombok.AccessLevel.PROTECTED;

@EqualsAndHashCode
@Getter
@NoArgsConstructor(access = PROTECTED)
public class OauthInformation {

private SocialToken socialToken;

private OauthId oauthId;

private MemberName memberName;

private Email email;

private GithubUrl githubUrl;

private ImageUrl imageUrl;

@Builder
private OauthInformation(final SocialToken socialToken,
final OauthId oauthId,
final MemberName memberName,
final Email email,
final GithubUrl githubUrl,
final ImageUrl imageUrl
) {
this.socialToken = socialToken;
this.oauthId = oauthId;
this.memberName = memberName;
this.email = email;
this.githubUrl = githubUrl;
this.imageUrl = imageUrl;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package touch.baton.domain.oauth;

import static java.util.Locale.ENGLISH;

public enum OauthType {

GITHUB;

public static OauthType from(final String name) {
return OauthType.valueOf(name.toUpperCase(ENGLISH));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package touch.baton.domain.oauth;

import lombok.EqualsAndHashCode;
import lombok.Getter;

@EqualsAndHashCode
@Getter
public final class SocialToken {

private final String value;

public SocialToken(final String value) {
this.value = value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package touch.baton.domain.oauth.authcode;

import touch.baton.domain.oauth.OauthType;

public interface AuthCodeRequestUrlProvider {

OauthType oauthServer();

String getRequestUrl();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package touch.baton.domain.oauth.authcode;

import org.springframework.stereotype.Component;
import touch.baton.domain.common.exception.ClientErrorCode;
import touch.baton.domain.oauth.OauthType;
import touch.baton.domain.oauth.exception.OauthRequestException;

import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static java.util.function.Function.identity;

@Component
public class AuthCodeRequestUrlProviderComposite {

private final Map<OauthType, AuthCodeRequestUrlProvider> authCodeProviders;

public AuthCodeRequestUrlProviderComposite(final Set<AuthCodeRequestUrlProvider> authCodeProviders) {
this.authCodeProviders = authCodeProviders.stream()
.collect(Collectors.toMap(
AuthCodeRequestUrlProvider::oauthServer, identity()
));
}

public String findRequestUrl(final OauthType oauthType) {
return findAuthCodeProvider(oauthType).getRequestUrl();
}

private AuthCodeRequestUrlProvider findAuthCodeProvider(final OauthType oauthType) {
return Optional.ofNullable(authCodeProviders.get(oauthType))
.orElseThrow(() -> new OauthRequestException(ClientErrorCode.OAUTH_REQUEST_URL_PROVIDER_IS_WRONG));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package touch.baton.domain.oauth.client;

import touch.baton.domain.oauth.OauthInformation;
import touch.baton.domain.oauth.OauthType;

public interface OauthInformationClient {

OauthType oauthType();

OauthInformation fetchInformation(final String authCode);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package touch.baton.domain.oauth.client;

import org.springframework.stereotype.Component;
import touch.baton.domain.common.exception.ClientErrorCode;
import touch.baton.domain.oauth.OauthInformation;
import touch.baton.domain.oauth.OauthType;
import touch.baton.domain.oauth.exception.OauthRequestException;

import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static java.util.function.Function.identity;

@Component
public class OauthInformationClientComposite {

private final Map<OauthType, OauthInformationClient> clients;

public OauthInformationClientComposite(final Set<OauthInformationClient> clients) {
this.clients = clients.stream()
.collect(Collectors.toMap(
OauthInformationClient::oauthType,
identity()
));
}

public OauthInformation fetchInformation(final OauthType oauthType, final String authCode) {
return Optional.ofNullable(clients.get(oauthType))
.orElseThrow(() -> new OauthRequestException(ClientErrorCode.OAUTH_INFORMATION_CLIENT_IS_WRONG))
.fetchInformation(authCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package touch.baton.domain.oauth.controller;

import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import touch.baton.domain.oauth.OauthType;
import touch.baton.domain.oauth.service.OauthService;

import java.io.IOException;

import static org.springframework.http.HttpHeaders.AUTHORIZATION;
import static org.springframework.http.HttpStatus.FOUND;

@RequiredArgsConstructor
@RequestMapping("/oauth")
@RestController
public class OauthController {

private final OauthService oauthService;

@GetMapping("/{oauthType}")
public ResponseEntity<Void> redirectAuthCode(@PathVariable("oauthType") final OauthType oauthType,
final HttpServletResponse response
) throws IOException {
final String redirectUrl = oauthService.readAuthCodeRedirect(oauthType);
response.sendRedirect(redirectUrl);

return ResponseEntity.status(FOUND).build();
}

@PostMapping("/login/{oauthType}")
public ResponseEntity<Void> login(@PathVariable final OauthType oauthType,
@RequestParam final String code
) {
final String jwtToken = oauthService.login(oauthType, code);

return ResponseEntity.ok()
.header(AUTHORIZATION, jwtToken)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package touch.baton.domain.oauth.controller.resolver;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthRunnerPrincipal {
}
Loading

0 comments on commit feccc7b

Please sign in to comment.