기록하기

OAuth2.0 Microsoft 소셜 로그인 본문

Server/Spring Boot

OAuth2.0 Microsoft 소셜 로그인

jjungdev 2024. 11. 7. 18:31

소셜 로그인을 연동하면서 Kakao, Naver, Google, Apple 로그인은 연동을 해보았지만, Microsoft 연동은 처음이었다.

진행하면서 많이 헤맸던 부분도 있고 더 공부가 필요하다고 생각한 부분도 있어서 더 까먹기(?) 전에 블로그에 정리를 해보려고 한다.

 

글의 목차는 다음과 같다.

 

  1. Microsoft 로그인을 위한 앱 설정
  2. Spring Security 설정
  3. OAuth2.0 흐름과 custom 설계

Microsoft 로그인을 위한 앱 설정

앱 설정

다른 소셜 로그인과 마찬가지로 일단 앱 등록 과정이 필요하다.

Microsoft Entra 관리 센터에 들어가서 앱 등록을 해주면 되는데 Microsoft(이하 MS) 연동에서는 조금 다른 개념 하나가 있다. 그 개념은 바로 tenant 라는 개념이다.

Microsoft Entra tenant 란?
Microsoft Entra 테넌트는 organization IT 부서의 제어 하에 있는 ID 보안 경계입니다. 이 보안 경계 내에서 개체(예: 사용자 개체) 관리 및 테넌트 전체 설정 구성은 IT 관리자가 제어합니다.
(출처 : https://learn.microsoft.com/ko-kr/microsoft-365/education/deploy/intro-azure-active-directory)

 

즉, 리소스에 액세스 하기 위해 인증 및 권한을 부여할 수 있는 디렉터리 개체이고, 개인 계정이나 회사 계정 모두에게 생성되는 값을 말한다.

특히 회사 계정의 경우 회사 내부의 사용자들을 하나의 조직으로 묶어서 관리할 수 있고, 개인 계정의 경우에도 개별 계정으로 관리가 되는 개념이다. 개인, 그룹의 계정 유형과 관계 없이 리소스 및 권한 관리를 위한 기본 단위로 생성되는 개념이라고 할 수 있다.

 

이 개념은 아래 코드 설정 내에서도 나오게 되므로 잘 기억해두는 것이 좋다.

 

이제 이렇게 사진에 나오는 것과 비슷하게 설정을 해주면 되는데, 설정에 앞서 소셜 로그인을 연동하는 방법에는 여러가지가 있지만 내가 선택한 방법은 서버가 OAuth2.0 라이브러리를 활용해서 인증하는 방법이다. 따라서, 리디렉션 URI 설정 내에는 MS 에 로그인을 완료한 유저의 정보를 전달 받게 될 서버의 엔드포인트 주소를 작성하면 된다. 예시는 다음과 같다.

http://localhost:8080/login/oauth2/code/microsoft

Spring Security 설정

build.gradle 설정 및 Spring Security 관련 내용을 작성하기 전에 기본적인 로그인 구조와 OAuth2.0 의 흐름을 정리하는 것이 좋을 것 같아 이 부분부터 정리를 해보고자 한다.

로그인 구조와 흐름

현재 구성된 서버는 MSA 구조로 설계되어 있어 auth 서버가 로그인 및 회원가입을 담당하고 있으며 소셜 로그인 인증 결과를 서버가 전달 받게 된다. 그림은 전체 구조를 축소해서 그려보았다.

이때 이 흐름의 가장 시작점은 로그인을 할 수 있는 링크를 호출하는 것인데 MS 로그인 버튼을 클릭하면 authorization url 을 전달해서 이 url 을 GET 방식으로 호출하도록 처리했다.

{서버 주소}/oauth2/authorization/{provider}?redirect_uri={redirect_uri}

 

즉, 위 링크를 호출하면 소셜 로그인의 흐름이 시작되는데 여기서 redirect_uri 에 들어가는 데이터는 client_id 등의 정보를 활용하여 생성할 수 있고 아래와 같이 만들 수 있다.

public String createAuthorizationUrl() {
    String authorizationUrl = "https://login.microsoftonline.com/" + azureProperties.tenant() + "/oauth2/v2.0/authorize";

    MultiValueMap<String, String> valueMap = new LinkedMultiValueMap<>();
    valueMap.add("client_id", azureProperties.clientId());
    valueMap.add("redirect_uri", azureProperties.redirectUri());
    valueMap.add("response_type", "code");
    valueMap.add("scope", "openid email profile");

    return UriComponentsBuilder.fromUriString(authorizationUrl)
            .queryParams(valueMap)
            .build()
            .toUriString();
}

 

이제 이 이후는 OAuth2.0 과 Spring Security 의 흐름을 잘 이해하고 있어야 한다. 전체적인 코드나 흐름을 다 설명할 수는 없지만 설정 파일에 작성한 내용 위주로 살펴보고자 한다. 일단 관련 implementation 과 설정을 먼저 확인해보자

implementation

이제는 코드 내에 설정을 진행하면서 설계를 진행해나가야 하는데 먼저, 라이브러리는 다음과 같이 추가해주었다. 여기서 azure-active-directory 는 인증과 권한 부여 기능을 쉽게 통합할 수 있도록 도와주는 라이브러리이다. Spring Security 와 Azure Active Directory 를 연결해서 인증된 정보를 사용할 수 있도록 해주는데 더 자세한 내용은 여기 링크에서 확인할 수 있다.

implementation 'org.springframework.boot:spring-boot-starter-oauth2-authorization-server'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'com.azure.spring:spring-cloud-azure-starter-active-directory:5.8.0'

 

이 외에 다른 설정들은 유연하게 추가해주면 될 것 같다.

 

yml 설정의 경우 아래와 같이 설정을 해주었는데 참고로 현재 프로젝트에서는 다른 소셜 로그인도 같이 처리가 되어 있기 때문에 MS 로그인만 구현하는 경우에는 설정 방법이 조금 다를 수도 있을 것 같다.

이 설정값들은 이후 SecurityConfig 파일에서 필요하기 때문에 ConfigurationProperties 어노테이션을 활용하여 속성값에 대한 처리를 진행했다.

oauth2:
  login:
    provider:
      azure:
        tenant: common
        tenant-id: ${TENANT_ID}
        client-id: ${CLIENT_ID}
        client-secret: ${CLIENT_SECRET}
        redirect-uri: ${REDIRECT_URI}

여기서 하나 확인해야할 부분은 tenant 부분이다. 위에서 redirect_uri 를 생성할 때도 이 데이터가 필요했는데 특정 tenant 에 소속된 유저만 가입을 하게 하려면 여기 데이터에 그 조직의 tenant id 를 넣어야 한다. 하지만 모든 유저가 가입이 가능한 일반적인 서비스 애플리케이션 구조라면 common 으로 설정을 해주어야 하고, 앱 설정 시에도 멀티 테넌트가 가능하도록 설정을 해주어야 한다.

이 부분이 가장 헷갈리는 내용이었는데 처음에 tenant 라는 개념을 잘 이해하지 못한 상태에서 구현을 했어서 개인 MS 계정으로는 왜 로그인을 못하는지 이해를 하지 못했었다.

계속 아래와 같은 에러 코드를 만나서 MS 로그인이 원망스러웠지만 문서를 찾아보니 멀티 테넌트가 가능하게 하려면 common 으로 설정해주면 된다고 하여 이슈를 해결했다.

 

이 tenant 라는 값은 나중에 다른 곳에서도 사용된다.

전체적인 OAuth2.0 로그인 흐름을 간단하게 그려보았는데 여기서 authorization_code 를 요청하거나 token 을 요청하는 곳에서 MS 서버로 요청을 보낼 때도 tenant 데이터가 필요하다. 특정 tenant 에 소속된 유저만 가입 가능한 구조가 아니라면 이때도 common 이라는 값으로 호출을 해야한다. 여기 MS Docs 를 살펴보면 다이어그램 그림 위에 MS Login URL 이 작게나마 표시되어 있다.

SecurityConfig

이제 남은 것은 Security Configuration 구성이다. 설정 내용에는 축약된 부분이 있어서 Session 설정이나 csrf 관련 내용은 기본적으로 구성해주는 방법이나 본인의 서비스 방향에 맞게 설정해주면 될 것 같다. 여기서 주요 확인해야할 부분은 HttpSecurity 내 oauth2Login 에 대한 설정인데, 이번 서비스에서는 아래와 같이 구성을 해주었다.

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

    private final SocialSignInProperties socialSignInProperties;
    private final OAuth2AuthorizationRequestRepository authorizationRequestRepository;
    private final CustomOAuth2UserService customOAuth2UserService;
    private final CustomAadOAuth2UserService customAadOAuth2UserService;
    private final OAuth2AuthenticationSuccessHandler successHandler;
    private final OAuth2AuthenticationFailureHandler failureHandler;
    private final ObjectMapper objectMapper;

    private static final String MS_LOGIN_URL = "https://login.microsoftonline.com/";

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                //그 외 설정은 생략
                .oauth2Login(oauth2 ->
                        oauth2.clientRegistrationRepository(clientRegistrationRepository())
                                .authorizationEndpoint(authorizationEndpointConfig ->
                                        authorizationEndpointConfig
                                                .authorizationRequestRepository(authorizationRequestRepository)
                                                .authorizationRequestResolver(
                                                        new CustomAuthorizationRequestResolver(clientRegistrationRepository(), objectMapper)
                                                )
                                )
                                .userInfoEndpoint(userInfoEndpointConfig ->
                                        userInfoEndpointConfig
                                                .userService(customOAuth2UserService)
                                                .oidcUserService(customAadOAuth2UserService)
                                )
                                .successHandler(successHandler)
                                .failureHandler(failureHandler)
                )
                .build();
    }

    public ClientRegistrationRepository clientRegistrationRepository() {
        //AzureClientRegistration 생성(그 외 다른 클라이언트 설정 추가 시 List 로 구성할 수 있음.
        return new InMemoryClientRegistrationRepository(azureClientRegistration);
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        return JwtDecoders.fromOidcIssuerLocation(MS_LOGIN_URL + socialSignInProperties.azure().tenantId() + "/v2.0");
    }
}

