Radu Cotescu's professional blog

g33k w17h pa45510n

Java, HTTPS and REST Web Services Using Apache CXF

Securing REST web services is a very debated topic on the Internet. Because REST represents an architecture, and not a protocol - like SOAP -, there aren’t any specifications dealing with security, leaving this aspect as a design decision for the software engineers / developers. Still, if you search the web to find out what are the approaches, you’ll see that most of the results suggest you use basic authentication over HTTPS.

The basic authentication is a trivial way of authenticating HTTP requests directly by the web server, without any added effort for the developer. Since REST isn’t very pretentious (it uses the well known HTTP methods for accessing resources), leaving the authentication to the web server is a natural thing to do. Still, this doesn’t mean that your REST API is secure. You merely separate users that should have access to your API from users that should be denied access. Enabling HTTPS on the server is the next step that needs to be followed, so that all the requests are secured. This avoids the classical man-in-the-middle attack and also assures that the credentials sent by the users aren’t visible to the whole world (although they are BASE64 encoded by the basic authentication mechanism, decoding them is a child’s play).

A practical example

For the next paragraphs, I will be very specific (the title should have already told you this). To add something more, let’s assume you use Maven (because you’re a real developer) and Jetty (only for testing - I assume that for production you use a proper server, like Tomcat). Enabling HTTPS on any other server shouldn’t be much different.

pom.xml

To enable HTTPS and basic user authentication on Jetty by using Maven, you would have to use an additional plug-in for generating keystores, unless you already have a keystore that you would like to use, in which case you should not keep it in the target folder of your project. The following section of pom.xml should do the trick:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>keytool-maven-plugin</artifactId>
  <executions>
      <execution>
          <phase>generate-resources</phase>
          <id>clean</id>
          <goals>
              <goal>clean</goal>
          </goals>
      </execution>
      <execution>
          <phase>generate-resources</phase>
          <id>genkey</id>
          <goals>
              <goal>genkey</goal>
          </goals>
      </execution>
  </executions>
  <configuration>
      <keystore>${project.build.directory}/jetty-ssl.keystore</keystore>
      <dname>cn=server_host_name</dname>
      <keypass>jetty6</keypass>
      <storepass>jetty6</storepass>
      <alias>jetty6</alias>
      <keyalg>RSA</keyalg>
  </configuration>
</plugin>
  <!-- Jetty support for testing -->
<plugin>
  <groupId>org.mortbay.jetty</groupId>
  <artifactId>maven-jetty-plugin</artifactId>
  <executions>
      <execution>
          <id>start-jetty</id>
          <phase>pre-integration-test</phase>
          <goals>
              <goal>run</goal>
          </goals>
          <configuration>
              <scanIntervalSeconds>0</scanIntervalSeconds>
              <daemon>true</daemon>
          </configuration>
      </execution>
      <execution>
          <id>stop-jetty</id>
          <phase>post-integration-test</phase>
          <goals>
              <goal>stop</goal>
          </goals>
      </execution>
  </executions>
  <configuration>
      <stopKey>foo</stopKey>
      <stopPort>9999</stopPort>
      <scanIntervalSeconds>2</scanIntervalSeconds>
      <contextPath>/restws</contextPath>
      <userRealms>
          <userRealm implementation="org.mortbay.jetty.security.HashUserRealm">
              <name>name_of_realm</name>
              <config>src/main/resources/sec.properties</config>
          </userRealm>
      </userRealms>
      <connectors>
          <connector implementation="org.mortbay.jetty.nio.SelectChannelConnector">
              <port>8080</port>
              <maxIdleTime>60000</maxIdleTime>
          </connector>
          <connector implementation="org.mortbay.jetty.security.SslSocketConnector">
              <port>8443</port>
              <maxIdleTime>60000</maxIdleTime>
              <keystore>${project.build.directory}/jetty-ssl.keystore</keystore>
              <password>jetty6</password>
              <keyPassword>jetty6</keyPassword>
          </connector>
      </connectors>
  </configuration>
</plugin>

The highlighted lines indicate the user realm. This is a file which describes to the server who are your users and what roles are they assigned. The specified file contains records of this form:

user: password,role

web.xml

