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
  • 385
  • 6
  • 23

4 Answers4

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,282
  • 1
  • 41
  • 54
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
  • 385
  • 6
  • 23
0

Tested with JUnit 4, spring-boot-starter-parent=2.3.12.RELEASE

I had the same problem when using a TestRestTemplate to test a Spring Boot backend with SSL enabled.

My JUnit tests based on the TestRestTemplate worked fine when the Spring server wasn't using SSL. But as soon as I configured it to use SSL with a self-signed certificate, by setting its property:

server.ssl.enabled=true

I began receiving the same exception as the OP.

After many attempts, I only managed to get a RestTemplate to connect to the SSL-enabled server, but this class doesn't handle the 4xx and 5xx server exceptions like the TestRestTemplate, which catches and unpacks them, allowing you to make assertions or inspect them with a debugger, so if I wanted my tests that triggered a server exception to pass, I would have had to rewrite them.

If only I could get the TestRestTemplate to work with the SSL-enabled server, I could have reused all of my JUnit tests with minimal rewriting.

After some digging and debugging, I discovered that the TestRestTemplate injected into the test class has an embedded RestTemplate with:

errorHandler=TestRestTemplate$NoOpResponseErrorHandler

which behaves the way I wanted. But its requestFactory doesn't support the SSL connections by default.

Long story short, instead of creating a new RestTemplate, I reused the one injected by the framework after giving it the SSL connection capabilities.

Basically you want to:

  1. set its requestFactory to a SSL-enabled one;
  2. capture its rootURI replacing http: with https: if the server is SSL-enabled, or the other way around if it's not;
  3. always use absolute URIs to make the connection with the server.

Here's the code for the base test class:

[...]

import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;

import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;


SpringBootTest.WebEnvironment.RANDOM_PORT)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@RunWith(SpringRunner.class)
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class })
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
@Slf4j
public abstract class BaseTest {
    protected HttpHeaders mHttpHeaders = new HttpHeaders();
    // use this to get its RestTemplate with 4xx and 5xx exception handling and rootUri
    @Autowired
    protected TestRestTemplate mAutowiredTestRestTemplate;
    // the RestTemplate one actually used by derived test classes
    protected RestTemplate mRestTemplate = null;
    // the injected rootURI
    protected String mRootUri;
    // inject flag from config
    @Value("${server.ssl.enabled}")
    private boolean mIsServerSslEnabled;

    // @Before is ok because is run when the class is already instantiated
    // but notice that it's run for every test in the class
    @Before
    public void initTest() {
        if (mRestTemplate == null) {
            initRestTemplateAndRootUri();
        }
    }

    /**
     * Init the mRestTemplate using the injected one with added SSL capabilities
     */
    private void initRestTemplateAndRootUri() {
        final String tplRootUri = mAutowiredTestRestTemplate.getRootUri();
        // fix the rootURI schema according to the SSL enabled state
        mRootUri = mIsServerSslEnabled ? tplRootUri.replace("http:", "https:") : tplRootUri.replace("https:", "http:");
        try {
            mRestTemplate = buildSslRestTemplate();
        } catch (Exception e) {
            // unrecoverable
            throw new RuntimeException(e);
        }
    }

    /**
     * Return the injected RestTemplate modified with a SSL context accepting self-signed certificates
     * 
     * @throws KeyStoreException
     * @throws NoSuchAlgorithmException
     * @throws KeyManagementException
     */
    private RestTemplate buildSslRestTemplate()
            throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException {
        SSLConnectionSocketFactory scsf = new SSLConnectionSocketFactory(
                SSLContexts.custom().loadTrustMaterial(null, new TrustSelfSignedStrategy()).build(),
                NoopHostnameVerifier.INSTANCE);
        CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(scsf).build();
        HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
        // instead of creating a new RestTemplate, reuse the one embedded in the
        // injected TestRestTemplate, which keeps its 4xx and 5xx exceptions handling
        // capabilities, just change its request factory to a SSL-enabled one
        RestTemplate result = mAutowiredTestRestTemplate.getRestTemplate();
        result.setRequestFactory(requestFactory);
        return result;
    }

    /**
     * Helper methods to make an absolute URI from a relative
     */
    protected String makeAbsUri(String relUri) {
        return mRootUri + relUri;
    }

    protected URI makeAbsUri(URI relUri) {
        try {
            return new URI(mRootUri + relUri.toString());
        } catch (URISyntaxException e) {
            // unrecoverable
            throw new RuntimeException(e);
        }
    }

}

While the derived test classes should call the modified mRestTemplate this way:

public class UserTest extends BaseTest {
    private static final String RELATIVE_URL = "/api/v1/user/";

[...]

    @Test
    public void readOneById_idNotExists_ko_notFound() {
        mHttpHeaders.clear();
        mHttpHeaders.set(MY_AUTH_HEADER_KEY, myJwtAuthHeaderValue);
        HttpEntity<String> entity = new HttpEntity<>(null, mHttpHeaders);
        Long userId = 999L;
        // this request should trigger a 4xx server exception
        // always use the absolute URI returned by the base class helper method
        ResponseEntity<MyCustomResponse<Object>> response = mRestTemplate.exchange(makeAbsUri(RELATIVE_URL + userId), HttpMethod.GET,
                entity, new ParameterizedTypeReference<MyCustomResponse<Object>>() {
                });
        // notice that this custom RestTemplate has caught the exception, just like an ordinary TestRestTemplate
        // and so the following code is executed:
        // check response
        assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
        assertNotNull(response.getBody());
        // - payload
        Object payload = response.getBody().getPayload();
        assertNull(payload);
        // - error status
        assertEquals(Status.NOT_FOUND, response.getBody().getStatus());
        // - error message
        Object errorsObj = response.getBody().getErrors();
        assertNotNull(errorsObj);
        assertTrue(errorsObj instanceof HashMap);
        HashMap<?, ?> errorMap = (HashMap<?, ?>) errorsObj;
        String msg = (String) errorMap.get("details");
        assertNotNull(msg);
        assertEquals(mMessageSource.getMessage("user.not.found", new Object[] { "#" + userId }, Locale.getDefault()), msg);
    }

In conclusion, this solution gave me the best of both worlds: a RestTemplate with SSL connection capabilities and the same 4xx and 5xx exception handling semantics of a TestRestTemplate class.

PJ_Finnegan
  • 1,981
  • 1
  • 20
  • 17