spring-mvcspring-securityintegration-testingauth0spring-oauth2

Spring security 6 integration test with JWT complains about missing HandlerMappingIntrospector


I have a Spring boot 3 MVC app with oauth security (spring security 6) and I want to create an integration test for it.

I followed Spring's documentation to set it up but it fails with

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'A Bean named mvcHandlerMappingIntrospector of type org.springframework.web.servlet.handler.HandlerMappingIntrospector is required to use MvcRequestMatcher. Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext.' available at app//org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry.createMvcMatchers(AbstractRequestMatcherRegistry.java:119)

Test test class:

@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [CustomSecurityConfig::class])
@WebAppConfiguration
class SecuritySetupTest {

    @Autowired
    private lateinit var context: WebApplicationContext

    private lateinit var mvc: MockMvc

    @BeforeEach
    fun setup() {
        mvc = MockMvcBuilders
            .webAppContextSetup(this.context)
            .apply<DefaultMockMvcBuilder>(springSecurity())
            .build()
    }

    @Test
    fun contextLoads() {
        assertTrue { true }
    }
}

Security configuration class:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
class CustomSecurityConfig {

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .authorizeHttpRequests {
                it.requestMatchers("/actuator/**", "/swagger/**", "/v3/api-docs" ).permitAll()
                  .anyRequest().authenticated()
            }
            .oauth2ResourceServer {
                it.jwt(Customizer.withDefaults())
            }

        return http.build();
    }
}

As I use auth0 I use their starter as dependency:

implementation 'com.okta.spring:okta-spring-boot-starter:3.0.7'

If I add a HandlerMappingIntrospector explicitly, - which is not needed when I start the app locally or the test with a @SpringBootTest -, it misses a jwtDecoder that I don't want to mock. I specifically want to test the controller layer and not wire in the service layer / using @MockBean while also having api-doc and swagger endpoints.

Help is much appreciated.


Solution

  • In a Spring Boot application, you should write integration tests with @SpringBootTest. Use profiles and test properties if there is some auto-configuration you want to disable during these tests.

    Unit VS Integration Tests

    I specifically want to test the controller layer and not wire in the service layer / using @MockBean while also having api-doc and swagger endpoints.

    As neither Swagger nor Actuator endpoints are part of your code base, they should be excluded from unit tests. Note that a test doesn't have to be "integration" to run with mocked authentications. This can be done in unit tests too. You should probably not try to test access rules to all endpoints in a single test class.

    Use @SpringBootTest (with @AutoconfigureMockMvc) when testing how some endpoints auto-configured by Boot starters integrate with your app, for instance when writing smoke tests or checking those auto-configured enpoints access rules.

    Use @WebMvcTest - which is much faster as it loads much less configuration - when unit testing a @Controller you wrote. In that case, Actuator and Swagger endpoints should be out of scope. Note that it is likely that you'll have to manually @Import(...) the security configuration when testing access control.

    Refer to this Baeldung article I wrote for details of @Controller unit-tests with @WebMvcTest and Spring application integration-tests with @SpringBootTests, both with MockMvc and OAuth2 access control using mocked authentications.

    Mocking the JwtDecoder or Not

    it misses a jwtDecoder that I don't want to mock

    If you don't mock the JWT decoder, you'll have to authorize requests with valid tokens. This requires the authorization server to be accessible during tests for:

    This is more of end-to-end tests which are slow, fragile, and for which JUnit is not adapted: you can hardly mimic the user interaction with the authorization server UI during authorization code flow.

    So, when testing access control, you'd better use mocked JwtDecoder and Authentication instances, as described in the article linked above.

    Use Token Claims to Build Test Authentication

    We can distinguish several steps when creating the security context of a request to a resource server with a JWT decoder:

    1. the JWT Bearer token - which is base64 string - is decoded and validated by a JwtDecoder. If the validation is successful, the decoder outputs a org.springframework.security.oauth2.jwt.Jwt
    2. this output is passed to an authentication converter (a Converter<Jwt, AbstractAuthenticationToken>). The default converter outputs a JwtAuthenticationToken, but we can easily configure one producing anything that extends AbstractAuthenticationToken. It is during this conversion that token claims are turned into authorities.
    3. the AbstractAuthenticationToken instance is put in the security context.

    As wrote above, using an actual JwtDecoder is an issue during tests because it requires an access to an actual authorization server. As a consequence, the step 1. is mocked in unit and integration tests.

    @WithJwt from spring-addons-oauth2-test, is designed to start at step 2.. It uses a JSON payload from the test resources to build a stub org.springframework.security.oauth2.jwt.Jwt and provide it to the authentication converter it finds in the test context - or Spring Security default one if none is exposed as a bean.

    In contrast, the test Authentication factories behind MockMvc request post-processors and WebTestClient mutators start at step 3.. jwt() builds a stub JwtAuthenticationToken it injects in the test security context, but never uses the authentication converter for that, even if it is exposed as a bean. This means that authorities conversion logic in the security conf is not used and also that when using something else than JwtAuthenticationToken in your app, jwt() is inefficient.

    So, when you write "a jwtDecoder that I don't want to mock", if your intention is to have token claims converted in an Authentication instance the same way in tests and at runtime, @WithJwt is what you need.

    You don't Really Need okta-spring-boot-starter

    It isn't updated at a very high frequency, makes your app uselessly adherent to a specific OpenID Provider, gives only limited control over the authentication converter (you can configure nothing but a single role claim), and does not expose it as a bean.

    If your app is a resource server (a REST API authorizing requests with Bearer tokens), using spring-boot-starter-oauth2-resource-server requires no more than configuring the authentication converter to extract authorities from the private claim you asked Auth0 to put roles into. If you expose a Converter<Jwt, AbstractAuthenticationToken> @Bean as done in the first two samples in this doc (but unfortunately not the 3rd), @WithJwt will use it.

    If your app is an OAuth2 client with oauth2Login (requests authorized with session cookies, not tokens), using spring-boot-starter-oauth2-client requires:

    As an alternative to writing this Java conf yourself, you might use this starter of mine, which will do it for you based on a few application properties. This starter is compatible with any OIDC provider and maintained with the latest Boot versions, usually released same day. Also, it exposes the authentication converter as a @ConditionalOnMissingBean, which allows overriding the default one in application code.

    When using spring-addons-starter-oidc, spring-addons-starter-oidc-test provides with some support to load security conf in tests.