Home > other >  How to test Spring Security config with resource server?
How to test Spring Security config with resource server?

Time:02-01

I have Spring Security config that configures OAuth2 resource server and endpoints with the predefined scopes and what you can do with what endpoint. Config is below:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests(
                        auth -> auth
                                .antMatchers("/api/v1/private/**")
                                .hasAnyAuthority("SCOPE_api:read", "SCOPE_api:write")
                                .antMatchers("/api/v1/public/**")
                                .authenticated()
                ).oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
                .httpBasic().disable()
                .csrf().disable()
                .formLogin().disable()
                .logout().disable();
        return http.build();
    }
}

There are multiple answers on the similar question on StackOverflow such as 1, 2, 3, even though I look at them, I cannot wrap my head around it and how to test the setup, correctness etc.

My questions has two parts:

  • How can I test my config? There is a nice Baeldung article but it forces to run 2 separate applications to integration tests to succeed. Great for demo, not so great for CI and development.
  • What is normally can be tested in these use-cases? What is normal to unit-test and what is normal to integration test?

I have solved similar task with testcontainers and Keycloack, but that was possible as we had in test and prod Keycloack. This time, I have no control over resource server and I don't know it's type, so it makes my head to explode.

CodePudding user response:

As with all 3rd parties, you shouldn't test a 3rd party service (at least not from your Java project). You'll just have to trust that the devs are doing their job, and that the service correctly issues a JWT.

What you want to test are OAuth scopes (aka Authorities in Spring Security). That is done by mocking your Authentication context so that it includes a principal with your required scopes.

The way I set it up in my projects is the following:

  1. Make sure you have org.springframework.security:spring-security-test as a dependency

  2. In your test source, create an annotation that will act as your fake user setup

    @Retention(RetentionPolicy.RUNTIME)
    @WithSecurityContext(factory = WithMockUserSecurityContextFactory.class)
    public @interface WithMockUser {
        long userId() default 1L;
        String[] authorities() default "ROLE_USER";
    }
    
  3. Again in your test source make a class that will mock the Security Context

    public class WithMockUserSecurityContextFactory implements WithSecurityContextFactory<WithMockUser> {
        @Override
        public SecurityContext createSecurityContext(WithMockUser annotation) {
            var context = SecurityContextHolder.createEmptyContext();
    
            var authorities = Arrays.stream(annotation.authorities())
                    .map(SimpleGrantedAuthority::new)
                    .toList();
    
            var principal = UserPrincipal.builder()
                    .userId( annotation.userId() )
                    .email("[email protected]")
                    .authorities( authorities )
                    .enabled(true)
                    .build();
    
            var auth = new UserPrincipalAuthentication(principal);
            context.setAuthentication(auth);
            return context;
        }
    }
    
    

    Note that UserPrincipalAuthentication is my custom class that extends AbstractAuthenticationToken.

  4. Now you should be able to annotate your test methods with the custom annotation and pass in the authorities you want to test for, such as this:

    @AutoConfigureMockMvc
    @SpringBootTest
    class EndpointTest {
        @Test
        @WithMockUser(authorities = "scope:READ")
        void returnsUserData_ifHasReadScope() {
            // ...
            // MockMvc calls to your endpoint, and assertions
        }
    }
    

What this does, is it completely removes the 3rd party resource server from testing scope. In our test scope we don't care which server the JWT came from, it doesn't matter how it was decoded (that can be done through other unit tests), all that matters is if Access Control is working, given that User comes with a particular set of Authorities.

CodePudding user response:

As stated in @Thome answer, testing JWT decoding and validation should not be in the scope of your Java tests (this is spring framework team business unless you write your own decoder).

What I test regarding security rules is access-control and spring-security-test comes with some MockMvc request post-processors (see org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt for instance) as well as WebTestClient mutators (see org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockJwt) to configure Authentication of the right type and set it in test security context (but this is limited to MockMvc and WebTestClient and as so to @Controller tests.

You might also use annotations like @WithMockJwtAuth from this libs I maintain. This repo contains quite a few samples with unit and integration tests of secured controllers but also services.

Sample usage for a /secured-route end-point with hasRole("AUTHORIZED_PERSONNEL"):

        <dependency>
            <groupId>com.c4-soft.springaddons</groupId>
            <artifactId>spring-addons-oauth2-test</artifactId>
            <version>6.0.12</version>
            <scope>test</scope>
        </dependency>
@WebMvcTest(controllers = GreetingController.class, properties = { "server.ssl.enabled=false" })
class GreetingControllerTestWithSpringAddons {

    @MockBean
    MessageService messageService;
    
    @Captor
    ArgumentCaptor<String> whoCaptor;

    @Autowired
    MockMvc api;

    @Test
    void givenUserIsAnonymous_whenGetSecuredRoute_thenUnauthorized() throws Exception {
        api.perform(get("/secured-route")).andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockJwtAuth("ROLE_AUTHORIZED_PERSONNEL")
    void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenOk() throws Exception {
        final var secret = "Secret!";
        when(messageService.getSecret()).thenReturn(secret);

        api.perform(
                get("/secured-route"))
                .andExpect(status().isOk()).andExpect(content().string(secret));
    }

    @Test
    @WithMockJwtAuth("admin")
    void givenUserIsAuthenticatedButNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenForbidden() throws Exception {
        api.perform(get("/secured-route")).andExpect(status().isForbidden());
    }
}

P.S. In the same repo, you'll find starters to simplify your resource server security config (and also synchronize sessions and CSRF protection disabling...)

  • Related