Skip to content

Commit

Permalink
Bugfix: OAuth2 서버로부터 유저의 정보가 null로 들어왔을 때 에러처리하기 (Kernel360#172)
Browse files Browse the repository at this point in the history
* Feature: Add exception handling in security-filter logic

* Fix: api-에러코드를 사용하는 패키지로 이동

* Refactor: securityConfig에 구현했던 handler 분리

* Fix: 변수명 수정 & 필요없는 예외 클래스 삭제
  • Loading branch information
anso33 authored Jan 19, 2024
1 parent 16966b0 commit d1c6997
Show file tree
Hide file tree
Showing 12 changed files with 208 additions and 69 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package kernel.jdon.auth.dto;

import java.time.LocalDateTime;

import kernel.jdon.util.DateParserUtil;
import lombok.Getter;

@Getter
public class AuthExceptionResponse {

private String timestamp;
private int status;
private String message;
private String path;

public AuthExceptionResponse(int status, String message, String path) {
this.timestamp = DateParserUtil.dateTimeToString(LocalDateTime.now());
this.status = status;
this.message = message;
this.path = path;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@

@RequiredArgsConstructor
public enum AuthErrorCode implements ErrorCode {
NOT_FOUND_NOT_MATCH_PROVIDER_TYPE(HttpStatus.NOT_FOUND, "다른 소셜 로그인으로 가입된 이메일입니다."),;
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증되지 않은 사용자입니다."),
UNAUTHORIZED_NOT_MATCH_PROVIDER_TYPE(HttpStatus.UNAUTHORIZED, "다른 소셜 로그인으로 가입된 이메일입니다."),
UNAUTHORIZED_OAUTH_RETURN_NULL_EMAIL(HttpStatus.UNAUTHORIZED, "소셜 서비스에 이메일을 등록해야 서비스를 이용할 수 있습니다."),
FORBIDDEN(HttpStatus.FORBIDDEN, "권한이 없는 사용자입니다.");

private final HttpStatus httpStatus;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
package kernel.jdon.auth.service;

import java.util.List;
import java.util.Map;

import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import jakarta.servlet.http.HttpSession;
import kernel.jdon.auth.dto.JdonOAuth2User;
import kernel.jdon.auth.dto.SessionUserInfo;
import kernel.jdon.auth.error.AuthErrorCode;
import kernel.jdon.global.exception.ApiException;
import kernel.jdon.global.exception.UnAuthorizedException;
import kernel.jdon.member.domain.Member;
import kernel.jdon.member.domain.SocialProviderType;
import kernel.jdon.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Map;

@Slf4j
@Service
Expand Down Expand Up @@ -62,17 +61,28 @@ private DefaultOAuth2User getOAuth2UserFromOAuthServer(OAuth2UserRequest userReq
}

private String getEmailFromGithub(OAuth2User user) {
return (String)user.getAttributes().get("email");
String email = (String)user.getAttributes().get("email");
isEmailExist(email);

return email;
}

private String getEmailFromKakao(OAuth2User user) {
Map<String, Object> attributes = user.getAttributes();
return ((Map<String, String>)attributes.get("kakao_account")).get("email");
String email = ((Map<String, String>)attributes.get("kakao_account")).get("email");
isEmailExist(email);

return email;
}

private void isEmailExist(String email) {
if (email == null)
throw new UnAuthorizedException(AuthErrorCode.UNAUTHORIZED_OAUTH_RETURN_NULL_EMAIL);
}

private void checkRightSocialProvider(Member findMember, SocialProviderType socialProvider) {
if (!findMember.isRightSocialProvider(socialProvider))
throw new ApiException(AuthErrorCode.NOT_FOUND_NOT_MATCH_PROVIDER_TYPE);
throw new UnAuthorizedException(AuthErrorCode.UNAUTHORIZED_NOT_MATCH_PROVIDER_TYPE);
}

private SocialProviderType getSocialProvider(OAuth2UserRequest userRequest) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package kernel.jdon.config.auth;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import kernel.jdon.auth.dto.AuthExceptionResponse;
import kernel.jdon.auth.error.AuthErrorCode;
import kernel.jdon.error.ErrorCode;
import kernel.jdon.global.exception.UnAuthorizedException;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.PrintWriter;

@Component
public class JdonAuthExceptionHandler implements AuthenticationEntryPoint, AccessDeniedHandler, AuthenticationFailureHandler {

private void throwAuthException(HttpServletResponse response, ErrorCode authErrorCode, String redirectUri) throws
IOException {
AuthExceptionResponse exceptionResponse = new AuthExceptionResponse(
authErrorCode.getHttpStatus().value(), authErrorCode.getMessage(), redirectUri);
String exceptionResponseJson = new ObjectMapper().writeValueAsString(exceptionResponse);
response.setStatus(authErrorCode.getHttpStatus().value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("utf-8");
PrintWriter write = response.getWriter();
write.write(exceptionResponseJson);
write.flush();
}

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
throwAuthException(response, AuthErrorCode.UNAUTHORIZED, "/");
}

@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
throwAuthException(response, AuthErrorCode.FORBIDDEN, "/");
}

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
if (exception instanceof UnAuthorizedException) {
throwAuthException(response, ((UnAuthorizedException) exception).getErrorCode(), "/");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package kernel.jdon.config.auth;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import kernel.jdon.auth.dto.JdonOAuth2User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

import static kernel.jdon.auth.encrypt.AesUtil.encryptAESCBC;
import static kernel.jdon.auth.encrypt.HmacUtil.generateHMAC;
import static kernel.jdon.util.StringUtil.createQueryString;
import static kernel.jdon.util.StringUtil.joinToString;

@Component
@Slf4j
public class JdonOAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler{

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
JdonOAuth2User jdonOAuth2User = (JdonOAuth2User)authentication.getPrincipal();
if (isTemporaryUser(jdonOAuth2User)) {
String query = createUserInfoString(jdonOAuth2User.getEmail(), jdonOAuth2User.getSocialProviderType());
String encodedQueryString = createEncryptQueryString(query);
response.sendRedirect(joinToString("http://localhost:3000/oauth/info?", encodedQueryString));
}
}

private boolean isTemporaryUser(JdonOAuth2User jdonOAuth2User) {
return jdonOAuth2User.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_TEMPORARY_USER"));
}

private String createUserInfoString(String email, String provider) {
return joinToString(createQueryString("email", email), createQueryString("provider", provider));
}

private String createEncryptQueryString(String info) {
String encoded = null;
try {
encoded = encryptAESCBC(info);
encoded = joinToString(createQueryString("value", encoded),
createQueryString("hmac", generateHMAC(encoded)));
} catch (Exception e) {
log.warn(e.getMessage(), e);
}
return encoded;
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
package kernel.jdon.config;

import static kernel.jdon.auth.encrypt.AesUtil.*;
import static kernel.jdon.auth.encrypt.HmacUtil.*;
import static kernel.jdon.util.StringUtil.*;

import java.util.List;
package kernel.jdon.config.auth;

import kernel.jdon.auth.service.JdonOAuth2UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.web.cors.CorsConfiguration;

import kernel.jdon.auth.dto.JdonOAuth2User;
import kernel.jdon.auth.service.JdonOAuth2UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.List;

@Slf4j
@RequiredArgsConstructor
Expand All @@ -27,6 +19,8 @@
@Configuration
public class OAuth2SecurityConfig {
private final JdonOAuth2UserService jdonOAuth2UserService;
private final JdonOAuth2AuthenticationSuccessHandler jdonOAuth2AuthenticationSuccessHandler;
private final JdonAuthExceptionHandler jdonAuthExceptionHandler;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
Expand All @@ -40,12 +34,17 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

return config;
}));
http.exceptionHandling(exceptionConfig -> exceptionConfig
.authenticationEntryPoint(jdonAuthExceptionHandler)
.accessDeniedHandler(jdonAuthExceptionHandler));
http.csrf().disable();
http.authorizeHttpRequests(config -> config
.requestMatchers("/api/v1/member").hasAnyRole("USER")
.requestMatchers("api/**").permitAll()
.anyRequest().permitAll());
http.oauth2Login(oauth2Configurer -> oauth2Configurer
.successHandler(oAuth2AuthenticationSuccessHandler())
.successHandler(jdonOAuth2AuthenticationSuccessHandler)
.failureHandler(jdonAuthExceptionHandler)
.userInfoEndpoint(userInfoEndpointConfigurer -> userInfoEndpointConfigurer
.userService(jdonOAuth2UserService)));
http.logout(logoutConfigurer -> logoutConfigurer
Expand All @@ -56,36 +55,4 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

return http.build();
}

@Bean
public AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler() {
return ((request, response, authentication) -> {
JdonOAuth2User jdonOAuth2User = (JdonOAuth2User)authentication.getPrincipal();
if (isTemporaryUser(jdonOAuth2User)) {
String query = createUserInfoString(jdonOAuth2User.getEmail(), jdonOAuth2User.getSocialProviderType());
String encodedQueryString = createEncryptQueryString(query);
response.sendRedirect(joinToString("http://localhost:3000/oauth/info?", encodedQueryString));
}
});
}

private boolean isTemporaryUser(JdonOAuth2User jdonOAuth2User) {
return jdonOAuth2User.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_TEMPORARY_USER"));
}

private String createUserInfoString(String email, String provider) {
return joinToString(createQueryString("email", email), createQueryString("provider", provider));
}

private String createEncryptQueryString(String info) {
String encoded = null;
try {
encoded = encryptAESCBC(info);
encoded = joinToString(createQueryString("value", encoded),
createQueryString("hmac", generateHMAC(encoded)));
} catch (Exception e) {
log.warn(e.getMessage(), e);
}
return encoded;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package kernel.jdon.global.exception;

import kernel.jdon.error.ErrorCode;
import lombok.Getter;
import org.springframework.security.core.AuthenticationException;

@Getter
public class UnAuthorizedException extends AuthenticationException {
private final transient ErrorCode errorCode;

public UnAuthorizedException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package kernel.jdon.error.code.api;
package kernel.jdon.jobcategory.error;

import org.springframework.http.HttpStatus;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package kernel.jdon.error.code.api;
package kernel.jdon.member.error;

import org.springframework.http.HttpStatus;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
import kernel.jdon.auth.dto.object.RegisterMemberDto;
import kernel.jdon.auth.dto.request.RegisterRequest;
import kernel.jdon.auth.encrypt.AesUtil;
import kernel.jdon.error.code.api.JobCategoryErrorCode;
import kernel.jdon.error.code.api.MemberErrorCode;
import kernel.jdon.error.code.api.SkillErrorCode;
import kernel.jdon.jobcategory.error.JobCategoryErrorCode;
import kernel.jdon.member.error.MemberErrorCode;
import kernel.jdon.skill.error.SkillErrorCode;
import kernel.jdon.global.exception.ApiException;
import kernel.jdon.jobcategory.domain.JobCategory;
import kernel.jdon.jobcategory.repository.JobCategoryRepository;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package kernel.jdon.error.code.api;
package kernel.jdon.skill.error;

import org.springframework.http.HttpStatus;

Expand Down
15 changes: 15 additions & 0 deletions module-common/src/main/java/kernel/jdon/util/DateParserUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package kernel.jdon.util;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class DateParserUtil {

public static String dateTimeToString(LocalDateTime localDateTime) {
return localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
}

0 comments on commit d1c6997

Please sign in to comment.