简体   繁体   中英

How to mock JWT token to use it with Mockito and Spring Boot

I have a controller which gives the user a 403 response unless they are authenticated with a JWT token which is passed as a Bearer token via the authorization header. I'm looking for resources on how to test this with Mockito but I'm not very successful so far as most of them tell me to use the @WithMockUser annotation, which I understand is for Spring security yes, but does not include the mocking for a JWT token. I've tried to mock a few objects such as the UserDetailsClass and the JwtFilter and even hardcoding the bearer token but I think there should be more to it.

@MockBean
private CategoryCommandService categoryCommandService;

@Autowired
private MockMvc mockMvc;

@MockBean
private MyUserDetailsService myUserDetailsService;

@MockBean
private CategoryRepository categoryRepository;

@MockBean
private JwtUtil jwtUtil;

@Autowired
private JwtRequestFilter filter;


@Test
void testCreateCategory() throws Exception {

    CategoryCreateDto categoryCreateDto = new CategoryCreateDto("category");
    CategoryCreateDto categoryCreateResponseDto = new CategoryCreateDto(UUID.fromString("2da4002a-31c5-4cc7-9b92-cbf0db998c41"), "category");

    String jsonCreate = asJsonString(categoryCreateDto);
    String jsonResponse = asJsonString(categoryCreateResponseDto);

    RequestBuilder request = MockMvcRequestBuilders
            .post("/api/adverts/category")
            .content(jsonCreate)
            .header("Authorization", "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJmb29AZW1haWwuY29tIiwiZXhwIjoxNjM4ODU1MzA1LCJpYXQiOjE2Mzg4MTkzMDV9.q4FWV7yVDAs_DREiF524VZ-udnqwV81GEOgdCj6QQAs")
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .accept(MediaType.APPLICATION_JSON);
    mockMvc.perform(request).andReturn();

    when(categoryCommandService.createCategory(categoryCreateDto)).thenReturn(
            categoryCreateResponseDto);

    MvcResult mvcResult = mockMvc.perform(request)
            .andExpect(status().is2xxSuccessful())
            .andExpect(content().json(jsonResponse, true))
            .andExpect(jsonPath("$.id").value("2da4002a-31c5-4cc7-9b92-cbf0db998c41"))
            .andExpect(jsonPath("$.title").value("category"))
            .andReturn();

    logger.info(mvcResult.getResponse().getContentAsString());
}

Here my controller:

@CrossOrigin
@RequestMapping("/api/adverts/category")
@RestController
public class CategoryCommandController {

@Autowired
private CategoryCommandService categoryCommandService;

@Autowired
private CategoryRepository categoryRepository;

@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Object> createCategory(@RequestBody CategoryCreateDto categoryCreateDto) {

    if (categoryCreateDto.getTitle() != null) {
        return new ResponseEntity<>(categoryCommandService.createCategory(categoryCreateDto), HttpStatus.CREATED);
    }
    else {
        return new ResponseEntity<>(new FeedbackMessage("Missing title"), HttpStatus.BAD_REQUEST);
    }

}
}

And here my filter:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

import static com.example.adverts.SecurityConstants.SIGN_UP_URL;

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

@Autowired
private MyUserDetailsService userDetailsService;

@Autowired
private JwtUtil jwtUtil;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws ServletException, IOException {

    String path = request.getRequestURI();
    if (path.equals(SIGN_UP_URL)) {
        chain.doFilter(request, response);
        return;
    }

    final String authorizationHeader = request.getHeader("Authorization");

    String username = null;
    String jwt = null;

    if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
        jwt = authorizationHeader.substring(7);
        username = jwtUtil.extractUsername(jwt);
    } else {

        response.setStatus(HttpStatus.FORBIDDEN.value());
    }

    if (username != null) {

        UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

        if (jwtUtil.validateToken(jwt, userDetails)) {

            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities());
            usernamePasswordAuthenticationToken
                    .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        }

        chain.doFilter(request, response);

    }


}

}

And JwtUtil class:

@Service
public class JwtUtil {

private String SECRET_KEY = "secret";

public String extractUsername(String token) {
    return extractClaim(token, Claims::getSubject);
}

public Date extractExpiration(String token) {
    return extractClaim(token, Claims::getExpiration);
}

public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
    final Claims claims = extractAllClaims(token);
    return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
    return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
}

private Boolean isTokenExpired(String token) {
    return extractExpiration(token).before(new Date());
}

public String generateToken(UserDetails userDetails) {
    Map<String, Object> claims = new HashMap<>();
    return createToken(claims, userDetails.getUsername());
}

private String createToken(Map<String, Object> claims, String subject) {

    return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
            .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
            .signWith(SignatureAlgorithm.HS256, SECRET_KEY).compact();
}

public Boolean validateToken(String token, UserDetails userDetails) {
    final String username = extractUsername(token);
    return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}

Here is the whole Github branch.

https://github.com/francislainy/adverts-backend/tree/dev_jwt

Thank you.

UPDATE

For clarity if I hardcode a valid token I get a 200 status code but my tests will still fail with nothing returned for content whereas before JWT and Spring security they were passing.

在此处输入图像描述

The main problem is using

