diff --git a/be/issue-tracker-be/.gitignore b/be/issue-tracker-be/.gitignore index c2065bc26..515a8fe9e 100644 --- a/be/issue-tracker-be/.gitignore +++ b/be/issue-tracker-be/.gitignore @@ -35,3 +35,6 @@ out/ ### VS Code ### .vscode/ + +### auth +auth.properties diff --git a/be/issue-tracker-be/build.gradle b/be/issue-tracker-be/build.gradle index 7551cba17..769bc9644 100644 --- a/be/issue-tracker-be/build.gradle +++ b/be/issue-tracker-be/build.gradle @@ -24,12 +24,15 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-hateoas' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.projectlombok:lombok:1.18.16' testImplementation 'org.springframework.boot:spring-boot-starter-test' annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' annotationProcessor 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'mysql:mysql-connector-java' + implementation 'com.auth0:java-jwt:3.8.3' + implementation 'org.springdoc:springdoc-openapi-ui:1.5.8' implementation 'org.springdoc:springdoc-openapi-data-rest:1.5.8' diff --git a/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/config/InterceptorConfig.java b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/config/InterceptorConfig.java new file mode 100644 index 000000000..9afc2f7c8 --- /dev/null +++ b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/config/InterceptorConfig.java @@ -0,0 +1,13 @@ +package com.codesquad.issuetracker.auth.config; + +import com.codesquad.issuetracker.auth.intercepter.LoginInterceptor; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +public class InterceptorConfig implements WebMvcConfigurer { + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new LoginInterceptor()) + .addPathPatterns("/auth/**"); + } +} diff --git a/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/controller/AuthController.java b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/controller/AuthController.java new file mode 100644 index 000000000..49faa1626 --- /dev/null +++ b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/controller/AuthController.java @@ -0,0 +1,48 @@ +package com.codesquad.issuetracker.auth.controller; + +import com.codesquad.issuetracker.auth.dto.AuthResponse; +import com.codesquad.issuetracker.auth.dto.Type; +import com.codesquad.issuetracker.auth.service.AuthService; +import com.codesquad.issuetracker.auth.dto.GitHubAccessTokenResponse; +import com.codesquad.issuetracker.auth.util.JwtUtil; +import com.codesquad.issuetracker.user.UserDto; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import javax.security.auth.message.AuthException; + +@RequestMapping("/auth") +@Controller +public class AuthController { + + private final AuthService authService; + + public AuthController(AuthService authService) { + this.authService = authService; + } + + //Fixme : front로 변경 + @GetMapping("/callback") + public ResponseEntity callback(@RequestParam(value = "code") String code) throws AuthException { + return auth(code, "fe"); + } + + @GetMapping + public ResponseEntity auth(String code, String type) throws AuthException { + GitHubAccessTokenResponse token = authService.getAccessToken(code, type); + String accessToken = token.getAccessToken(); + + UserDto userDto = authService.getUserFromGitHub(accessToken); + + ResponseEntity authResponseResponseEntity = ResponseEntity.status(HttpStatus.CREATED). + body(new AuthResponse(JwtUtil.createJwt(userDto))); + + return authResponseResponseEntity; + } + + +} diff --git a/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/dto/AuthRequest.java b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/dto/AuthRequest.java new file mode 100644 index 000000000..8c755ef78 --- /dev/null +++ b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/dto/AuthRequest.java @@ -0,0 +1,13 @@ +package com.codesquad.issuetracker.auth.dto; + +public class AuthRequest { + private String code; + + public AuthRequest(String code) { + this.code = code; + } + + public String getCode() { + return code; + } +} diff --git a/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/dto/AuthResponse.java b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/dto/AuthResponse.java new file mode 100644 index 000000000..a58f2b4ca --- /dev/null +++ b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/dto/AuthResponse.java @@ -0,0 +1,13 @@ +package com.codesquad.issuetracker.auth.dto; + +public class AuthResponse { + private final String token; + + public AuthResponse(String token) { + this.token = token; + } + + public String getToken() { + return token; + } +} diff --git a/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/dto/GitHubAccessTokenRequest.java b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/dto/GitHubAccessTokenRequest.java new file mode 100644 index 000000000..c5052d7bd --- /dev/null +++ b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/dto/GitHubAccessTokenRequest.java @@ -0,0 +1,33 @@ +package com.codesquad.issuetracker.auth.dto; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class GitHubAccessTokenRequest { + @JsonProperty("client_id") + private final String clientId; + + @JsonProperty("client_secret") + private final String clientSecret; + + @JsonProperty("code") + private final String code; + + public GitHubAccessTokenRequest(String clientId, String clientSecret, String code) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.code = code; + } + + public String getClientId() { + return clientId; + } + + public String getClientSecret() { + return clientSecret; + } + + public String getCode() { + return code; + } +} diff --git a/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/dto/GitHubAccessTokenResponse.java b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/dto/GitHubAccessTokenResponse.java new file mode 100644 index 000000000..c73952105 --- /dev/null +++ b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/dto/GitHubAccessTokenResponse.java @@ -0,0 +1,56 @@ +package com.codesquad.issuetracker.auth.dto; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; + +public class GitHubAccessTokenResponse { + @JsonProperty("access_token") + private String accessToken; + + @JsonProperty("token_type") + private String tokenType; + + @JsonProperty("scope") + private String scope; + + public GitHubAccessTokenResponse() { + } + + public GitHubAccessTokenResponse(String accessToken, String tokenType, String scope) { + this.accessToken = accessToken; + this.tokenType = tokenType; + this.scope = scope; + } + + public String getScope() { + return scope; + } + + public String getTokenType() { + return tokenType; + } + + public String getAccessToken() { + return accessToken; + } + + @JsonSetter("access_token") + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + + @JsonSetter("token_type") + public void setTokenType(String tokenType) { + this.tokenType = tokenType; + } + + + + @JsonSetter("scope") + public void setScope(String scope) { + this.scope = scope; + } + +} diff --git a/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/dto/GitHubOAuth.java b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/dto/GitHubOAuth.java new file mode 100644 index 000000000..b8d074118 --- /dev/null +++ b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/dto/GitHubOAuth.java @@ -0,0 +1,16 @@ +package com.codesquad.issuetracker.auth.dto; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.PropertySource; + +@PropertySource(value = "classpath:auth.properties") +public class GitHubOAuth { + + @Value("${client_id}") + private static String clientId; + + @Value("${client_secret}") + private static String clientSecret; + + private static String code; +} diff --git a/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/dto/Type.java b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/dto/Type.java new file mode 100644 index 000000000..65a58390d --- /dev/null +++ b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/dto/Type.java @@ -0,0 +1,20 @@ +package com.codesquad.issuetracker.auth.dto; + +public enum Type { + IOS("ios"), + FE("fe"); + + private final String type; + + Type(String type) { + this.type = type; + } + + public static boolean isFe(String type) { + return FE.type.equals(type); + } + + public static boolean isIos(String type) { + return IOS.type.equals(type); + } +} diff --git a/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/exception/AuthenticationException.java b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/exception/AuthenticationException.java new file mode 100644 index 000000000..83f4c47dc --- /dev/null +++ b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/exception/AuthenticationException.java @@ -0,0 +1,7 @@ +package com.codesquad.issuetracker.auth.exception; + +public class AuthenticationException extends RuntimeException{ + public AuthenticationException(String message) { + super(message); + } +} diff --git a/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/exception/JWTCreationException.java b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/exception/JWTCreationException.java new file mode 100644 index 000000000..a8e9c9e01 --- /dev/null +++ b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/exception/JWTCreationException.java @@ -0,0 +1,7 @@ +package com.codesquad.issuetracker.auth.exception; + +public class JWTCreationException extends RuntimeException{ + public JWTCreationException(String message) { + super(message); + } +} diff --git a/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/exception/JwtException.java b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/exception/JwtException.java new file mode 100644 index 000000000..40207926b --- /dev/null +++ b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/exception/JwtException.java @@ -0,0 +1,7 @@ +package com.codesquad.issuetracker.auth.exception; + +public class JwtException extends RuntimeException { + public JwtException(String message) { + super(message); + } +} diff --git a/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/exception/LoginRequired.java b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/exception/LoginRequired.java new file mode 100644 index 000000000..bc8908f94 --- /dev/null +++ b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/exception/LoginRequired.java @@ -0,0 +1,16 @@ +package com.codesquad.issuetracker.auth.exception; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +// NOTE: Login이 필요한 API일 경우 사용 +@Target({METHOD, ANNOTATION_TYPE}) +@Retention(RUNTIME) +@Documented +public @interface LoginRequired { +} diff --git a/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/intercepter/LoginInterceptor.java b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/intercepter/LoginInterceptor.java new file mode 100644 index 000000000..1774fdeb0 --- /dev/null +++ b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/intercepter/LoginInterceptor.java @@ -0,0 +1,38 @@ +package com.codesquad.issuetracker.auth.intercepter; + +import com.codesquad.issuetracker.auth.exception.LoginRequired; +import com.codesquad.issuetracker.auth.exception.AuthenticationException; +import com.codesquad.issuetracker.auth.util.JwtUtil; +import com.codesquad.issuetracker.user.UserDto; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import static org.springframework.http.HttpHeaders.AUTHORIZATION; + +public class LoginInterceptor implements HandlerInterceptor { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + if (isLoginRequired(handler)) { + authenticate(request); + } + return true; + } + + private boolean isLoginRequired(Object handler) { + return handler instanceof HandlerMethod + && ((HandlerMethod) handler).hasMethodAnnotation(LoginRequired.class); + } + + private void authenticate(HttpServletRequest request) { + String[] splitAuth = request.getHeader(AUTHORIZATION).split(" "); + String tokenType = splitAuth[0].toLowerCase(); + if (splitAuth.length < 1 || !tokenType.equals("bearer")) { + throw new AuthenticationException("잘못된 Authorization Header 입니다."); + } + UserDto userDto = JwtUtil.decodeJwt(splitAuth[1]); + request.setAttribute("user", userDto); + } +} diff --git a/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/service/AuthService.java b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/service/AuthService.java new file mode 100644 index 000000000..25a3a2727 --- /dev/null +++ b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/service/AuthService.java @@ -0,0 +1,92 @@ +package com.codesquad.issuetracker.auth.service; + +import com.codesquad.issuetracker.auth.dto.GitHubAccessTokenRequest; +import com.codesquad.issuetracker.auth.dto.GitHubAccessTokenResponse; +import com.codesquad.issuetracker.auth.dto.Type; +import com.codesquad.issuetracker.user.UserDto; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.PropertySource; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.*; + +import javax.security.auth.message.AuthException; +import java.util.Optional; + +@PropertySource(value = "classpath:auth.properties") +@Service +public class AuthService { + @Value("${gitHub_AccessTokenUri}") + private String gitHubAccessTokenUri; + + @Value("${gitHub_UserUri}") + private String gitHubUserUri; + + @Value("${client_id}") + private String clientId; + + @Value("${client_secret}") + private String clientSecret; + + @Value("${ios_client_id}") + private String iosClientId; + + @Value("${ios_client_secret}") + private String iosClientSecret; + + private static RestTemplate restTemplate = new RestTemplate(); + + public GitHubAccessTokenResponse getAccessToken(String code, String type) throws AuthException { + + RequestEntity request = RequestEntity + .post(gitHubAccessTokenUri) + .header("Accept", "application/json") + .body(new GitHubAccessTokenRequest(getClientId(type), getClientSecret(type), code)); + + ResponseEntity response = restTemplate + .exchange(request, GitHubAccessTokenResponse.class); + + return Optional.ofNullable(response.getBody()) + .orElseThrow(() -> new AuthException("Access Token 획득 실패")); + } + + public UserDto getUserFromGitHub(String accessToken) throws AuthException { + RequestEntity request = RequestEntity + .get(gitHubUserUri) + .header("Accept", "application/json") + .header("Authorization", "token " + accessToken) + .build(); + + ResponseEntity response = restTemplate + .exchange(request, UserDto.class); + + return Optional.ofNullable(response.getBody()) + .orElseThrow(() -> new AuthException("유저 정보 획득 실패")); + } + + private String getClientId(String type) { + if(Type.isFe(type)) { + return clientId; + } + + if(Type.isIos(type)) { + return iosClientId; + } + + throw new RuntimeException("타입 확인 불가"); + } + + private String getClientSecret(String type) { + if(Type.isFe(type)) { + return clientSecret; + } + + if(Type.isIos(type)) { + return iosClientSecret; + } + + throw new RuntimeException("타입 확인 불가"); + } + +} diff --git a/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/util/JwtUtil.java b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/util/JwtUtil.java new file mode 100644 index 000000000..02e450fee --- /dev/null +++ b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/auth/util/JwtUtil.java @@ -0,0 +1,53 @@ +package com.codesquad.issuetracker.auth.util; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.interfaces.JWTVerifier; +import com.codesquad.issuetracker.auth.exception.JWTCreationException; +import com.codesquad.issuetracker.auth.exception.JwtException; +import com.codesquad.issuetracker.user.UserDto; +import lombok.Data; + +@Data +public class JwtUtil { + private static final String JWT_SECRET = "jwtSecret"; + private static final String JWT_ISSUER = "jwtIssuer"; + private static final String USER_LOGIN = "login"; + private static final String USER_NAME = "name"; + private static final String USER_AVATAR = "avatar"; + private static final Algorithm algorithmHS = Algorithm.HMAC256(JWT_SECRET); + + public static String createJwt(UserDto user) { + try { + return JWT.create() + .withIssuer(JWT_ISSUER) + .withClaim(USER_LOGIN, user.getLogin()) + .withClaim(USER_NAME, user.getName()) + .withClaim(USER_AVATAR, user.getAvatarUrl()) + .sign(algorithmHS); + } catch (JWTCreationException exception) { + throw new JwtException("JWT 생성 실패"); + } + } + + public static UserDto decodeJwt(String token) throws JwtException { + try { + JWTVerifier verifier = JWT.require(algorithmHS) + .withIssuer(JWT_ISSUER) + .build(); + + DecodedJWT jwt = verifier.verify(token); + + String login = jwt.getClaim(USER_LOGIN).asString(); + String name = jwt.getClaim(USER_NAME).asString(); + String avatarUri = jwt.getClaim(USER_AVATAR).asString(); + + return new UserDto(login, name, avatarUri); + } catch (JWTVerificationException exception) { + throw new JwtException("잘못된 jwt 입니다."); + } + } + +} diff --git a/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/user/UserDto.java b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/user/UserDto.java new file mode 100644 index 000000000..4c0bd1706 --- /dev/null +++ b/be/issue-tracker-be/src/main/java/com/codesquad/issuetracker/user/UserDto.java @@ -0,0 +1,36 @@ +package com.codesquad.issuetracker.user; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class UserDto { + @JsonProperty("login") + private String login; + + @JsonProperty("avatar_url") + private String avatarUrl; + + @JsonProperty("name") + private String name; + + public UserDto() { + } + + public UserDto(String login, String avatarUrl, String name) { + this.login = login; + this.avatarUrl = avatarUrl; + this.name = name; + } + + public String getLogin() { + return login; + } + + public String getAvatarUrl() { + return avatarUrl; + } + + public String getName() { + return name; + } + +} diff --git a/be/issue-tracker-be/src/main/resources/static/login.html b/be/issue-tracker-be/src/main/resources/static/login.html new file mode 100644 index 000000000..d7a18f56f --- /dev/null +++ b/be/issue-tracker-be/src/main/resources/static/login.html @@ -0,0 +1,10 @@ + + + + + login + + + login + +