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:
- An external client sends a HTTP request with a JWT token in the
Authorization
header. - The
JwtRequestfilter
extends theOncePerRequestFilter
and extracts an information from the token. - This is passed to the
CustomUserDetailsService
which checks if a user with this information exists in the database. - 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:
- The
JwtRequestFilter
is not invoked at all, i.e. theloadUserById()
method is not called. - 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 theUsernameNotFoundException
.
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.