🎯 목표 : 개인 프로젝트 진행 중 발생한 이슈에 대한 해결 기록
📒 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
가 사용되고, ProviderManager
의 authenticate()
를 호출하게 된다.
@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);
ProviderManager
의 authenticate()
메소드 일부다. 위 코드에서 제일 마지막 부분을 보면, AuthenticationProvider
인터페이스의 authenticate()
를 호출하게 된다.
- 여기서 사용되는 구현체는
OAuth2LoginAuthenticationProvider
다. OAuth2LoginAuthenticationProvider
의 authenticate()
메소드를 살펴보자.
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;
if (loginAuthenticationToken.getAuthorizationExchange().getAuthorizationRequest().getScopes()
.contains("openid")) {
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> userService
의 loadUser()
로직을 호출한다.
- 그렇다면, openid가 포함 되어 있을경우 대신 처리한다고 하는
OidcAuthorizationCodeAuthenticationProvider
를 살펴 보자.
public class OidcAuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
private final OAuth2UserService<OidcUserRequest, OidcUser> userService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2LoginAuthenticationToken authorizationCodeAuthentication = (OAuth2LoginAuthenticationToken) authentication;
if (!authorizationCodeAuthentication.getAuthorizationExchange().getAuthorizationRequest().getScopes()
.contains(OidcScopes.OPENID)) {
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 설정도 함께 관리하는게 좋겠다 생각들었다.
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)
감사합니다 진짜 큰 도움이 되었습니다.