이 설정들을 어느 정도 진행했다면 Config 내용과 함께 OAuth2.0 흐름을 간단하게 따라가보고자 한다.

OAuth2.0 흐름과 custom 설계

Security 설정을 했다면 이제 실제로 로그인을 해보면서 내 로그인 정보로 비즈니스 로직을 구성할 차례이다. 비즈니스 로직은 유저 정보를 확인한 뒤 JWT 및 Session 생성 등과 관련된 로직일텐데 이번 블로그에서는 그 내용까지는 다루지 않으려고 한다. 대신 SecurityConfig 내용에서 설정 내용들을 바탕으로 어떻게 정보가 전달되고 이 Custom 설정은 왜 필요한지 정리를 해보고자 한다.

아 그리고 그 전에 CustomOAuth2UserService 와 CustomAadOAuth2UserService 가 왜 둘 다 필요한지 궁금할 수 있을 것 같은데, 현재 MS 로그인 위주로 구현했지만 다른 클라이언트 서비스, 예를 들면 Google, Kakao 등의 소셜 로그인도 같이 연동하고 있다고 생각해서 각각의 로그인을 처리하는 Service 객체로 구성했다.

 

여기서,

- CustomOAuth2UserService 는 일반적인 Google 이나 다른 소셜 로그인을 지원하기 위한 설정이고,