@MockBean
private JwtUtil jwtUtil;

Which make JwtRequestFilter perform wrongly in

    if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
        jwt = authorizationHeader.substring(7);
        username = jwtUtil.extractUsername(jwt);
    }

As username will always return null from the mock bean.

To use the actual JwtUtils Add includeFilters to include it in spring context, then we also need to mock myUserDetailsService.loadUserByUsername used in JwtRequestFilter . After that the test will pass. Refer to comment inside below code for the changes.

@WebMvcTest(value = CategoryCommandController.class, includeFilters = {
        // to include JwtUtil in spring context
        @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = JwtUtil.class)})
class CategoryCommandControllerTest {

    Logger logger = LoggerFactory.getLogger(CategoryCommandController.class);

    @MockBean
    private CategoryCommandService categoryCommandService;

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private MyUserDetailsService myUserDetailsService;

    @MockBean
    private CategoryRepository categoryRepository;

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private JwtRequestFilter filter;

    //    @WithMockUser
    @Test
    void testCreateCategory() throws Exception {

        CategoryCreateDto categoryCreateDto = new CategoryCreateDto("category");
        CategoryCreateDto categoryCreateResponseDto = new CategoryCreateDto(UUID.fromString("2da4002a-31c5-4cc7-9b92-cbf0db998c41"), "category");

        String jsonCreate = asJsonString(categoryCreateDto);
        String jsonResponse = asJsonString(categoryCreateResponseDto);
        UserDetails dummy = new User("foo@email.com", "foo", new ArrayList<>());
        String jwtToken = jwtUtil.generateToken(dummy);
        RequestBuilder request = MockMvcRequestBuilders
                .post("/api/adverts/category")
                .content(jsonCreate)
                .header("Authorization", "Bearer " + jwtToken)
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .accept(MediaType.APPLICATION_JSON);
// Below line is not used
//        mockMvc.perform(request).andReturn();

        //             Should be createCategory(eq(categoryCreateDto))?
        when(categoryCommandService.createCategory(categoryCreateDto)).thenReturn(
                categoryCreateResponseDto);
        // Mock Service method used in JwtRequestFilter
        when(myUserDetailsService.loadUserByUsername(eq("foo@email.com"))).thenReturn(dummy);
        MvcResult mvcResult = mockMvc.perform(request)
                .andExpect(status().is2xxSuccessful())
//                .andExpect(content().json(jsonResponse, true))
                .andExpect(jsonPath("$.id").value("2da4002a-31c5-4cc7-9b92-cbf0db998c41"))
//                .andExpect(jsonPath("$.title").value("category"))
                .andReturn();

        logger.info(mvcResult.getResponse().getContentAsString());
    }
    ...
}

We just fixed the issue (accepting the other answer for being a more elegant solution).

1st and easier option:

Disable filter authentication for controller test classes:

@AutoConfigureMockMvc(addFilters = false)
class CategoryCommandControllerTest {

You can then perhaps test jwt authorization separately.

2nd and perhaps better option:

Remove the extra pieces from the configure method within the WebSecurity class to end up with only this.

@Override
protected void configure(HttpSecurity http) throws Exception {

    http.csrf().disable();
}

Then under the JwtRequestFilter class add a return when a 403 is caught on the else part of this if block.

 if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
        jwt = authorizationHeader.substring(7);
        username = jwtUtil.extractUsername(jwt);
    } else {
        response.setStatus(HttpStatus.FORBIDDEN.value());
        return;
    }

And move the doChain.filter piece outside of the other if block.

  if (username != null) {

        UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

        if (jwtUtil.validateToken(jwt, userDetails)) {

            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities());
            usernamePasswordAuthenticationToken
                    .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        }

        // chain.doFilter(request, response);

    }

    chain.doFilter(request, response);

}

@MockBean JwtDecoder jwtDecoder; should be enough in association with either:

Both in action:

@WebMvcTest(GreetingController.class)
@Import(SampleApi.WebSecurityConfig.class)
class GreetingControllerAnnotatedTest {

    @MockBean JwtDecoder jwtDecoder;

    @Autowired
    MockMvc api;

    @Test
    @WithMockJwtAuth(authorities = "ROLE_AUTHORIZED_PERSONNEL", claims = @OpenIdClaims(sub = "Ch4mpy", preferredUsername = "Tonton Pirate"))
    void greetWithAnnotation() throws Exception {
        api.perform(get("/greet")).andExpect(content().string("Hello Ch4mpy! You are granted with [ROLE_AUTHORIZED_PERSONNEL]."));
    }

    @Test
    void greetWithPostProcessor() throws Exception {
        api.perform(get("/greet").with(SecurityMockMvcRequestPostProcessors.jwt()
                .authorities(List.of(new SimpleGrantedAuthority("ROLE_AUTHORIZED_PERSONNEL"))).jwt(jwt -> {
                    jwt.subject("Ch4mpy");
                    jwt.claims(claims -> claims.put(StandardClaimNames.PREFERRED_USERNAME, "Tonton Pirate"));
                }))).andExpect(content().string("Hello Ch4mpy! You are granted with [ROLE_AUTHORIZED_PERSONNEL]."));
    }
}

Complete sample there

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM