Pierre Mardon
Pierre Mardon

Reputation: 747

Mock Spring's remote JWT service

I'm currently using RemoteTokenServices class:

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Value("${auth-server.url}")
    private String authEndpoint;

    @Value("${security.oauth2.client.client-id}")
    private String clientId;

    @Value("${security.oauth2.client.client-secret}")
    private String clientSecret;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("ms/legacy");
    }

    @Bean
    public ResourceServerTokenServices tokenService() {
        RemoteTokenServices tokenServices = new RemoteTokenServices();
        tokenServices.setClientId(clientId);
        tokenServices.setClientSecret(clientSecret);
        tokenServices.setCheckTokenEndpointUrl(authEndpoint + "/uaa/oauth/check_token");
        return tokenServices;
    }
}

I want to be able to mock this easily and properly for all my endpoints integration tests, knowing that:

Is there a standard way to:

  1. Produce a JWT token by hand ?
  2. Mock all token service accesses easily ?

The expected result would be that I can write an endpoint test with only a few extra lines to setup the right JWT in the request, and the token service would agree on its validity dumbly.

Upvotes: 1

Views: 1953

Answers (1)

Pierre Mardon
Pierre Mardon

Reputation: 747

Given that we don't want to test security at all, the best solution for this kind of case is to:

  • use standard Spring tests security management @WithMockUser along with MockMvc
  • adapt the ResourceServerConfigurerAdapter for tests:
  • create a base class that hosts all the config except for tokens
  • create an inheriting class for non-tests profiles (@ActiveProfiles("!test")) that hosts the token specific configuration
  • create an inheriting class for test profile that deactivates the remote token check (security.stateless(false);)
  • make the test classes use test profile
  • inject the proper token-extracted infos at the right time in tests

Here is how it was implemented in practice:

Base ResourceServerConfigurerAdapter so that the configuration has a major common part between tests and non-tests contexts:

public class BaseResourceServerConfiguration extends ResourceServerConfigurerAdapter {

  @Override
  public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    resources.resourceId("ms/legacy");
  }

  @Override
  public void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().anyRequest().permitAll().and().cors().disable().csrf().disable().httpBasic().disable()
        .exceptionHandling()
        .authenticationEntryPoint(
            (request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
        .accessDeniedHandler(
            (request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED));
  }

}

Its implementation outside for non-test:

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Profile("!test")
public class ResourceServerConfiguration extends BaseResourceServerConfiguration {

    @Value("${auth-server.url}")
    private String authEndpoint;

    @Value("${security.oauth2.client.client-id}")
    private String clientId;

    @Value("${security.oauth2.client.client-secret}")
    private String clientSecret;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("ms/legacy");
    }

    @Bean
    public ResourceServerTokenServices tokenService() {
        RemoteTokenServices tokenServices = new RemoteTokenServices();
        tokenServices.setClientId(clientId);
        tokenServices.setClientSecret(clientSecret);
        tokenServices.setCheckTokenEndpointUrl(authEndpoint + "/uaa/oauth/check_token");
        return tokenServices;
    }
}

And for tests:

@Configuration
@EnableResourceServer
@ActiveProfiles("test")
public class TestResourceServerConfigurerAdapter extends BaseResourceServerConfiguration {

  @Override
  public void configure(ResourceServerSecurityConfigurer security) throws Exception {
    super.configure(security);

    // Using OAuth with distant authorization service, stateless implies that the request tokens
    // are verified each time against this service. In test, we don't want that because we need
    // properly isolated tests. Setting this implies that the security is checked only locally
    // and allows us to mock it with @WithMockUser, @AutoConfigureMockMvc and autowired MockMVC
    security.stateless(false);
  }

}

Inject token specific info with a request filter for tests:

@Component
@ActiveProfiles("test")
public class TestRequestFilter extends OncePerRequestFilter {

  private Optional<InfoConf> nextInfoConf = Optional.empty();

  // Request info is our request-scoped bean that holds JWT info
  @Autowired
  private RequestInfo info;

  @Override
  protected void doFilterInternal(HttpServletRequest httpServletRequest,
      HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
    if (nextInfoConf.isPresent()) {
      info.setInfoConf(nextInfoConf.get());
    }
    filterChain.doFilter(httpServletRequest, httpServletResponse);
  }

  public void setNextInfoConf(InfoConf nextInfoConf) {
    this.nextInfoConf = Optional.of(nextInfoConf);
  }

  public void clearNextInfoConf() {
    nextInfoConf = Optional.empty();
  }

}

And of course make the JWT parsing do nothing when there's no JWT.

We also wrote a small utility component to create the relevant info to inject.

A typical integration test will be like this:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@ActiveProfiles("test")
public class TestClass {

    @Autowired
    protected MockMvc mockMvc;

    @Before
    public void before() {
        // Create an user in DB
        // Inject the related information in our filter
    }

    @After
    public void after() {
        // Cleanup both in DB and filter
    }

    @Test
    @WithMockUser
    public void testThing() throws Exception {
        // Use MockMVC
    }
}

Another solution is to indeed mock the ResourceServerTokenServices but in fact it's much more a pain to build proper tokens, and using Spring's standard security mock seems much more appropriate.

Upvotes: 1

Related Questions