- CustomAadOAuth2UserService 는 Azure Active Directory(AAD) 와 같은 Microsoft 로그인을 지원하기 위한 설정이다.

AAD 는 OIDC(OpenID Connect) 프로토콜을 사용해 인증을 수행하기 때문에 OIDC 기반 설정이 필요한데 여기서 OIDC 란, OAuth2.0 의 액세스 토큰 외에도 ID Token 을 추가로 제공해서 신뢰성 있는 사용자 정보를 가져올 수 있도록 하는 프로토콜을 말한다.

OAuth2LoginConfigurer.clientRegistrationRepository()

그러면 가장 처음으로 확인할 정보는 clientRegistrationRepository 설정이다.

여기서는 클라이언트 설정을 관리하는 역할을 한다. 우리는 MS 로그인만 살펴보았지만 다른 소셜 로그인 서드 파티도 클라이언트로 등록할 수 있으며 client_id, client_secret, scope 등을 포함해서 설정할 수 있다. 아래 clientRegistrationRepository() 메소드를 통해 설정을 진행해주었으며 OAuth2.0 제공자와 통신하기 위해 클라이언트 설정을 가져오고 이 설정이 있어야 인증 서버에 접근할 수 있다.

OAuth2LoginConfigurer.authorizationEndpoint()

그 다음은 권한 부여 엔드포인트를 설정하는 부분이다. 사용자가 로그인을 요청하면 이 설정을 참고하여 인증 서버로 리디렉션을 하는 역할을 하는데 여기서 custom 설계로 2가지의 설정을 추가했다. 가장 먼저 살펴볼 것은 authorizationRequestRepository 이다. 

 

OAuth2AuthorizationRequestRepository

사용자가 인증 과정에서 필요한 정보를 임시로 저장하기 위한 클래스로 일반적으로 불필요할 수도 있는데 설계 과정에서 한 가지 문제 상황을 맞닥뜨려서 해당 클래스를 구현하게 되었다. 단일 서버에서는 발생하지 않았으나 이중화된 서버일 경우에는 위 구조가 갖는 문제가 있다.

 

기본적으로 OAuth2.0 인증 방식은 Session 데이터를 활용한다. 이 정보가 활용되는 곳은 이전에 로그인을 요청한 유저가 맞는지, 즉 이전 인증 요청을 확인할 때 사용이 되고, 확인이 되어야 다음 흐름으로 이어지는 구조이다. 따라서 만약 세션값이 달라지거나 데이터가 없다면 요청에 문제가 있다고 판단하여 인증 과정을 더 이상 진행하지 못하게 된다.

