Enterprise Java

SSL / TLS REST Server – Client with Spring and TomEE

When building a system, developers usually disregard the security aspects. Security has been always something very important to worry about, but it’s attracting even higher concerns than before. Just this year we had a few cases like the Heartbleed Bug or the CelebrityGate scandal. This has nothing to do with the post, but are just examples that security really matters and we should be aware of it.

With the increasing popularity of REST services it makes sense that these need to be secured in some way. A couple of weeks ago, I had to integrate my client with a REST service behind https. I have never done it before and that’s the reason for this post. I have to confess that I’m no security expert myself, so please correct me if I write anything stupid.

The Setup

For this example I have used the following setup:

I’m not going into many details about SSL and TSL, so please check here for additional content. Note that TLS is the new name for SSL evolution. Sometimes there is confusion between the two and people often say SSL, but use the newest version of TSL. Keep that in mind.

Don’t forget to follow the instructions on the following page to setup SSL for Tomcat: SSL Configuration HOW-TO. This is needed for the server to present the client with a set of credentials, a Certificate, to secure the connection between server and client.

The Code

Service

Let’s create a simple Spring REST Service:

RestService.java

@Controller
@RequestMapping("/")
public class RestService {
    @RequestMapping(method = RequestMethod.GET)
    @ResponseBody
    public String get() {
        return "Called the get Rest Service";
    }
}

And we also need some wiring for this to work:

RestConfig.java

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.radcortez.rest.ssl")
public class RestConfig {}

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app
    version="3.1"
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">

    <servlet>
        <servlet-name>rest</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
        </init-param>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>com.radcortez.rest.ssl</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>rest</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <security-constraint>
        <web-resource-collection>
            <web-resource-name>Rest Application</web-resource-name>
            <url-pattern>/*</url-pattern>
        </web-resource-collection>
        <user-data-constraint>
            <!-- Needed for our application to respond to https requests -->
            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
        </user-data-constraint>
    </security-constraint>
</web-app>

Please, note the elements security-constraint, user-data-constraint and <transport-guarantee>CONFIDENTIAL</transport-guarantee>. These are needed to specify that the application requires a secure connection. Check Securing Web Applications for Java Applications.

Running the Service

Just deploy the application on the TomEE Server using your favourite IDE environment and access https://localhost:8443/. You should get the following (you might need to accept the server certificate first):

rest-service-localhost

Note that the browser protocol is https and the port is 8443 (assuming that you kept the default settings in SSL Configuration HOW-TO.

Client

Now, if you try to call this REST service with a Java client, most likely you are going to get the following message and Exception (or similar):

Message: I/O error on GET request for “https://localhost:8443/”:sun.security.validator.ValidatorException:

Exception: Caused by: 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

This happens because the running JDK does not have a valid certificate for your server. You can import it, and get rid of the problem, but let’s do something more interesting. We are going to programatically supply a trusted keystore with our server certificate.

This is especially useful if:

  • you are running your code into multiple environments
  • you don’t have to manually import the certificate into the JDK every time
  • if you upgrade the JDK you have to remember about the certificates
  • for some odd reason you don’t have access to the JDK itself to import the certificate

Let’s write some code:

RestClientConfig.java

@Configuration
@PropertySource("classpath:config.properties")
public class RestClientConfig {
    @Bean
    public RestOperations restOperations(ClientHttpRequestFactory clientHttpRequestFactory) throws Exception {
        return new RestTemplate(clientHttpRequestFactory);
    }

    @Bean
    public ClientHttpRequestFactory clientHttpRequestFactory(HttpClient httpClient) {
        return new HttpComponentsClientHttpRequestFactory(httpClient);
    }

    @Bean
    public HttpClient httpClient(@Value("${keystore.file}") String file,
                                 @Value("${keystore.pass}") String password) throws Exception {
        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        FileInputStream instream = new FileInputStream(new File(file));
        try {
            trustStore.load(instream, password.toCharArray());
        } finally {
            instream.close();
        }

        SSLContext sslcontext =
                SSLContexts.custom().loadTrustMaterial(trustStore, new TrustSelfSignedStrategy()).build();
        SSLConnectionSocketFactory sslsf =
                new SSLConnectionSocketFactory(sslcontext, new String[]{"TLSv1.2"}, null,
                                               BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
        return HttpClients.custom().setSSLSocketFactory(sslsf).build();
    }

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }
}

Here we use Spring RestOperations interface which specified a basic set of RESTful operations. Next we use Apache HTTP Components SSLConnectionSocketFactory which gives us the ability to validate the identity of the server against a list of trusted certificates. The certificate is loaded from the same file used on the server by KeyStore.

RestServiceClientIT.java

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = RestClientConfig.class)
public class RestServiceClientIT {
    @Autowired
    private RestOperations rest;

    @Test
    public void testRestRequest() throws Exception {
        ResponseEntity response = rest.getForEntity("https://localhost:8443/", String.class);
        System.out.println("response = " + response);
        System.out.println("response.getBody() = " + response.getBody());
    }
}

A simple test class. We also need a properties file with the keystore file location and password:

config.properties

keystore.file=${user.home}/.keystore
keystore.pass=changeit

This should work fine if you used all the defaults.

Running the Test

If you now run the test which invokes the REST service within a Java client, you should get the following output:

Response: <200 OK,Called the get Rest Service,{Server=[Apache-Coyote/1.1], Cache-Control=[private], Expires=[Thu, 01 Jan 1970 01:00:00 WET], Content-Type=, Content-Length=[27], Date=[Tue, 23 Dec 2014 01:29:20 GMT]}>

Body: Called the get Rest Service

Conclusion

That’s it! You can now call your REST service with your client in a secured manner. If you prefer to add the certificate to the JDK keystore, please check this post.

Stay tuned for an equivalent for Java EE JAX-RS equivalent.

Resources

You can clone a full working copy from my github repository: REST SSL.

Roberto Cortez

My name is Roberto Cortez and I was born in Venezuela, but I have spent most of my life in Coimbra – Portugal, where I currently live. I am a professional Java Developer working in the software development industry, with more than 8 years of experience in business areas like Finance, Insurance and Government. I work with many Java based technologies like JavaEE, Spring, Hibernate, GWT, JBoss AS and Maven just to name a few, always relying on my favorite IDE: IntelliJ IDEA.Most recently, I became a Freelancer / Independent Contractor. My new position is making me travel around the world (an old dream) to customers, but also to attend Java conferences. The direct contact with the Java community made me want to become an active member in the community itself. For that reason, I have created the Coimbra Java User Group, started to contribute to Open Source on Github and launched my own blog (www.radcortez.com), so I can share some of the knowledge that I gained over the years.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button