Java Web Programming with Eclipse contents
Last modified February 28, 2011 11:13 am

back next

Web Services (continued)

Video

Security Mechanisms

Overview

In general, web services are made secure on the application level by satisfying the following criteria.

Another application-level security property of interest for web services is non-repudiation, which enables the communicating end points to prove to third parties that they received messages from each other. Non-repudiation is accomplished through the use of digital signatures. We will not add this feature to the example applications.

In this section we show how to satisfy the 3 criteria given above in our example applications. Specifically, we will use Transport Layer Security (TLS), also referred to as Secure Sockets Layer (SSL), in which the server authenticates to the client through the use of the public key infrastructure (PKI). By using TLS/SSL in this way, the server will have authenticated to the client, the communication channel will be private, and tampering attacks will be detected. For client authentication, we will have the client present a secret access key with each request.

Public/Private Key Pair

The TLS/SSL protocol requires that the user posses a public/private keypair. The private key is keep a secret from everyone, but the public key is made public by distributing a public key certificate. In the case of web applications, the public key certificate contains the server's public key and identifying information for the server. The most important piece of identifying information in the certificate is the canonical name (CN), which is set to the host name of the application server. For example, the host name of the web server for California State University San Bernardino is csusb.edu. Also, the certificate is signed by a trusted entity whose self-signed certificate is held by connecting clients.

In our case, the certificate that our server will present to connecting clients is a self-signed certificate, Therefore, in order for clients to accept this certificate, we need to add the server's self-signed certificate to the client's container of trusted certificates called a truststore.

We already generated a self-signed certificate for the publisher application in the chapter on web application security. We did that with the following script, which runs under Windows.

keytool -genkey ^
        -keystore keystore ^
        -alias tomcat ^
        -keyalg RSA ^
        -keysize 2048 ^
        -dname CN=localhost ^
        -storepass changeit ^
        -keypass changeit

Notice that the canonical name is specified within the distinguished name (dname) parameter. To give an example of how the CN attribute is used by clients, consider the following. The client initiates a secure connection with a host with IP address that the DNS system returned for the domain name cnn.com. Initiating a secure connection means the client starts an SSL handshake procedure in which the server sends its certificate to the client. Suppose that the certificate presented to the server contained a CN value of csusb.edu. In this case, the client's secure socket subsystem rejects the connection because the CN value does not match with the host with which it is trying to connect. One way this could happen is if an attacker took control of the DNS server that the client uses to resolve domain names. We set the CN value to localhost because in our development environment, the client will connect to another process running on the same computer.

Create Truststore

Run the following command to extract the server's self-signed certificate from the keystore. Make sure you replace $TOMCAT_HOME with the path to tomcat on your system. Also, run the command in a terminal window with the current directory equal to the wiki folder within your Eclipse workspace. Note that the following command uses ^ for line continuation; for Linux and Mac, use \ instead.

keytool -export ^
        -rfc ^
        -file publisher.cert ^
        -alias tomcat ^
        -storetype JKS ^
        -storepass changeit ^
        -keypass changeit ^
        -keystore $TOMCAT_HOME/conf/keystore 

You may need to try different forms of the above script to make it work on your system. For this purpose, it's convenient to place the command in a file, and run the file from the terminal window. For Windows, you could call the file extract_publisher_cert.bat, and for Linux/Mac, you can call it extract_publisher_cert.sh. I used the following to make the script run correctly on my system.

"C:\Program Files\Java\jre1.6.0_07\bin\keytool" ^
        -export ^
        -rfc ^
        -file publisher.cert ^
        -alias tomcat ^
        -storetype JKS ^
        -storepass changeit ^
        -keypass changeit ^
        -keystore "C:\Program Files\Apache Software Foundation\Tomcat 6.0\conf\keystore"

On successful completion of the command, it will report that it created the file publisher.cert. Check to see that this file exists in the wiki project folder in your Eclipse workspace.

The next step is to create a truststore that the wiki application will use for its secure connections. A truststore is a collection of trusted certificates. Run the following command from within the wiki project folder in your Eclipse workspace. If you are running under Linux or Mac, remember to replace ^ with \ for line continuation.

