Blog

MockMvc에서 HttpOnly 쿠키의 토큰 값을 확인했던 방법

로그인에 JWT를 사용하면서 Access Token과 Refresh Token을 응답으로 보냈고 그중 Refresh Token은 HttpOnly 쿠키에 담았습니다.

HttpOnly는 브라우저 JavaScript에서 쿠키를 읽거나 수정하는 것을 제한하는 속성입니다. 프론트엔드에서 모든 쿠키 설정을 막거나 다른 보안 문제까지 해결해 준다는 뜻은 아닙니다. 적용 범위는 Set-Cookie 문서에서 확인할 수 있습니다.

응답 본문은 jsonPath로 검사할 수 있었지만 쿠키는 달랐습니다. Set-Cookie 헤더에는 토큰 값뿐 아니라 쿠키 이름과 여러 속성이 함께 들어왔고 그 차이 때문에 처음 작성한 비교가 실패했습니다.

요청까지 준비하기

테스트는 given, when, then으로 나눴습니다. 서비스와 토큰 생성기의 반환값을 정한 뒤 ResponseCookie에 HttpOnly, Secure, SameSite, 경로, 수명을 설정했습니다. 요청을 보내는 부분까지는 다음과 같습니다.

ResponseCookie 빌더의 maxAge(long)은 초 단위입니다. 당시 코드에 넣은 3600000을 밀리초나 1시간으로 읽어서는 안 됩니다.

Java
@Test
    @WithMockUser
    @DisplayName("어드민 로그인 성공 테스트")
    void successAdminLogin() throws Exception {
        // given
        given(memberService.loginAdmin(any())).willReturn(member);
        given(jwtProvider.createAccessToken(any())).willReturn("ACCESS_TOKEN");
        given(jwtProvider.createRefreshToken(any())).willReturn("REFRESH_TOKEN");

        ResponseCookie responseCookie = ResponseCookie.from("refreshToken", "REFRESH_TOKEN")
                .httpOnly(true)
                .secure(true)
                .sameSite("None")
                .path("/")
                .maxAge(3600000)
                .build();

        given(memberService.createHttpOnlyCookie(any())).willReturn(responseCookie);
        // when
        ResultActions resultActions = mockMvc.perform(post("/admin/login")
                .contentType("application/json")
                .with(csrf())
                .content(objectMapper.writeValueAsString(adminLoginRequest)));

        ...
    }

/admin/login으로 JSON 요청을 보냈습니다. 아래 테스트에서 Refresh Token은 Set-Cookie, Access Token은 accessToken 헤더로 확인합니다. 본문에는 별도의 응답 DTO가 담깁니다.

헤더 전체를 토큰 값과 비교해서 실패했습니다

Java
@Test
    @WithMockUser
    @DisplayName("어드민 로그인 성공 테스트")
    void successAdminLogin() throws Exception {
        // given
        given(memberService.loginAdmin(any())).willReturn(member);
        given(jwtProvider.createAccessToken(any())).willReturn("ACCESS_TOKEN");
        given(jwtProvider.createRefreshToken(any())).willReturn("REFRESH_TOKEN");

        ResponseCookie responseCookie = ResponseCookie.from("refreshToken", "REFRESH_TOKEN")
                .httpOnly(true)
                .secure(true)
                .sameSite("None")
                .path("/")
                .maxAge(3600000)
                .build();

        given(memberService.createHttpOnlyCookie(any())).willReturn(responseCookie);
        // when
        ResultActions resultActions = mockMvc.perform(post("/admin/login")
                .contentType("application/json")
                .with(csrf())
                .content(objectMapper.writeValueAsString(adminLoginRequest)));

        // then
        resultActions
                .andExpect(status().isOk())
                .andExpect(header().string(HttpHeaders.SET_COOKIE, responseCookie.getValue()))
                .andExpect(header().string("accessToken", "ACCESS_TOKEN"))
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.admin").value(true))
                .andExpect(jsonPath("$.writer").value(true))
                .andDo(print());
    }

responseCookie.getValue()로는 토큰 값만 나오고 Set-Cookie 헤더에는 이름과 속성이 붙어서 옵니다. 당시 기록한 비교 결과입니다.

Expected :REFRESH_TOKEN
Actual   :refreshToken=REFRESH_TOKEN; Path=/; Max-Age=3600000; Expires=Sun, 3 Sep 2023 05:49:05 GMT; Secure; HttpOnly; SameSite=None

기대값은 REFRESH_TOKEN 하나였고 실제 값은 refreshToken=...으로 시작하는 헤더 전체였습니다.

헤더를 꺼내 포함 여부를 검사했습니다

토큰 값만 비교하려면 어떻게 해야 할까요? andExpect에 람다식을 넣어 응답 헤더를 꺼낸 뒤 AssertJ로 검사해 봤습니다.

아래 코드는 토큰 문자열이 헤더에 포함되는지만 확인합니다. 쿠키 이름이 정확한지, 값 전체가 같은지, HttpOnly·Secure·SameSite 속성이 올바른지는 검증하지 않습니다. 여러 쿠키가 반환되는 경우도 이 한 헤더 검사만으로는 모두 확인할 수 없습니다.

Java
@Test
    @WithMockUser
    @DisplayName("어드민 로그인 성공 테스트")
    void successAdminLogin() throws Exception {
        // given
        given(memberService.loginAdmin(any())).willReturn(member);
        given(jwtProvider.createAccessToken(any())).willReturn("ACCESS_TOKEN");
        given(jwtProvider.createRefreshToken(any())).willReturn("REFRESH_TOKEN");

        ResponseCookie responseCookie = ResponseCookie.from("refreshToken", "REFRESH_TOKEN")
                .httpOnly(true)
                .secure(true)
                .sameSite("None")
                .path("/")
                .maxAge(3600000)
                .build();

        given(memberService.createHttpOnlyCookie(any())).willReturn(responseCookie);
        // when
        ResultActions resultActions = mockMvc.perform(post("/admin/login")
                .contentType("application/json")
                .with(csrf())
                .content(objectMapper.writeValueAsString(adminLoginRequest)));

        // then
        resultActions
                .andExpect(status().isOk())
                .andExpect(result -> {
                    String setCookieValue = result.getResponse().getHeader(HttpHeaders.SET_COOKIE);
                    assertThat(setCookieValue).contains(responseCookie.getValue());
                })
                .andExpect(header().string("accessToken", "ACCESS_TOKEN"))
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.admin").value(true))
                .andExpect(jsonPath("$.writer").value(true))
                .andDo(print());
    }

setCookieValue에 헤더 문자열을 저장한 뒤 예상 토큰이 들어 있는지 비교하니 테스트가 통과했습니다.