Spring Security OAuth2.0 에서 Google Login만 OAuth2UserService의 커스텀 구현체로 로직 타지 않는 문제

cw k·2022년 3월 4일
3

Spring

목록 보기
1/3
post-thumbnail

문제발생

개인 프로젝트를 진행하는 도중 Spring Security OAuth2.0 로그인 구현을 하였다. Google, GitHub, Naver 로그인을 구현하였는데, 어플리케이션에서 서비스 프로바이더(Google, GitHub, Naver) 에 인증 요청을 보내고 그 결과를 받아 사용자정보(email)를 DB에서 조회해 데이터가 없을 경우 가입하는 프로세스였다.

구현을 완료한 후 GitHub과 Naver는 정상적으로 DB에 값이 저장 되는데, Google만 저장이 안되는 문제가 발생하였다.

원인 분석

디버깅

위 프로세스를 진행하기 위해서는 Spring Security OAuth2UserService인터페이스의 loadUser를 구현한 커스텀 구현체를 구현해야 하는데 나는 OAuth2UserService에서 구현한 DefaultOAuth2UserService 를 상속받아 커스텀 구현체를 구현했다.

현재 프로젝트의 커스텀 구현체인 CustomOAuth2UserService 에 브레이크 포인트를 걸고 디버깅을 진행하였으나 브레이크포인트에 걸리지 않았다. (즉, CustomOAuth2UserService 로 로직이 진행되지 않았다.)

Spring Security는 기본적으로 SecurityFilterChain 에 연결된 필터들을 통해 여러가지 처리를 한다. OAuth2.0 로긴의 경우 OAuth2LoginAuthenticationFilter 를 타게 되는데 OAuth2LoginAuthenticationFilterattemptAuthentication에서 다음 로직을 실행한다.

// OAuth2LoginAuthenticationFilter.java
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
...
OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(clientRegistration,
				new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
                
OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this
				.getAuthenticationManager().authenticate(authenticationRequest); // 이부분

OAuth2LoginAuthenticationToken 에는 인증을 위한 다양한 정보들이 담겨있다.

이 중에는 사용자가 프로퍼티에 설정한 registration, cliend-id, client-secret 등의 정보도 담겨있다

결론부터 말하자면, 위 디버깅 정보에서 scopesopenid가 문제였는데 이에 대한 설명은 뒤에서 하기로 한다.

attemptAuthentication 메서드 내에 주석으로 표기한 authenticateAuthenticationManager라는 인터페이스에 정의된 메서드이다. AuthenticationManager 인터페이스는 다음과 같이 여러가지 구현체가 있는데 그 중 ProviderManager 의 구현체를 사용한다.

ProviderManagerauthenticate는 아래의 과정을 수행하는데

// ProviderManager.java
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		...
		Authentication result = null;
		...
		for (AuthenticationProvider provider : getProviders()) {
        	...
			result = provider.authenticate(authentication); // 이부분
			...

이는 AuthenticationProvider 인터페이스의 authenticate 를 호출한다.

AuthenticationProvider 도 다음과 같이 여러가지 구현체를 갖고 있는데 여기서는 OAuth2LoginAuthenticationProvider를 사용한다.

OAuth2LoginAuthenticationProviderauthenticate는 다음과 같은 처리를 하는데 여기서 위의 디버깅에서 보았던 scopes에서 openid가 나온다. 아래 코드 내의 주석에도 나와있듯이 scopesopenid가 포함되어 있을 경우 OpenId Connect Authentication Request로 판단하여 null을 리턴하고OidcAuthorizationCodeAuthenticationProvider 가 대신 처리 한다.

// OAuth2LoginAuthenticationProvider.java
@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;
		}
        ...
        OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
				loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));

만약 openid 값이 없을경우 아래로 로직이 진행되고 this.userService.loadUser() 가 호출된다 여기서 userService가 사용자가 구현했던 CustomOAuth2UserService인 것이다. (따로 커스텀한 구현체가 없을 경우 DefaultOAuth2UserService)

참고로, OpenID Connect를 처리하는 OidcAuthorizationCodeAuthenticationProviderauthenticate 에서도 비슷한 처리를 하는데

// OidcAuthorizationCodeAuthenticationProvider.java
OidcUser oidcUser = this.userService.loadUser(new OidcUserRequest(clientRegistration,
				accessTokenResponse.getAccessToken(), idToken, additionalParameters));

여기서의 기본 구현체는 OidcUserService 라는 구현체이다.

OAuth2LoginAuthenticationToken 디버깅정보 중 scopesopenid가 포함되어 사용자가 정의한 CustomOAuth2UserService로 정상적으로 진행되지 않았던 것이다.

왜 scope에 openid가 포함되었는지?

Spring Security에서는 편의를 위해 Google, GitHub, Facebook 등의 대형 서비스에 대해 기본적으로 여러가지 설정을 해두었는데 해당 설정이 되어있는 곳이 CommonOAuth2Providerenum이다.
(참고로 Naver는 없기 때문에 모든 설정값을 직접 지정해주어야함)

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;
		}

	},
    
    GITHUB {

		@Override
		public Builder getBuilder(String registrationId) {
			ClientRegistration.Builder builder = getBuilder(registrationId,
					ClientAuthenticationMethod.CLIENT_SECRET_BASIC, DEFAULT_REDIRECT_URL);
			builder.scope("read:user");
			builder.authorizationUri("https://github.com/login/oauth/authorize");
			builder.tokenUri("https://github.com/login/oauth/access_token");
			builder.userInfoUri("https://api.github.com/user");
			builder.userNameAttributeName("id");
			builder.clientName("GitHub");
			return builder;
		}

	},
    ...

위 코드에서 보듯이 Google의 기본 scope는 따로 설정하지 않을 경우 "openid", "profile", "email" 이다.

앞서 첨부하였던, oauth 설정 yml 스크린샷에서 보듯이 나는 각 서비스에 따로 scope를 지정해주지 않았다.

따라서 기본 설정인 openidscope 에 포함되어 OpenID Connect 로직을 타게된 것이었다.

해결

oauth 관련 yml 설정에 다음과 같이 scope를 지정해 주었더니

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: XXXX
            client-secret: XXXX
            scope: profile, email

정상적으로 실행되어 데이터베이스에 정보가 적재되는것을 확인할 수 있었다.

요약

  1. Spring Security는 편의를 위해 Google, GitHub, Facebook 등의 대형 서비스프로바이더에 대한 OAuth2 기본 설정을 CommonOAuth2Provider에 지정해두었다.
  2. 사용자가 scope에 대한 설정을 하지 않으면 CommonOAuth2Provider에서 기본 scope 정보를 가져온다.
  3. 구글은 openid, email, profile 이 기본값으로 설정되어 있어 내부적으로 openid connect 로직을 타게 된다.
  4. 따라서 사용자가 커스텀한 서비스를 타지 않았던 것이다.

1개의 댓글

comment-user-thumbnail
2023년 2월 8일

와 ㅠㅠ 이거때문에 하루종일 고통받았다가 scope에서 openid 빼니 돌아가서 더 멘붕 중이었는데ㅠㅠ 이거 보고 이해 잘 하고 갑니다!! ㅎㅎ

답글 달기