[Spring] Spring Security OAuth2 구현중 Google Login CustomUserService 로직 동작하지 않는 문제

Gogh·2023년 1월 2일
1

Project

목록 보기
1/5
post-thumbnail

🎯 목표 : 개인 프로젝트 진행 중 발생한 이슈에 대한 해결 기록

📒 OAuth2 Google Login CustomOAuth2UserService 동작 하지 않음

📌 Occurrence Issue

  • Spring Security OAuth2의 Google, Kakao 로그인 구현중, Google 로그인에서만 OAuth2UserService를 구현한 CustomOAuth2UserService 로직을 타지 않고 Default 로직으로 흘러 원하는 에트리뷰트를 수정하지 못하는 문제 발생.
  • 구현 완료 하였지만, Kakao 로그인은 정상적으로 CustomOAuth2UserService 로직으로 흘러 필요한 에트리뷰트를 가지고 DB 저장 완료.
  • Google 로그인도 정상 동작 하지만, CustomOAuth2UserService 로직을 타지 않아서 원하는 에트리뷰트 가공로직을 거치지 않음.

📌 Cause Analysis

  • Google 로그인 구현까지는 사용자의 Attribute 객체 내부에 추가 객체가 없어 SuccesseHandler에서 mail, name등 간단한 에트리뷰트 명으로 파싱이 수월했다.
  • Kakao 로그인 구현 중 Kakao 로그인 사용자의 Attribute 객체 내부에 추가 객체가 있어 필요한 데이터 파싱에 까다로워 OAuth2UserService 인터페이스를 구현하여 loadUser()에서 로그인 서버별로 분기하여 관리하기로 하였다.
  • 구현 완료후 문제점이 발생하였는데, 디버깅 과정을 기록하려한다.
  • OAuth2의 로그인은 OAuth2LoginAuthenticationFilter필터를 거쳐 attemptAuthentication()에서 다음 로직으로 흘려준다.
  • attemptAuthentication() 메소드의 중간지점에 아래와 같은 코드가 있다 OAuth2LoginAuthenticationToken를 할당해주는 로직인데, 해당 로직에서 브레이크 포인트 설정하고 디버깅 하였다.
  OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this
          .getAuthenticationManager().authenticate(authenticationRequest);

  • 위 그림과 같은 데이터 들을 볼수 있는데 눈여겨 볼 부분은 scopes 부분이다. scopes의 요소로 openid, profile, email을 가지고 있다. 기억해두자.
  • attemptAuthentication() 내부에서 AuthenticationManager 인터페이스의 authenticate()메소드를 호출 하는 것을 볼수 있다.
  • 여기서 AuthenticationManager의 구현체는 ProviderManager가 사용되고, ProviderManagerauthenticate()를 호출하게 된다.