keytool -import ^
        -noprompt ^
        -alias tomcat ^
        -file publisher.cert ^
        -storetype JKS ^
        -keypass changeit ^
        -storepass changeit ^
        -keystore web/WEB-INF/truststore 

Upon successful completion of the above command, a trust store file will be created in web/WEB-INF, which contains the certificate with the public key used by tomcat to establish TLS/SSL connections.

Modify Deployment Descriptor

We need to add an additional security constraint to the deployment descriptor of the publisher web application, so that requests for its 2 web services are redirected to the secure port. To do this, add the following security-constraint elements to the web-resource-collection element in the web.xml file.

         <url-pattern>/publish</url-pattern>
         <url-pattern>/unpublish</url-pattern>

These elements tell the Web container which URLs to apply the security restriction.

Add the following two lines to the contextInitialized2 method of the init class within the wiki application.

String trustStorePath = servletContext.getRealPath("/WEB-INF/truststore");
System.setProperty("javax.net.ssl.trustStore", trustStorePath);

In the wiki application, locate the following line inside the publish page servlet.

Socket socket = new Socket("localhost", 8080);

Replace this line with the following.

SocketFactory socketFactory = SSLSocketFactory.getDefault();
Socket socket = socketFactory.createSocket("localhost", 8443);

Perform the same replacement in the unpublish page servlet.

Start and stop the publisher and wiki applications, and verify that you can still publish and unpublish wiki pages.

Authenticating Clients

The next step is to add code needed for client authentication. There are many possible ways to do client authentication within the context of an SSL connection. The following are a list of some of the possible client authentication mechanisms.

  1. Use HTTP basic authentication in which the username and password are base 64 encoded and included within a single header in each request.
  2. Include the username and password in nonstandard (application defined) headers in every request.
  3. Include username and password as elements in the XML document submitted to the service.
  4. Require client authentication as part of SSL connection establishment.
  5. Require a secret access key with each request.

In this section, we show how to add client authentication to the publisher web service by requiring that a secret access key be submitted by the client with each request. This is an authentication mechanism that is commonly used for REST-based web services.

Add the following code to the publish news item service. Place the code of the following listing just after the code that extracts the title and link from the request.

      // Authenticate client.
      Element accessKeyElement = item.getChild("accessKey");
      if (accessKeyElement == null)
      {
         resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
         return;
      }
      String accessKey = accessKeyElement.getText();
      User user = new UserDAO().findByAccessKey(accessKey);
      if (user == null)
      {
         resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
         return;
      }

Organize imports. Notice that the findByAccessKey method produces an error because it is not yet defined. Do the following instructions in order to learn a new way to conveniently add the findByAccessKey method to the UserDAO class.

Place the cursor in the string findByAccessKey. Hold down the control key and press 1. From the list choose Create method findByAccessKey(String) in type UserDAO.

Notice that the UserDAO file opens in the editor window and that a skeleton implementation of the findByAccessKey method is inserted in to the file. Replace the auto-generated contents of findByAccessKey with the contents of the following listing.

      ResultSet rs = null;
      PreparedStatement statement = null;
      Connection connection = null;
      try
      {
         connection = getConnection();
         String sql = "select * from user where accesskey=?";
         statement = connection.prepareStatement(sql);
         statement.setString(1, accessKey);
         rs = statement.executeQuery();
         if (!rs.next())
         {
            return null;
         }
         return read(rs);
      }
      catch (SQLException e)
      {
         throw new RuntimeException(e);
      }
      finally
      {
         close(rs, statement, connection);
      }

Modify the read method of UserDAO so that the access key is read from the database and written into the user object that it returns. The read method should look like the contents of the following listing when you are done.

   private User read(ResultSet rs) throws SQLException
   {
      Long id = new Long(rs.getLong("id"));
      String username = rs.getString("username");
      String password = rs.getString("password");
      String accessKey = rs.getString("accesskey");
      User user = new User();
      user.setId(id);
      user.setUsername(username);
      user.setPassword(password);
      user.setAccessKey(accessKey);
      return user;
   }

