Im facing an issue when running integration tests on my web layer.
I wanna mock this endpoint:
public ResponseEntity<List<Book>> getBooks(BearerTokenAuthentication auth, @AuthenticationPrincipal OidcUser principal) {...}
So essentially it takes in a bearer token and optionally a principal, looks for the books and returns them.
I also have the following configuration for the security filter chain:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(CsrfConfigurer::disable)
.authorizeHttpRequests((authz) ->
authz.anyRequest().authenticated()
).oauth2ResourceServer(oauth2 -> oauth2.jwt().decoder(jwtDecoder()).jwtAuthenticationConverter(new JwtBearerTokenAuthenticationConverter()));
return http.build();
}
public JwtDecoder jwtDecoder() {
return (token) -> {
JWT jwt = null;
try {
jwt = JWTParser.parse(token);
Map<String, Object> headers = new LinkedHashMap<>(jwt.getHeader().toJSONObject());
Map<String, Object> claims = jwt.getJWTClaimsSet().getClaims();
return new Jwt(token, null, null, headers, claims);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
};
}
So essentially Im receiving a jwt and I'm converting it into an authentication object to put it in springs security context.
Now what Im kinda having trouble with is testing it. Im mocking the jwt using c4_soft.springaddons @WithJwt() where I pass some standard jwt with mock values. This is what my test currently looks like:
@SpringBootTest
@AutoConfigureMockMvc
public class BookApiTest {
private final MockMvc mockMvc;
@MockBean
FeignClient feignClient;
@MockBean
JwtDecoder jwtDecoder;
@Autowired
BookApiTest(MockMvc mockMvc) {
this.mockMvc = mockMvc;
}
@Test
@WithJwt("jwt.json")
public void testGetBooks() throws Exception {
ResponseEntity<List<Book>> responseEntity = ResponseEntity.ok(List.of(sampleBooks));
when(feignClient.getBooks()).thenReturn(responseEntity);
this.mockMvc.perform(MockMvcRequestBuilders.get("/books")
.with(SecurityMockMvcRequestPostProcessors.csrf()))
.andExpect(status().isOk());
}
}
Now my problem is that whenever. I run my test in this setup, Im getting: jakarta.servlet.ServletException: Request processing failed: java.lang.IllegalStateException: Current user principal is not of type [org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication]: JwtAuthenticationToken [Principal=org.springframework.security.oauth2.jwt.Jwt@a795d843, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[]]
And that's whats irritating for me. Provided I have a valid jwt and I'm using the configuration above using @autoconfiguremvc, I expect the jwt to be automatically converted into an Authentication object and provided to the security context. Why it fails during the test, I don't get it.
The spring documentation, articles on SO or other sites could not help me so far.
Maybe someone notices a crucial error or something that Im missing. Id be gratefull.
@WithJwt
and@WithMockJwtAuth
require custom authentication converter to be exposed as a@Bean
(instead of inlining it with a lambda in the SecurityFilterChain definition). The authentication factory needs this bean to build the sameAuthentication
instance as you would get at runtime.
Note that the same warning is included in the Baeldung article about testing with mocked OAuth2 authentications.
// Expose the authorization converter as @Bean
@Bean
JwtBearerTokenAuthenticationConverter authenticationConverter() {
return new JwtBearerTokenAuthenticationConverter();
}
// Inject the @Bean in the filter-chain
@Bean
public SecurityFilterChain filterChain(
HttpSecurity http,
Converter<Jwt, AbstractAuthenticationToken> authenticationConverter) throws Exception {
http
.csrf(CsrfConfigurer::disable)
.authorizeHttpRequests((authz) -> authz
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt()
.decoder(jwtDecoder())
.jwtAuthenticationConverter(authenticationConverter));
return http.build();
}
JwtAuthenticationConverter
. From JwtBearerTokenAuthenticationConverter
Javadoc:It's not intended that this implementation be configured since it is simply an adapter.
It seems pretty unsafe to override the default JWT decoder with one that validates none of the iss
, iat
, and exp
claims.
It's pointless to use .with(SecurityMockMvcRequestPostProcessors.csrf())
in your test since you use .csrf(CsrfConfigurer::disable)
in your security conf.