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.
CodePudding user response:
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("[email protected]", "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("[email protected]"))).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());
}
...
}
CodePudding user response:
We just fixed the issue.
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);
}