Spring Boot Version: 2.4.2

I'm aware that this is not an actual bug report but it's something I don't understand and probably needs some attention from someone with more knowledge.

My application has starter-web and starter-webflux on the classpath but it's implemented as MVC. It provides the following workflow:

  1. An external client sends a HTTP request with a JWT token in the Authorization header.
  2. The JwtRequestfilter extends the OncePerRequestFilter and extracts an information from the token.
  3. This is passed to the CustomUserDetailsService which checks if a user with this information exists in the database.
  4. Only if this check passes, the controller method is invoked with @AuthenticationPrincipal as parameter.

Now I'd like to test this controller method with Spring's @WebTestClient and mutate it with mockJwt(). I successfully managed to get the test request invoke the CustomUserDetailsService but there seem to be two problems:

  1. The JwtRequestFilter is not invoked at all, i.e. the loadUserById() method is not called.
  2. The user is not found by the CustomUserDetailsService. As I'd like to avoid relying on the actual database, my idea was to mock the repository with Mockito but it always throws the UsernameNotFoundException.

This is the test implementation:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserControllerIT {

    @Autowired
    private WebApplicationContext context;

    private WebTestClient webTestClient;

    @MockBean
    private UserRepo userRepo;

    @BeforeEach
    public void setup() {
        webTestClient = MockMvcWebTestClient
                .bindToApplicationContext(this.context)
                .apply(springSecurity())
                .build();
    }

    @Test
    @WithUserDetails
    public void testIt() {
        final User user = createUser("foo@bar.com", "my-password");

        when(userRepo.findByEmail(anyString())).thenReturn(Optional.of(user));
        when(userRepo.findById(anyLong())).thenReturn(Optional.of(user));

        webTestClient
                .mutateWith(mockJwt())
                .get()
                .uri("/services/user")
                .accept(MediaType.APPLICATION_JSON)
                .exchange()
                .expectStatus().is2xxSuccessful();
    }
}

Here are the relevant classes:

JwtRequestFilter

@Component
@Slf4j
public class JwtRequestFilter extends OncePerRequestFilter {

    private static final String BEARER_PREFIX = "Bearer ";

    private final HandlerExceptionResolver resolver;

    private final JwtService jwtService;
    private final CustomUserDetailsService userDetailsService;

    public JwtRequestFilter(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver, JwtService jwtService, CustomUserDetailsService userDetailsService) {
        this.resolver = resolver;
        this.jwtService = jwtService;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            String token = parseTokenFromRequest(request);
            if (isNotBlank(token)) {
                String subject = jwtService.getSubject(token);
                if (isNotBlank(subject) && isNull(SecurityContextHolder.getContext().getAuthentication())) {
                    final MyaUserDetails userDetails = userDetailsService.loadUserById(Long.valueOf(subject));
                    if (jwtService.validateToken(token, userDetails)) {
                        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                    }
                }
            }
            filterChain.doFilter(request, response);
        } catch (JwtException e) {
            resolver.resolveException(request, response, null, e);
        }
    }

    private String parseTokenFromRequest(HttpServletRequest request) {
        String token = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (isNotBlank(token) && token.startsWith(BEARER_PREFIX)) {
            return token.substring(BEARER_PREFIX.length());
        } else {
            return null;
        }
    }
}

CustomUserDetailsService

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private static final String USER_NOT_FOUND = "User not found";

    private final UserRepo userRepo;

    @Override
    public CustomUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        final User user = userRepo.findByEmail(username)
                .orElseThrow(() -> new UsernameNotFoundException(USER_NOT_FOUND));
        return buildUserDetails(user);
    }

    public CustomUserDetails loadUserById(Long id) throws UsernameNotFoundException {
        final User user = userRepo.findById(id)
                .orElseThrow(() -> new UsernameNotFoundException(USER_NOT_FOUND));
        return buildUserDetails(user);
    }
}

UserController

@GetMapping
public ResponseEntity<UserData> getUserData(@AuthenticationPrincipal CustomUserDetails userDetails) {
    try {
        final UserData userData = userService.getUser(userDetails.getId());
        return ResponseEntity.ok(userData);
    } catch (UserNotFoundException e) {
        log.error("User with id {} not found", userDetails.getId());
        return ResponseEntity.notFound().build();
    }
}

Comment From: wilkinsona

This is to be expected as you haven't configured your MockMvcWebTestClient with any filters.

When Spring Boot auto-configures a MockMvc instance, it adds filter automatically. I expect we'll do something similar when we add support for MockMvcWebTestClient.

I'm aware that this is not an actual bug report but it's something I don't understand and probably needs some attention from someone with more knowledge.

As mentioned in the guidelines for contributing, we prefer to use GitHub issues only for bugs and enhancements. In the future, please use Stack Overflow or Gitter for something like this.