Search code examples
javaspring-bootspring-securityauth0

How do I test main app without spring security?


I have a main app that exposes endpoint like this. I have a test like this. It simply uses TestRestTemplate, calls actuator endpoint and checks health:

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = HealthDataImporterApplication.class, webEnvironment = WebEnvironment.RANDOM_PORT)
@TestPropertySource(locations = "classpath:application-test.properties")
public class HealthDataImporterApplicationTest {

    @Autowired
    private TestRestTemplate testRestTemplate;
    @LocalServerPort
    private int serverPort;

    @Test
    public void health() {
        assertThat("status", read(get("health"), "$.status").equals("UP"));
    }

    private String get(final String resource) {
        return testRestTemplate.getForObject(String.format("http://localhost:%s/actuator/%s", serverPort, resource),
                String.class);
    }
}

I now added SecurityConfig class to add authentication to the app. Here is SecurityConfig class:

@EnableWebSecurity
public class SecurityConfig {

    private static final String ACTUATOR_URL = "/actuator/**";
    private static final String SCOPE_PREFIX = "SCOPE_";
    private static final String PERMISSION_SCOPE = "scope:scope";

    @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, ACTUATOR_URL).permitAll()
            .anyRequest().hasAuthority(SCOPE_PREFIX + PERMISSION_SCOPE)
            .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 = JwtDecoders.fromOidcIssuerLocation(issuer);
        jwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(JwtValidators.createDefaultWithIssuer(issuer),
                new AudienceValidator(audience)));
        return jwtDecoder;
    }
}

Main app imports this SecurityConfig class so authentication is there. However, it fails the existing test because it complains it is missing auth0.audience and other value. It seems like it is trying to actually call auth0 url, which I don't want the test to. Otherwise, I have to put actual auth0 audience and domain which is sensitive data.

How can I disable spring security with TestRestTemplate? Is it worth to test this class at all?


Solution

  • As you are using spring-boot, do not override the JwtDecoder just to check audiences. Use spring.security.oauth2.resourceserver.jwt.audiences property instead (this is a comma separated list of acceptable audience).

    Rather than deactivating security, I prefer to use mocked identities and include access-control in test coverage, using MockMvc (yes, even in @SpringBootTest with many components wired).

    Refer to this other answer for details on how to do that in unit-test and integration-test.

    Because I find request post-processors not that readable and I happen to unit-test secured components which are not controllers (like @Service or @Respository with method-security like @PreAuthorize, @PostFilter, etc.), I created a lib with test annotations for OAuth2:

    @SpringBootTest(webEnvironment = WebEnvironment.MOCK)
    @AutoConfigureMockMvc
    class ApplicationIntegrationTest {
    
        @Autowired
        MockMvc api;
    
        // actuator
        @Test
        void givenRequestIsAnonymous_whenGetStatus_thenOk() throws Exception {
            api.get("/actuator/health")
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.status").value("UP"));
        }
    
        // secured resource
        @Test
        void givenRequestIsAnonymous_whenGetMachin_thenUnauthorized() throws Exception {
            api.perform(get("/api/v1/machin"))
                .andExpect(status().isUnauthorized());
        }
    
        @Test
        @WithMockJwtAuth("SCOPE_openid", "SCOPE_scope:scope")
        void givenUserIsGrantedWithExpectedAuthority_whenGetMachin_thenOk() throws Exception {
            api.perform(get("/api/v1/machin"))
                .andExpect(status().isOk());
        }
    
        @Test
        @WithMockJwtAuth("SCOPE_openid")
        void givenUserIsNotGrantedWithExpectedAuthority_whenGetMachin_thenForbidden() throws Exception {
            api.perform(get("/api/v1/machin"))
                .andExpect(status().isForbidden());
        }
    }
    

    As comparison, the same sample with just spring-security-test:

    @SpringBootTest(webEnvironment = WebEnvironment.MOCK)
    @AutoConfigureMockMvc
    class ApplicationIntegrationTest {
    
        @Autowired
        MockMvc api;
    
        // actuator
        @Test
        void givenRequestIsAnonymous_whenGetStatus_thenOk() throws Exception {
            api.perform(get("/actuator/health/liveness"))
                .andExpect(status().isOk());
        }
    
        // secured resource
        @Test
        void givenRequestIsAnonymous_whenGetMachin_thenUnauthorized() throws Exception {
            api.perform(get("/api/v1/machin"))
                .andExpect(status().isUnauthorized());
        }
    
        @Test
        void givenUserIsGrantedWithExpectedAuthority_whenGetMachin_thenOk() throws Exception {
            api.perform(get("/api/v1/machin")
                    .with(jwt().jwt(jwt -> jwt.authorities(List.of(
                            new SimpleGrantedAuthority("SCOPE_openid"),
                            new SimpleGrantedAuthority("SCOPE_scope:scope"))))))
                .andExpect(status().isOk());
        }
    
        @Test
        void givenUserIsNotGrantedWithExpectedAuthority_whenGetMachin_thenForbidden() throws Exception {
            api.perform(get("/api/v1/machin")
                    .with(jwt().jwt(jwt -> jwt.authorities(List.of(
                            new SimpleGrantedAuthority("SCOPE_openid"))))))
                .andExpect(status().isForbidden());
        }
    }