Search code examples
javaspring-bootunit-testingspring-securityauth0

How to write unit test for SecurityConfig for spring security


I have a class for spring security, validating token from the user. I got the code from Auth0 website and modified antMatcher part for my configuration. Here is the code:

@EnableWebSecurity
public class SecurityConfig {

    @Value("${auth0.audience}")
    private String audience;

    @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
    private String issuer;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        /*
        This is where we configure the security required for our endpoints and setup our app to serve as
        an OAuth2 Resource Server, using JWT validation.
        */
        http
            .csrf().disable()
            .authorizeRequests()
            .antMatchers(HttpMethod.GET, "/data/actuator/**").permitAll()
            .antMatchers(HttpMethod.PUT, "/data/**").hasAuthority("SCOPE_data:write")
            .anyRequest().authenticated()
            .and().cors()
            .and().oauth2ResourceServer().jwt();
        return http.build();
    }

    @Bean
    JwtDecoder jwtDecoder() {
        /*
        By default, Spring Security does not validate the "aud" claim of the token, to ensure that this token is
        indeed intended for our app. Adding our own validator is easy to do:
        */
        NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
                JwtDecoders.fromOidcIssuerLocation(issuer);
        OAuth2TokenValidator<Jwt> audienceValidator =
                new com.nuance.pindata.health.importer.security.AudienceValidator(audience);
        OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
        OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
        jwtDecoder.setJwtValidator(withAudience);
        return jwtDecoder;
    }
}

I am now trying to write unit test, but there is no good way to test it. I can practically test changing method/path, but it is not straight forward how to write this unit test, and it can be done through integration (automation) tests.

From Spring Security HttpSecurity Configuration Testing, he suggests not writing unit test for such security config as well. What is the right approach here? If I should write unit test, how can I achieve this?


Solution

  • I covered this subject in this Baeldung article.

    You can test actuator endpoints access-control in integration tests only (@SpringBootTest). For your own secured @Components, you can do it also in unit-tests (many samples in this repo):

    • @Controller with @WebMvcTest (@WebfluxTest if you were in a reactive app)
    • plain JUnit with @ExtendWith(SpringExtension.class), @EnableMethodSecurity and @Import of the tested component (@Service or @Repository with method security like @PreAuthorize expressions) to get an autowired instance instrumented with security

    spring-security-test comes with some MockMvc request post-processors (see org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt in your case) as well as WebTestClient mutators (see org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockJwt) to configure Authentication of the right type (JwtAuthenticationToken in your case) and set it in test security context, but both have important limitations:

    • it can be used only with MockMvc and WebTestClient and as so are inefficient when testing something else than a @Controller (it is useless when testing method security on a @Repository or @Service)
    • it does not use the authentication converter in the security context. It builds a stub Authentication instance based using the Java DSL to set properties, but things like the authorities conversion logic or actual Authentication type to build are not considered.

    Sample usage in an integration test (@SpringBootTest) for actuator to be up (but you get the idea for unit-tests):

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <scope>test</scope>
    </dependency>
    
    import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
    
    @SpringBootTest(webEnvironment = WebEnvironment.MOCK)
    @AutoConfigureMockMvc
    class ApplicationIntegrationTest {
    
        @Autowired
        MockMvc api;
    
        @Test
        void givenUserIsAnonymous_whenGetLiveness_thenOk() throws Exception {
            api.perform(get("/data/actuator/health/liveness"))
                .andExpect(status().isOk());
        }
    
        @Test
        void givenUserIsAnonymous_whenGetMachin_thenUnauthorized() throws Exception {
            api.perform(get("/data/machin"))
                .andExpect(status().isUnauthorized());
        }
    
        @Test
        void givenUserIsGrantedWithDataWrite_whenGetMachin_thenOk() throws Exception {
            api.perform(get("/data/machin")
                    .with(jwt().jwt(jwt -> jwt.authorities(List.of(new SimpleGrantedAuthority("SCOPE_data:write"))))))
                .andExpect(status().isOk());
        }
    
        @Test
        void givenUserIsAuthenticatedButNotGrantedWithDataWrite_whenGetMachin_thenForbidden() throws Exception {
            api.perform(get("/data/machin")
                    .with(jwt().jwt(jwt -> jwt.authorities(List.of(new SimpleGrantedAuthority("SCOPE_openid"))))))
                .andExpect(status().isForbidden());
        }
    }
    

    You might also use @WithJwt from this libs I maintain. As opposed to post-processors and mutators in spring-security-test, it:

    • works when testing any kind of @Component
    • uses the authentication converter if it is exposed as a @Bean: it builds a stub org.springframework.security.oauth2.jwt.Jwt from a JSON payload in test resources and calls the actual authentication converter with it.

    Above Sample becomes:

    <dependency>
        <groupId>com.c4-soft.springaddons</groupId>
        <artifactId>spring-addons-oauth2-test</artifactId>
        <version>8.0.0</version>
        <scope>test</scope>
    </dependency>
    
    @SpringBootTest(webEnvironment = WebEnvironment.MOCK)
    @AutoConfigureMockMvc
    class ApplicationIntegrationTest {
    
        @Autowired
        MockMvc api;
    
        @Test
        @WithAnonymousUser
        void givenUserIsAnonymous_whenGetLiveness_thenOk() throws Exception {
            api.perform(get("/data/actuator/health/liveness"))
                .andExpect(status().isOk());
        }
    
        @Test
        @WithAnonymousUser
        void givenUserIsAnonymous_whenGetMachin_thenUnauthorized() throws Exception {
            api.perform(get("/data/machin"))
                .andExpect(status().isUnauthorized());
        }
    
        @Test
        @WithJwt("ch4mp.json")
        void givenUserIsCh4mp_whenGetMachin_thenOk() throws Exception {
            api.perform(get("/data/machin"))
                .andExpect(status().isOk());
        }
    
        @Test
        @WithJwt("tonton-pirate.json")
        void givenUserIsTontonPirate_whenGetMachin_thenForbidden() throws Exception {
            api.perform(get("/data/machin"))
                .andExpect(status().isForbidden());
        }
    }