1

My project has a series of integration tests that use TestRestTemplate and MockMvc. These had been passing successfully.

I have now added Spring Boot Starter Security and Spring Security OAuth2 Autoconfigure dependencies to my project. I have added a custom class that extends WebSecurityConfigurerAdapter to allow open access (for the moment) to my applicaiton. Here is the class

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
            .authorizeRequests()
            .anyRequest()
            .permitAll();
    }

    @Override
    public void configure(WebSecurity webSecurity) {
        webSecurity
            .ignoring()
            .antMatchers(HttpMethod.OPTIONS, "/**");
    }
}

The application also needs to act as an OAuth2 Resource Server so I have also annotated my main class with @EnableResourceServer. I provide the path to the trusted key store as run parameters when running the application. -Djavax.net.ssl.trustStore=<where the cert is stored locally> -Djavax.net.ssl.trustStorePassword=<the password>

The application works fine but now all of the integration tests are failing. Here is an example of the error common to all the tests that use the TestRestTemplate

Could not fetch user details: class org.springframework.web.client.ResourceAccessException, I/O error on GET request for <the path to my userinfo URL>: 
PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: 
unable to find valid certification path to requested target; nested exception is javax.net.ssl.SSLHandshakeException: 
PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

It seems that the TestRestTemplate I am using for my tests needs to be instructed to use the same keystore that the application does. Is it possible to do this? How would it work for MockMvc?

Space Cadet
  • 365
  • 4
  • 19

3 Answers3

3

Solution for Spring Boot 2

The following answer is aimed at folk developing against Spring Boot 2 and using self-signed certificates for development (proper certificates recommended for production - see https://letsencrypt.org/).

You can create a keystore file containing self-signed certs using the keytool command: -

keytool -genkey -storetype PKCS12 \
    -alias selfsigned_localhost_sslserver \
    -keyalg RSA -keysize 2048 -validity 3650 \
    -dname "CN=localhost, OU=Engineering, O=Acme Corp, L=New York, S=New York, C=US" \
    -noprompt -keypass changeit -storepass changeit \
    -keystore keystore-self-signed.p12

The keystore-self-signed.p12 file will contain a self-signed certificate and this file can be moved into the src/main/resources folder (or src/test/resources if you prefer).

Add the following to your application.yaml Spring config to use SSL and point to the keystore: -

server:
  port: 443
  ssl:
    enabled: true
    key-store: classpath:keystore-self-signed.p12
    key-store-type: PKCS12
    protocol: TLS
    enabled-protocols: TLSv1.2   # Best practice - see https://github.com/ssllabs/research/wiki/SSL-and-TLS-Deployment-Best-Practices
    key-password: changeit
    key-store-password: changeit

Let's create a super simple Spring Boot controller endpoint to test: -

@RestController
public class PingController {

    @GetMapping("/ping")
    public ResponseEntity<String> ping() {
        return new ResponseEntity<>("pong", HttpStatus.OK);
    }

}

We can now hit this endpoint with a curl command (or Postman) i.e.

$ curl https://localhost/ping --insecure --silent
pong

Note: if we don't include --insecure then curl will return curl: (60) SSL certificate problem: self signed certificate.

To test a proper Spring Boot integration test to his endpoint using TestRestTemplate then we can do the following: -

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class PingControllerTest {

    @Value("${server.ssl.key-store}")
    private Resource keyStore;   // inject keystore specified in config

    @Value("${server.ssl.key-store-password}")
    private String keyStorePassword;  // inject password from config

    @LocalServerPort
    protected int port;   // server port picked randomly at runtime

    private TestRestTemplate restTemplate;

    @Before
    public void setup() throws Exception {
        SSLContext sslContext = new SSLContextBuilder()
            .loadTrustMaterial(
                keyStore.getURL(),
                keyStorePassword.toCharArray()
            ).build();
        SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext);
        HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory).build();
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(
            httpClient);
        RestTemplateBuilder rtb = new RestTemplateBuilder()
            .requestFactory(() -> factory)
            .rootUri("https://localhost:" + port);
        this.restTemplate = new TestRestTemplate(rtb, null, null, HttpClientOption.SSL);
    }

    @Test
    public void shouldPing() {
        ResponseEntity<String> result = restTemplate.getForEntity("/ping", String.class);
        assertEquals(HttpStatus.OK, result.getStatusCode());
        assertEquals("pong", result.getBody());
    }


}

As you can see the setup method creates an instance of the SSLContext object which loads (and "trusts") the self-sign certs in the keystore-self-signed.p12 file (injected via the Spring Resource object).

The SSLContext class is injected into a SSLConnectionSocketFactory object, which in turn is injected into a HttpClient object which is then injected into a HttpComponentsClientHttpRequestFactory object.

This factory object is finally injected into a TestRestTemplate instance for use in the shouldPing integration test.

NOTE - I initially lost time with the following code:

...
this.restTemplate = new TestRestTemplate(rgb);

... but this returned ...

org.springframework.web.client.ResourceAccessException: I/O error on GET request for "https://localhost:56976/ping": 
    sun.security.validator.ValidatorException: PKIX path building failed: 
    sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target; nested exception is 
    javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: 
    sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

After debugging through the TestRestTemplate I realised that you must use the 4 parameter constructor of TestRestTemplate with HttpClientOption.SSL i.e.

this.restTemplate = new TestRestTemplate(rtb, null, null, HttpClientOption.SSL);

However, if you're using normal RestTemplate (e.g. outside of Spring tests) then the following works: -

...
RestTemplate restTemplate = new RestTemplate(rgb);

NOTE, to improve - create a @Bean method which returns a TestRestTemplate instance.

bobmarksie
  • 3,005
  • 1
  • 38
  • 48
1

I think you may also need to pass -Djavax.net.ssl.trustStore= -Djavax.net.ssl.trustStorePassword= parameters while running tests. For running single test pass arguments in configuration and in maven also you can pass these parameters.

Below two links might help

Specifying trust store information in spring boot application.properties

http://codeboarding.com/tag/testresttemplate/

0

Thank you, the first link you posted was very useful. This is my working code for a RestTemplate that accepts any cert, if anyone else finds it useful. It's still dependent on valid tokens being provided but that's another story.

private RestTemplate buildRestTemplate() throws Exception {
    SSLContext sslContext = new SSLContextBuilder()
        .loadTrustMaterial(
            new TrustSelfSignedStrategy()
        ).build();
    SSLConnectionSocketFactory socketFactory =
        new SSLConnectionSocketFactory(sslContext);
    HttpClient httpClient = HttpClients.custom()
        .setSSLSocketFactory(socketFactory).build();
    HttpComponentsClientHttpRequestFactory factory =
        new HttpComponentsClientHttpRequestFactory(httpClient);
    return new RestTemplate(factory);
}
Space Cadet
  • 365
  • 4
  • 19