아래 클래스를 디버깅 해보면 loadAuthorizationRequest 과정에서 문제가 생기게 되어 더 이상의 로그인 흐름을 이어가지 못하게 된다.

따라서, AuthorizationRequestRepository<OAuth2AuthorizationRequest> 를 구현한 클래스를 추가로 설계했고 이를 주입시켰다.

해당 클래스에서는 각 메소드를 구현하는데, 임시로 인증 정보를 Cookie 데이터에 저장했다가 다시 확인하는 로직으로 구현하여 위 문제를 해결할 수 있었다.

 

OAuth2AuthorizationRequestResolver

또 다른 설정은 authorizationRequestResolver 이다. 비즈니스 요구사항에 따라 추가적인 파라미터가 필요할 때가 있는데 이때 인증 요청에 대한 커스텀을 진행할 수 있다. 그 파라미터로 어떤 검증을 추가로 할 수도 있고 리디렉션을 시켜야할 수도 있는데 이때 이를 처리해줄 수 있는 클래스가 바로 위 클래스를 구현한 CustomAuthorizationRequestResolver 이다.

 

공식 문서에 의하면, 커스텀한 파라미터를 state 라는 값으로 설정할 수 있다고 한다. 따라서 redirect_uri 뒤에 붙은 추가적인 파라미터에 대해서 위 클래스에서 resolve 를 해준 뒤 OAuth2AuthorizationRequest 에 설정을 해주면 해당 정보를 이후 successHandler 에서 가져올 수 있다.

예시는 다음과 같다.

private OAuth2AuthorizationRequest customAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, CustomSocialParameter customSocialParameter) {
    if (Objects.isNull(customSocialParameter)) {
        return OAuth2AuthorizationRequest.from(authorizationRequest).build();
    }

    try {
        return OAuth2AuthorizationRequest.from(authorizationRequest)
                .state(objectMapper.writeValueAsString(customSocialParameter)) //암호화 등 추가 진행 가능
                .build();
    } catch (JsonProcessingException e) {
	//exception 생략
    }
}

OAuth2LoginConfigurer.userInfoEndpoint()

이 클래스는 사용자 정보를 가져오는 단계이다. 사용자가 소셜 로그인에 성공해서 인증에 성공하면 액세스 토큰을 발급 받는데 이 토큰을 사용해서 사용자 정보를 조회하는 요청을 보낸다.

여기서는 위에서 설명한 바와 같이 CustomOAuth2UserService 와 CustomAadOAuth2UserService 클래스가 각각 인증된 사용자 정보인 OAuth2User, OidcUser 정보를 load 한다.

그러면 로직에 필요한 정보들을 필드 변수로 갖는 CustomOAuth2User 객체를 생성해주어 successHandler 에서 마지막 로직을 처리할 수 있게 된다.

@Getter
public class CustomOAuth2User implements UserDetails, OAuth2User, OidcUser {
	//생략
}

AbstractAuthenticationFilterConfigurer.successHandler()

이제 마지막 과정이다.

앞에 과정들이 성공적으로 처리가 되었다면 successHandler 에서 JWT 생성 및 세션 처리 등의 마지막 로직 처리 후 redirect 를 시킨다.

여기서 만약 state 파라미터가 필요하다면 request.getParameter("state") 를 활용하여 가져올 수 있다. 상세한 로직은 삭제하고 예시로 클래스만 가져와보았다.

@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, AuthenticationException {
        //로직 생략
        
        response.sendRedirect(redirectUrl);
    }
}

AbstractAuthenticationFilterConfigurer.failureHandler()

만약, 소셜 로그인 과정에서 처음에 약관 동의에 취소를 누르거나 어떠한 이유로 실패를 했을 경우 해당 클래스에 요청이 들어오게 된다. 이때는 특정 페이지나 로그인 페이지로 리다이렉트를 시킬 수 있을 것이다.

@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationFailureHandler implements AuthenticationFailureHandler {
		
        //로직 생략
        
        response.sendRedirect(redirectUrl);
        response.getWriter().flush();
    }
}

 

대략적으로 MS 로그인의 설정 과정과 로그인 흐름을 작성해보았다. 많은 블로그나 공식 문서에서 잘 설명되어 있는 부분이 많아서 생략한 내용이 많은데 MS 로그인의 경우 Docs 를 찾는 과정에서도 많이 헤맸기 때문에 조금이나마 도움이 되는 글이 되었으면 좋겠다.