// ProviderManger.java
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException{
        Class<?extends Authentication> toTest=authentication.getClass();
        AuthenticationException lastException=null;
        AuthenticationException parentException=null;
        Authentication result=null;
        //.....
        for(AuthenticationProvider provider:getProviders()){
        if(!provider.supports(toTest)){
            //......
        }
        try{
        result=provider.authenticate(authentication);
        //......
  • ProviderManagerauthenticate() 메소드 일부다. 위 코드에서 제일 마지막 부분을 보면, AuthenticationProvider 인터페이스의 authenticate()를 호출하게 된다.
  • 여기서 사용되는 구현체는 OAuth2LoginAuthenticationProvider다. OAuth2LoginAuthenticationProviderauthenticate() 메소드를 살펴보자.
// OAuth2LoginAuthenticationProvider.java

public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {

  private final OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider;

  private final OAuth2UserService<OAuth2UserRequest, OAuth2User> userService;
  //......

	@Override
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    OAuth2LoginAuthenticationToken loginAuthenticationToken = (OAuth2LoginAuthenticationToken) authentication;
    // Section 3.1.2.1 Authentication Request -
    // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest scope
    // REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
    if (loginAuthenticationToken.getAuthorizationExchange().getAuthorizationRequest().getScopes()
    .contains("openid")) {
    // This is an OpenID Connect Authentication Request so return null
    // and let OidcAuthorizationCodeAuthenticationProvider handle it instead
    return null;
    }
    //......
    Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();
    OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
            loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
    //......
  • 위 코드에서 주석 부분을 보면, REQUIRED. OpenID Connect requests MUST contain the "openid" scope value. This is an OpenID Connect Authentication Request so return null and let OidcAuthorizationCodeAuthenticationProvider handle it instead부분이 있다.
  • scope에 openid가 포함되어 있을경우 OpenId Connect Authentication Request로 확인되고 null을 리턴한다음 OidcAuthorizationCodeAuthenticationProvider에서 대신 처리한다고 되어있다.
  • 포함하고 있지 않다면, 아래의 OAuth2User oauth2User = this.userService.loadUser()로직이 실행되고 OAuth2UserService<OAuth2UserRequest, OAuth2User> userServiceloadUser() 로직을 호출한다.
  • 그렇다면, openid가 포함 되어 있을경우 대신 처리한다고 하는 OidcAuthorizationCodeAuthenticationProvider를 살펴 보자.
// OidcAuthorizationCodeAuthenticationProvider.java
public class OidcAuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
    //...
    private final OAuth2UserService<OidcUserRequest, OidcUser> userService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
      OAuth2LoginAuthenticationToken authorizationCodeAuthentication = (OAuth2LoginAuthenticationToken) authentication;
      // Section 3.1.2.1 Authentication Request -
      // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
      // scope
      // REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
      if (!authorizationCodeAuthentication.getAuthorizationExchange().getAuthorizationRequest().getScopes()
          .contains(OidcScopes.OPENID)) {
        // This is NOT an OpenID Connect Authentication Request so return null
        // and let OAuth2LoginAuthenticationProvider handle it instead
        return null;
      }
        //......
      OidcUser oidcUser = this.userService.loadUser(new OidcUserRequest(clientRegistration,
        accessTokenResponse.getAccessToken(), idToken, additionalParameters));
      //......
  • OidcAuthorizationCodeAuthenticationProvider 코드의 일부다 Google 로그인에서는 해당 로직을 타고 들어와 아래에 있는 OidcUser oidcUser = this.userService.loadUser()메소드가 호출 될 것이다.
  • 여기서 호출되는 userService는 OAuth2UserService<OidcUserRequest, OidcUser>의 구현체다.
  • OAuth2UserService<OidcUserRequest, OidcUser>의 구현체는 OidcUserService로 해당 클래스가 호출되게 된다.
  • 바로 이 부분에서 OidcUserService가 호출되기 때문에 내가 구현한 CustomOAuth2UserService 로직이 적용되지 않았던 것이다.
  • 그렇다면, 지금 원하고 있는 CustomOAuth2UserService를 적용하기 위해서는 뭐가 필요할까?
  • OAuth2LoginAuthenticationProvider에서 봤던 OAuth2UserService<OAuth2UserRequest, OAuth2User> userService를 구현한 userService의 로직이 적용되어야한다.
  • 위 코드에서 주석부분을 보면 openid가 없다면 OAuth2LoginAuthenticationProvider에서 처리를 대신한다고 OidcAuthorizationCodeAuthenticationProvider의 주석 내용과 반대로 되어 있다.
  • OAuth2LoginAuthenticationProvider 로직에서 openid를 포함하고 있지 않을때 실행되는 OAuth2UserService<OAuth2UserRequest, OAuth2User>의 Default 구현체는 DefaultOAuth2UserService다.
  • 커스텀 한 구현체가 있다면 SecurityConfig에 커스텀 구현체를 적용하고 해당 구현체가 실행될 것이다.
  • 즉, Google 로그인의 scopes 요소에 openid가 없어야 된다는 말이다.

📌 Issue Solution

  • 해결 방법을 찾았으니 적용해보자.
  • Google 로그인의 scopes 요소에 openid는 어디서 왔을까?
  • Spring Security OAuth2에서 Google,GitHub 등 기본으로 지원하는 서비스 들이있다. 그 설정들의 Provider 역할을 하는 enum 타입의 CommonOAuth2Provider가 있다.
public enum CommonOAuth2Provider {

	GOOGLE {

		@Override
		public Builder getBuilder(String registrationId) {
			ClientRegistration.Builder builder = getBuilder(registrationId,
					ClientAuthenticationMethod.CLIENT_SECRET_BASIC, DEFAULT_REDIRECT_URL);
			builder.scope("openid", "profile", "email");
			builder.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth");
			builder.tokenUri("https://www.googleapis.com/oauth2/v4/token");
			builder.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs");
			builder.issuerUri("https://accounts.google.com");
			builder.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo");
			builder.userNameAttributeName(IdTokenClaimNames.SUB);
			builder.clientName("Google");
			return builder;
		}

	},
//......
  • 위 코드를 보면 builder.scope("openid", "profile", "email"); 여기서 Default scope를 지정해 준다.
  • 여기서 openid 만 제거한다면 내가 구현한 CustomOAuth2UserService 로직을 적용할 수 있을 것이다.
  • application.properti/yml의 google.scope 속성 제한을 한다면 openid가 사라진다고 한다. 하지만, 내 프로젝트에서는 client-id, client-secret 환경변수를 별도의 yml 파일에 관리해서 그런지 application.yml을 수정해도 적용되지 않았다.
  • 그래서 CustomOAuth2Provider를 작성하여 Google 로그인에서 사용하는 ClientRegistration를 아래와 같이 커스텀 하였다. CustomOAuth2Provider을 만들어 Kakao 설정도 함께 관리하는게 좋겠다 생각들었다.
// CustomOAuth2Provider.java
public enum CustomOAuth2Provider {


    GOOGLE {
        @Override
        public ClientRegistration.Builder getBuilder(String registrationId) {
            ClientRegistration.Builder builder = getBuilder(registrationId,
                    ClientAuthenticationMethod.CLIENT_SECRET_BASIC, DEFAULT_LOGIN_REDIRECT_URL);
            builder.scope("profile", "email");
            builder.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth");
            builder.tokenUri("https://www.googleapis.com/oauth2/v4/token");
            builder.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs");
            builder.issuerUri("https://accounts.google.com");
            builder.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo");
            builder.userNameAttributeName(IdTokenClaimNames.SUB);
            builder.clientName("Google");
            return builder;
        }

    },
    KAKAO {
        @Override
        public ClientRegistration.Builder getBuilder(String registrationId) {
            ClientRegistration.Builder builder = getBuilder(registrationId,
                    ClientAuthenticationMethod.CLIENT_SECRET_POST, DEFAULT_LOGIN_REDIRECT_URL);
            builder.scope("account_email","profile_nickname");
            builder.authorizationUri("https://kauth.kakao.com/oauth/authorize");
            builder.tokenUri("https://kauth.kakao.com/oauth/token");
            builder.userInfoUri("https://kapi.kakao.com/v2/user/me");
            builder.userNameAttributeName("id");
            builder.clientName("Kakao");
            return builder;
        }
    };

    //.......
  • 위 코드로 Security Config에 적용해주니 정상적으로 CustomOAuth2UserService 로직이 적용되는 것을 확인할 수 있다.
  • 디버깅 과정을 하나하나 살펴보며 Spring Security OAuth2의 동작 원리에 대해 조금더 이해할수 있었고, 한번더 복습하는 기회가 되었다. 앞으로 다른 소셜 로그인 기능을 추가하며 복기해볼 예정이다.
  • 전체 구현 코드는 https://github.com/sussa3007/prac-project-foodservice를 확인하면 된다.


> Reference
> [https://stackoverflow.com/questions/49715769/why-is-my-oauth2-config-not-using-my-custom-userservice](https://stackoverflow.com/questions/49715769/why-is-my-oauth2-config-not-using-my-custom-userservice)
profile
컴퓨터가 할일은 컴퓨터가

1개의 댓글

comment-user-thumbnail
2023년 7월 19일

감사합니다 진짜 큰 도움이 되었습니다.

답글 달기