Spring Security WebTestClient does not invoke OncePerRequestFilter when mocking JWT

Viewed 499

Spring Boot Version: 2.4.2

My application implements 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 rely 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();
    }
}
0 Answers
Related