简体   繁体   中英

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:

  • the JWT is decoded in a OncePerRequestFilter to get some crucial info
  • I'm not interested in testing auth failures (well I am but that's not something that we want to do on each endpoint)

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM