OAuth2.0 인증 성공 후, AuthenticationSuccessHandler 테스트해보자

 

문제

OAuth2.0 인증 프로세스 중, 사용자를 식별하기 위한 토큰을 생성하는 것을 검증하려고 한다.

문제는 OAuth2.0는 외부 인증 서버를 사용하고 인증 프로세스 대부분이 짜여진 인터페이스를 구현하는 형태라,

외부 인증 서버에서 사용자 인증 성공을 받고 나서의 상황을 테스트에서 어떻게 구현할지 난감했었다.

 

테스트 대상

@RequiredArgsConstructor
@Component
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final JwtProvider jwtProvider;

    @Override
    public void onAuthenticationSuccess(
            HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException {
        super.clearAuthenticationAttributes(request);

        CustomOAuth2User auth2User = (CustomOAuth2User) authentication.getPrincipal();
        Long userId = auth2User.getUserId();
        String role = auth2User.getRole();

        String generatedToken = jwtProvider.generateToken(userId, role, ACCESS_TOKEN_DURATION);
        response.setHeader(HEADER_AUTHORIZATION, generatedToken);
        response.sendRedirect("/login/success");
    }

}

 

OAuth2 인증에 성공하면

1. 토큰을 Authorization 헤더에 넣고

2. /login/success 로 리다이렉트한다.

 

어려웠던 점

@AutoConfigureMockMvc
@SpringBootTest
class OAuth2SuccessHandlerTest {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    OAuth2SuccessHandler oAuth2SuccessHandler;

    @MockBean
    JwtProvider jwtProvider;

    @DisplayName("OAuth2.0 로그인 인증이 성공하면 사용자에게 jwt 발급하고 /login/success 리다이렉트한다.")
    @Test
    @WithCustomOAuth2MockUser
    void onAuthenticationSuccess() throws Exception {
        //given
        String token = "test_token";
        given(jwtProvider.generateToken(anyLong(), anyString(), any(Duration.class)))
                .willReturn(token);

        //when 
        oAuth2SuccessHandler.onAuthenticationSuccess();
        
        // then
        mockMvc.perform(get("/test"))
                .andExpect(status().isOk())
                .andExpect(header().stringValues(HEADER_AUTHORIZATION, token));
    }
}

 

처음에는 WebMvcTest로 요청을 보내서 테스트하려 했다. 문제는 @WithCustomOAuth2MockUser(@WithMockUser)을 붙이면 인증 프로세스를 생략해서 success handler가 작동할 수 없었다. 대부분의 로그인 테스트는 토큰이 발급된 후의 요청에 초점이 잡혀있어, 인증 과정 안에 있는 로직을 참고하기 어려웠다.

참고로 위 @WithCustomOAuth2MockUser은 @WithMockUser처럼 인증 프로세스를 생략하고 인증된 요청처럼 보이도록 한다. 프로덕션 코드에서 요청의 공통 로직을 처리하기 위해 Authentication에 필드를 추가하였고, 검증에 필요한 부분이기 때문에 커스텀 어노테이션을 작성했다.

 

최종 구현

@DisplayName("OAuth2.0 로그인 인증이 성공하면 사용자에게 jwt 발급하고 /login/success 리다이렉트한다.")
@Test
void onAuthenticationSuccess() throws Exception {
    //given
    OAuth2User oauth2User = getOauth2User();
    Authentication authentication = mock(Authentication.class);
    given(authentication.getPrincipal())
            .willReturn(oauth2User);

    String token = "test_token";
    given(jwtProvider.generateToken(anyLong(), anyString(), any(Duration.class)))
            .willReturn(token);

    //when
    oAuth2SuccessHandler.onAuthenticationSuccess(request, response, authentication);

    // then
    verify(response).setHeader(eq(HEADER_AUTHORIZATION), eq(token));
    verify(response).sendRedirect("/login/success");
}

private OAuth2User getOauth2User() {
    String nameAttributeKey = "sub";
    String registrationId = "test_reg";
    Long userId = 1L;
    String userRole = "test_role";

    return new CustomOAuth2User(
            List.of(new SimpleGrantedAuthority(USER.toString())),
            Map.of(nameAttributeKey, registrationId),
            nameAttributeKey,
            userId,
            userRole
    );
}

Authorization Server로부터 인증 성공을 받았다고 가정하기 위해서 SecurityContext에 저장된 Authentication을 mock으로 처리한다. Authentication은 사용자 정보(UserInfo)를 담은 OAuth2User 커스텀 객체를 반환하도록 한다. 

 

토큰 생성기, jwtProvider의 검증은 위 테스트 메서드의 관심사가 아니므로 mock으로 처리한다.

 

@DisplayName("인증에 성공한 후 발급받은 토큰은 2시간 동안 유효하다.")
@Test
void accessTokenDuration() throws Exception {
    //given
    OAuth2User oauth2User = getOauth2User();
    Authentication authentication = mock(Authentication.class);
    given(authentication.getPrincipal())
            .willReturn(oauth2User);

    //when
    oAuth2SuccessHandler.onAuthenticationSuccess(request, response, authentication);

    // then
    Duration acceessTokenDuration = Duration.ofHours(2);
    verify(tokenProvider, only()).generateToken(anyLong(), anyString(), eq(acceessTokenDuration));
}

 

 

리프레쉬 토큰을 검증하기 위해서 엑세스 토큰의 시간을 변경하는 경우가 있다.

운영 서버에서 이와 같은 상황을 방지하기 위해 유효시간 테스트를 짰다.

시간에 대한 검증을 하기 위해서 토큰 생성 메서드에 시간을 외부에서 주입할 수 있도록, 검증할 수 있도록 수정했다.