javaspringspring-bootjwt

Java Spring Integration Test: Mock BearerTokenAuthentication with Oauth2


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.


Solution

  • RTFM

    @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 same Authentication 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();
    }
    

    Side notes

    It's not intended that this implementation be configured since it is simply an adapter.