Now you must configure your web application to accept HTTPS encrypted content. This is done in the web.xml file, by adding the following lines of code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<security-constraint>
  <web-resource-collection>
      <web-resource-name>application_name</web-resource-name>
  <!-- all URLs are protected -->
      <url-pattern>/*</url-pattern>
  </web-resource-collection>
  <auth-constraint>
      <role-name>user</role-name>
  </auth-constraint>
  <user-data-constraint>
      <!-- redirect all requests to HTTPS -->
      <transport-guarantee>CONFIDENTIAL</transport-guarantee>
  </user-data-constraint>
</security-constraint>
<login-config>
  <auth-method>BASIC</auth-method>
  <realm-name>name_of_realm</realm-name>
</login-config>

REST web services and Apache CXF

So, everything is set up and working. Well, almost. If your REST web services also include some client code, you might encounter an exception with the following message:

1
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

The exception is thrown because the certificate your server uses is not from a known Certificate Authority. If you have studied the POM example, you’d see that the certificate is generated at every execution of the install goal. Moreover, the certificate is self-signed. From here we have two solutions:

  1. try to create a single key store and save it in the src folder, configuring the server to use the file from this location; then start the server and use the program written by Andreas Sterbenz which you can find here; grab the certificate and move the resulted file into the security folder of your JRE (on Linux this should be /usr/lib/jvm/java-6-sun/jre/lib/security/); although this will work okay on your machine, if your project should be accessed by multiple developers, each member of the team has to add that certificate to his/hers JVM (which is not quite nice);

  2. create an alternative TrustStore used only by your application; this is by far the most elegant solution, which adds some more code to the application but doesn’t force your team members to add dummy certificates to their JVM.

Implementing the TrustStore

The class implementing the X509TrustManager is trivial:

1
2
3
4
5
6
7
8
9
10
11
public class FakeTrustManager implements javax.net.ssl.X509TrustManager {
  public java.security.cert.X509Certificate[] getAcceptedIssuers() {
      return null;
  }
  public void checkClientTrusted(java.security.cert.X509Certificate[] certs,
          String authType) {
  }
  public void checkServerTrusted(java.security.cert.X509Certificate[] certs,
          String authType) {
  }
}

To make all your Apache CXF REST clients use the new trust store, it is necessary to create a static method in an utility class that sets everything up for the clients:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void setClientAuthentication(Object client) {
  Properties p = PropertiesLoader
          .getPropertiesFromFile("config.properties");
  ClientConfiguration config = WebClient.getConfig(client);
  HTTPConduit httpConduit = (HTTPConduit) config.getConduit();
  if (p.getProperty("user.name") != null
          && p.getProperty("user.password") != null) {
      AuthorizationPolicy authorization = new AuthorizationPolicy();
      authorization.setUserName(p.getProperty("user.name"));
      authorization.setPassword(p.getProperty("user.password"));
      httpConduit.setAuthorization(authorization);
  }
  TLSClientParameters tlsParams = new TLSClientParameters();
  TrustManager[] trustAllCerts = new TrustManager[] { new FakeTrustManager() };
  tlsParams.setTrustManagers(trustAllCerts);
  // disables verification of the common name (the host for which the certificate has been issued)
  tlsParams.setDisableCNCheck(true);
  httpConduit.setTlsClientParameters(tlsParams);
}

The properties file you see loaded in the previous code output contains the username and password for authentication, but it’s different from the realm. This is because the realm would be harder to parse, although the information is still plain text. Although it might seem redundant to have the username and password in two files, the realm can actually hold users belonging to different roles - therefore the parsing effort.

The PropertiesLoader class is not standard, but again it’s something trivial:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.net.URL;
import java.util.Properties;
public class PropertiesLoader {
  public static Properties getPropertiesFromFile(String propertiesFile) {
      Properties p = new Properties();
      ClassLoader loader = PropertiesLoader.class.getClassLoader();
      if (loader == null) {
          loader = ClassLoader.getSystemClassLoader();
      }
      URL url = loader.getResource(propertiesFile);
      try {
          p.load(url.openStream());
      } catch (Exception e) {
          System.err.println("Could not load configuration file: "
                  + propertiesFile);
      }
      return p;
  }
}

Having done all this, your clients should now work as expected. Just remember that the FakeTrustStore is to be used only for testing purposes.

Code, How To, Java, Web Services

« Ubuntu and Internet Connection Sharing via a wireless card How to secure your SSH server from brute-force attacks »

Comments