Notice that the call to setAccessKey on the user object is marked by Eclipse as a compilation error. To fix this, you need to add an accessKey property to the user class. First, open the User class and declare a private member variable called accessKey of type String. Second, add the following accessor methods to the User class.

   public String getAccessKey()
   {
      return accessKey;
   }
   public void setAccessKey(String accessKey)
   {
      this.accessKey = accessKey;
   }

We need to add an accessKey column to the user table in the database. To do this, modify the createdb.sql script in the publisher project so that the user table creation command looks like the contents of the following listing.

create table user
(
   id integer primary key,
   username varchar(255) unique,
   password varchar(255),
   accesskey varchar(255) unique
);

The accesskey column needs to be unique so that an access key maps to a single user. Also, this declaration results in the construction of an index that makes the select command run efficiently when restricting the select to a given access key value using a select clause.

Modify the insertdb.sql script so that an access key is specified for the admin user. The following shows how the access key of 1234 is added to the insert command within insertdb.sql.

insert into user (id, username, password, accesskey) 
values (4, 'admin', 'D033E22AE348AEB5660FC2140AEC35850C4DA997', '1234');

Run the all target of the ant build file so that tables are dropped, re-created and populated with sample data.

Now, we need to modify the unpublish service, so that it also checks for the access key. Add the following code to the doGet method of UnpublishNewsItemService. Place the code of the following listing before the code that looks up the news item to delete.

      // Authenticate client.
      String accessKey = req.getParameter("accessKey");
      if (accessKey == null)
      {
         resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
         return;
      }
      User user = new UserDAO().findByAccessKey(accessKey);
      if (user == null)
      {
         resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
         return;
      }

Don't forget to organize imports.

Because we haven't modified the client in any way, client attempts to publish a news items should fail, since it is not currently programmed to send the access key.

Test

Stop and then start the publisher and wiki applications. Then try to publish a wiki page and verify that the operation fails and that the server reports a JDOMParseException as shown in the following figure.

JDOMParseException

Actually, the publisher web service is returning a failure code through HTTP with the following line:

resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);

The doPost method of the PublishPageServlet does not check for failure codes; it simply assumes that the request succeeded and then tries to parse the body of the response as an XML document with a single id element. Therefore, the wiki application reports a parse error, rather than a more useful error message. To fix this problem, read the HTTP status line separately from the request headers, and then after reading through the headers, check to see if the server returned a success code. The following lines of code show how this can be done in the publish method of the PublishPageServlet in the wiki application. The while(true) loop in the following snippet is already in the publish method; you need to add the line that reads the status line before this loop, and then add the code that checks for success after the loop.

      // Read the HTTP status line (the first line of the HTTP headers in a response).
      String statusLine = br.readLine();
      
      // Read through the header lines.
      while (true)
      {
         String line = br.readLine();
         if (line.length() == 0) break;
      }

      // Check for success code.
      if (!statusLine.startsWith("HTTP/1.1 200"))
      {
         throw new RuntimeException("Publish web service failed with " + statusLine);
      }

Now, restart the wiki application. Try to publish a wiki page and observe the more informative error message that is reported. The following figure shows the modified error message by updating the publish method.

Modified error message for publish method

Now, modify the publish method of PublishPageServlet so that the access key is sent with each request. Add the following two lines in the same place that we create the title and link elements.

Element accessKeyElement = new Element("accessKey");
accessKeyElement.addContent("1234");

Add the following line to the place in the code where we add titleElement and linkElement to the root element.

root.addContent(accessKeyElement);

At this point, the wiki application is sending the access key with publish requests. We still need to have the wiki application send the access key with unpublish requests as well. To do this, modify the line in the unpublish page servlet that constructs the url request. The new line should look as follows.

String requestLine = 
         "GET /publisher/unpublish?id=" + 
         page.getPublishedId() + 
         "&accessKey=1234 HTTP/1.1\r\n";

Restart the wiki application and try to publish and unpublish wiki pages. The applications should work correctly at this point.

back next

Copyright 2007-2009 David Turner and Jinseok Chae. All rights reserved.