One of my more recent challenges at work has been to secure the web service communication between two servers that live behind a firewall. I wanted the communication to be encrypted, and to require some form of authentication. I also needed to provide access to a specific method from one of the web services to several third parties who's servers are also behind the firewall. These third parties were using Java 1.4 on a BEA WebLogic server, and Apache/PHP on the other. In this entry I will discuss the solution to the problem with exact command line and source code examples.
Web service security was new to me and I explored many avenues before arriving at transport layer SSL and HTTP BASIC authentication. Initially I was looking at using Sun Access Manager to do message level end-to-end security. Later I looked at using the new WSIT (Web Service Interoperability Technology) and it's corresponding NetBeans plugin. These are both very impressive and powerful technologies that are useful in situations where you may want to do single sign on, federated identity, LDAP, etc. I had the opportunity to have a lengthy conversation with three of the WSIT engineers at JavaOne about my scenario. They helped me realize that I don't need this level of sophistication for my simple requirements.
Part 1 – Generating and installing SSL Certificates
I'll start by explaining how to set up transport layer SSL security between two servers. What exactly does that mean? Transport layer security is point to point, meaning that the receiving end will be able to decrypt the entire message. This is fine for my scenario and is what I will show next. In other scenarios, your web service requests might go through multiple points before reaching their destinations (such as a Policy Enforcement Point.) You may not want the middle-man to be able to see the entire message. Access Manager and WSIT enable you to encrypt specific fields in your SOAP payload, or even the entire payload, while leaving the SOAP header clear-text for the middle-man. This is called end-to-end security, or message level security.
Lets begin by describing the setup:
Server A
- Glassfish V2 application server
- WAR file containing a web application that acts as the web service client
Server B
- Glassfish V2 application server
- Central reservation data service. This service is for querying central reservations data. It is an EJB 3.0 service endpoint that lives inside of a JAR. I will also show how to configure a web.xml deployment descriptor for those who prefer servlet service endpoints.
The first step is to generate a new self signed SSL key and certificate for use on Server B. The application server comes with a default key that you definitely want to replace for production use. Keys are stored in a Java keystore file and managed by Java's standard keytool command line utility.
cd /opt/SUNWappserver/domains/domain1/config/
cp keystore.jks keystore-backup.jks
keytool -delete -alias s1as -keystore keystore.jks
Enter keystore password: changeit
Note: the 'changeit' password is the default keystore password. Type it in exactly as you see. At this time I do not know how to tell Glassfish the new password if I changed it. I believe the process is different between development and production environments.
The first two commands are used to make a backup of Glassfish's keystore file because we are going to make some changes to it. Next, we deleted the s1as alias from the keystore. An alias is a name for a key that lives inside of a keystore. Many keys can live inside of a keystore. You use the alias to refer to the key when using keytool, configuring Glassfish, and in other tools such as WSIT and Access Manager. Glassfish uses the s1as alias for it's default SSL key. You can't generate a new key on top of an existing alias, so we needed to delete it first. An other approach would be to create a new alias for the new key, then reconfigure Glassfish to use it.
keytool -genkey -keyalg RSA -keysize 1024 -alias s1as -keystore keystore.jks -validity 365
Enter keystore password: changeit
What is your first and last name?
[Unknown]: serverb.mycompany.com
What is the name of your organizational unit?
[Unknown]: IT
What is the name of your organization?
[Unknown]: My Company
What is the name of your City or Locality?
[Unknown]: Toronto
What is the name of your State or Province?
[Unknown]: ON
What is the two-letter country code for this unit?
[Unknown]: CA
Is CN=serverB.mycompany.com, OU=IT, O=My Company, L=Toronto, ST=ON, C=CA correct?
[no]: yes
Enter key password for
(RETURN if same as keystore password):
Next we generated a new 1024 bit RSA key that will expire in 365 days, and put it in the s1as alias. It is important that you enter the host name of the server to be secured with the certificate being created in the “first and last name” field. Even though the question reads "your first and last name," it is necessary to enter the host name of the computer instead. This should be the same host name that will be used in the URLs to access the server.
keytool -export -alias s1as -file serverb-cert.cer -keystore keystore.jks
Enter keystore password: changeit
Certificate stored in file <serverb-cert.cer>
Next we exported a certificate to serverb-cert.cer so that we can import it into Server A. Restart Server B's Glassfish for the new key to take effect.
Now we will import the certificate into Server A's trusted keystore. This is the keystore where certificate authorities usually go. Since we want our self signed certificate to be trusted, we need to import it into the trusted keystore. To do this, copy the serverb-cert.cer to AppServer\domains\domain1\config\ on Server A. Open a console on Server A, cd to the directory you just copied a certificate into, then run the following command:
keytool -import -alias serverb.mycompany.com -file serverb-cert.cer -keystore cacerts.jks
Enter keystore password: changeit
Owner: CN=serverB.mycompany.com, OU=IT, O=My Company, L=Toronto, ST=ON, C=CA
Issuer: CN=serverB.mycompany.com, OU=IT, O=My Company, L=Toronto, ST=ON, C=CA
Serial number: 4665c3d8
Valid from: Tue Jun 05 16:13:12 EDT 2007 until: Tue Jun 05 16:13:12 EDT 2008
Certificate fingerprints:
MD5: 2D:7F:F8:1D:EB:54:6D:4E:EB:1A:AF:99:34:C1:0A:F2
SHA1: 2A:01:08:85:FD:DC:7A:65:59:E0:07:F0:4E:12:5A:D4:A9:EA:A1:37
Signature algorithm name: MD5withRSA
Version: 1
Trust this certificate? [no]: yes
Certificate was added to keystore
Restart Glassfish on Server A for the new certificate to take effect.
Part 2 – Configuring Realms And Users for HTTP BASIC Authentication
Earlier I said that Server B will host the service, and Server A will be the client. Before we can enable HTTP BASIC authentication in the web service, we need to create the username and password that will be used by the service client. Glassfish has several repositories for user accounts, called realms. We're going to create a new realm for our service, then add a user into it. To do this, log into Server B's Glassfish web console at http://serverB:4848 and log in as admin. The default password is adminadmin.Use the tree on the left to navigate to Configuration --> Security --> Realms. Click the New button to create a new realm and enter the following information:
| Name | myRealm |
|---|---|
| Class Name | com.sun.enterprise.security.auth.realm.file.FileRealm |
| JAAS context | fileRealm |
| Key File | ${com.sun.aas.instanceRoot}/config/myRealm-keyfile |
Click OK. It will save your new realm and bring you back to the list of realms. Click on your new realm to bring up the Edit Realm screen. Click the Manage Users button. Next you will see an empty list of users. Click the New button to create a new user. Enter the following information:
| User ID | testClient |
|---|---|
| Group List | Users |
| New Password | secret |
| Confirm New Password | secret |
Note: You make up the group name. It does not come from a list somewhere else.
Press OK to save. We now have a new realm and user account that can be used for the service.
Part 3 – Creating a Secured Web Service
I will be using NetBeans to create the service. First, create an EJB Module project (Java EE 5) called CentralDataService. Once the project is created, right click the project in the project tree and select New --> Web Service. If you can't find Web Service, click Other to bring up the New File window. Select the Web Services category, and Web Service file type. Click next.
On the New Web Service screen, enter “CentralData” for the service name, and “com.ryandelaplante.centraldata.service” for the package. Click Finish to generate the new web service.
Before we modify the code of the web service, we'll create a Reservation object that will later be used as the return value. Create a “com.ryandelaplante.domain” package, and a new Reservation class inside of it. The code should look like this
package com.ryandelaplante.domain;
public class Reservation {
private String confNumber;
private String firstName;
private String lastName;
public Reservation() {
}
public String getConfNumber() {
return confNumber;
}
public void setConfNumber(String confNumber) {
this.confNumber = confNumber;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
Next, modify the CentralData.java web service to look like the following:
package com.ryandelaplante.centraldata.service;
import com.ryandelaplante.domain.Reservation;
import javax.ejb.Stateless;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;
@Stateless()
@WebService()
public class CentralData {
@WebMethod
public Reservation[] findReservations(@WebParam(name = "lastName")
String lastName) {
Reservation[] results = { new Reservation(), new Reservation() };
results[0].setConfNumber("0001");
results[0].setFirstName("John");
results[0].setLastName("Doe");
results[1].setConfNumber("0002");
results[1].setFirstName("Jane");
results[1].setLastName("Doe");
return results;
}
}
Enabling SSL
To enable transport layer SSL security for this web service, you need to modify the deployment descriptor. For EJB service endpoints you modify the application server specific deployment descriptor (sun-ejb-jar.xml for Glassfish). For servlet endpoint services you modify the web.xml deployment descriptor. Your deployment descriptor might be almost empty by default. You'll need to add the missing parts.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sun-ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Application Server 9.0 EJB 3.0//EN" "http://www.sun.com/software/appserver/dtds/sun-ejb-jar_3_0-0.dtd">
<sun-ejb-jar>
<enterprise-beans>
<ejb>
<ejb-name>CentralData</ejb-name>
<webservice-endpoint>
<port-component-name>CentralData</port-component-name>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</webservice-endpoint>
</ejb>
</enterprise-beans>
</sun-ejb-jar>
The transport-guarantee element is what enables SSL security. If this service was a servlet endpoint in a web project instead of an EJB endpoint, then the web.xml deployment descriptor would have been updated like this:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<security-constraint>
<display-name>SSL</display-name>
<web-resource-collection>
<web-resource-name>Everything</web-resource-name>
<description/>
<url-pattern>/*</url-pattern>
<http-method>GET</http-method>
<http-method>PUT</http-method>
<http-method>HEAD</http-method>
<http-method>POST</http-method>
<http-method>OPTIONS</http-method>
<http-method>TRACE</http-method>
<http-method>DELETE</http-method>
</web-resource-collection>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
</web-app>
Enabling HTTP BASIC Authentication
Next we're going to add HTTP BASIC authentication security. Earlier we had created a new security realm in Glassfish called myRealm and a new user inside the realm called testClient. We put testClient in a group called Users. To enable HTTP BASIC authentication we need to modify the deployment descriptor again:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sun-ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Application Server 9.0 EJB 3.0//EN" "http://www.sun.com/software/appserver/dtds/sun-ejb-jar_3_0-0.dtd">
<sun-ejb-jar>
<security-role-mapping>
<role-name>AuthorizedClients</role-name>
<group-name>Users</group-name>
</security-role-mapping>
<enterprise-beans>
<ejb>
<ejb-name>CentralData</ejb-name>
<webservice-endpoint>
<port-component-name>CentralData</port-component-name>
<login-config>
<auth-method>BASIC</auth-method>
<realm>myRealm</realm>
</login-config>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</webservice-endpoint>
</ejb>
</enterprise-beans>
</sun-ejb-jar>
The security-role-mapping section maps a role name that you make up for use by your web service to a group name used by real user accounts in the application server. The role name could be exactly the same as the group name for simplicity; but I chose a different name to demonstrate how it can be used.
If this service was a servlet endpoint in a web project instead of an EJB endpoint, then the web.xml deployment descriptor would have been updated like this:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<security-constraint>
<display-name>SSL</display-name>
<web-resource-collection>
<web-resource-name>Everything</web-resource-name>
<description/>
<url-pattern>/*</url-pattern>
<http-method>GET</http-method>
<http-method>PUT</http-method>
<http-method>HEAD</http-method>
<http-method>POST</http-method>
<http-method>OPTIONS</http-method>
<http-method>TRACE</http-method>
<http-method>DELETE</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>AuthorizedClients</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<login-config>
<auth-method>BASIC</auth-method>
<realm>myRealm</realm>
</login-config>
<security-role>
<role-name>AuthorizedClients</role-name>
</security-role>
</web-app>
The web.xml deployment descriptor does not have a security-role-mapping section so you need to enter that information into the application server specific deployment descriptor (sun-web.xml for Glassfish). The sun-web.xml file would like look this:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sun-web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Application Server 9.0 Servlet 2.5//EN" "http://www.sun.com/software/appserver/dtds/sun-web-app_2_5-0.dtd">
<sun-web-app error-url="">
<security-role-mapping>
<role-name>AuthorizedClients</role-name>
<group-name>Users</group-name>
</security-role-mapping>
<context-root>/CentralDataService</context-root>
<class-loader delegate="true"/>
<jsp-config>
<property name="keepgenerated" value="true">
<description>Keep a copy of the generated servlet class' java code.</description>
</property>
</jsp-config>
</sun-web-app>
The last step is to specify which methods you would like to secure with HTTP BASIC authentication. You can use the @RolesAllowed annotation on a per-method basis, or for the whole class. In this example we'll use it per method.
package com.ryandelaplante.centraldata.service;
import com.ryandelaplante.domain.Reservation;
import javax.annotation.security.RolesAllowed;
import javax.ejb.Stateless;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;
@Stateless()
@WebService()
public class CentralData {
@WebMethod
@RolesAllowed("AuthorizedClients")
public Reservation[] findReservations(@WebParam(name = "lastName")
String lastName) {
Reservation[] results = { new Reservation(), new Reservation() };
results[0].setConfNumber("0001");
results[0].setFirstName("John");
results[0].setLastName("Doe");
results[1].setConfNumber("0002");
results[1].setFirstName("Jane");
results[1].setLastName("Doe");
return results;
}
}
Note that you can have a different role per method. If you would like to include multiple roles, then use the following syntax:
@RolesAllowed( { “Role1”, “Role2” } )
Part 4 – Creating a Secured Web Service Client
Before we can begin creating the web service client, we need the service's WSDL address. I am using NetBeans to create the client. The CentralData project has a Web Services tree node in the project tree. Expand this node to reveal your web service. Right click the service and select Properties. A properties dialog window will open and the WSDL address is listed at the bottom. Highlight the address and press CTRL-C to copy it into the clipboard.
Create a new web project and name it CentralDataClient. Once the project is created, right click the project in the project tree and select New --> Web Service Client. If you can't find Web Service Client, click Other to bring up the New File window. Select the Web Services category, and Web Service Client file type. Click next.
On the New Web Service Client screen click the WSDL URL radio button and paste the URL into the textbox beside it. Since we have secured this web service, you need to change the URL a bit. Change the http to https, and change the port number from 8080 to 8181 (Glassfish's default https port). In the package name textbox enter com.ryandelaplante.centraldata.client.
Next were going to create a basic servlet that will use the web service client. Right click the project, select New --> Servlet. On the New Servlet screen enter FindRes for the class name, and com.ryandelaplante.servlets for the package. Click the Finish button to generate the servlet and have it added to the web.xml deployment descriptor. The new servlet class's code window will be displayed.
In the project tree, expand the Web Service References node down to Central Data --> CentralDataService --> CentralDataPort --> findReservations. Drag/drop the findReservations method into the servlet code window just before the out.close(); line. NetBeans will generate a few lines of code to help you get started calling the web service operation. The servlet's processRequest method now looks something like this:
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
try { // Call Web Service Operation
com.ryandelaplante.centraldata.client.CentralData port = service.getCentralDataPort();
// TODO initialize WS operation arguments here
java.lang.String lastName = "";
// TODO process result here
java.util.List<com.ryandelaplante.centraldata.client.Reservation> result = port.findReservations(lastName);
out.println("Result = "+result);
} catch (Exception ex) {
// TODO handle custom exceptions here
}
out.close();
}
We're going to tweak the generated the code a bit to display the information from the web service response:
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
try { // Call Web Service Operation
com.ryandelaplante.centraldata.client.CentralData port = service.getCentralDataPort();
// TODO initialize WS operation arguments here
java.lang.String lastName = "Doe";
// TODO process result here
java.util.List<com.ryandelaplante.centraldata.client.Reservation> result = port.findReservations(lastName);
for (com.ryandelaplante.centraldata.client.Reservation res : result) {
out.println("Conf# : " + res.getConfNumber() + ", Name: " +
res.getFirstName() + " " + res.getLastName() + "<br/>");
}
} catch (Exception ex) {
// TODO handle custom exceptions here
}
out.close();
}
You do not need to modify the deployment descriptor to enable SSL because the WSDL URL includes https. As long as Server B's certificate has been added to Server A's trusted keystore then SSL will work.
The last thing left to do is add support for HTTP BASIC authentication. You need to set the username and password on the port object before using it. I have created a getCentralDataPort() convenience method to do this. In the sample code the username and password are hard coded in. You would load this information from somewhere else such as env-entry settings in your deployment descriptor.
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
try { // Call Web Service Operation
com.ryandelaplante.centraldata.client.CentralData port = getCentralDataPort();
// TODO initialize WS operation arguments here
java.lang.String lastName = "Doe";
// TODO process result here
java.util.List result = port.findReservations(lastName);
for (com.ryandelaplante.centraldata.client.Reservation res : result) {
out.println("Conf# : " + res.getConfNumber() + ", Name: " +
res.getFirstName() + " " + res.getLastName() + "<br/>");
}
} catch (Exception ex) {
// TODO handle custom exceptions here
}
out.close();
}
private com.ryandelaplante.centraldata.client.CentralData getCentralDataPort() {
com.ryandelaplante.centraldata.client.CentralData port = service.getCentralDataPort();
((BindingProvider)port).getRequestContext().put(
BindingProvider.USERNAME_PROPERTY, "testClient");
((BindingProvider)port).getRequestContext().put(
BindingProvider.PASSWORD_PROPERTY, "secret");
return port;
}
Now you can deploy the web service client project to Server A and give it a try. If you find any errors or know of a better way to do something then please let me know!
While I was first playing with HTTP BASIC authentication, I was using an EJB session bean instead of a servlet to call the web service operation. The service was a servlet endpoint in a web project and I had enabled authentication for all HTTP type requests (GET, POST, etc.) The @WebServiceRef resource injection seems to try to download the WSDL while the instance of my EJB Session Bean was being created. Because I wasn't able to provide it a username and password at this stage, it would fail. To get around this I ended up removing the HTTP GET line in the service endpoint's web.xml so that HTTP GET did not require authentication. The WSDL is loaded using HTTP GET. I think it is safe to do this because the real web service operations are done using HTTP POST.
Working from his home office in Toronto,
Ryan de Laplante can be found developing software in
Java by day, and obsessing with technology by night.
Ryan has been designing and writing software for
IJW since 1998 and is very passionate about his work.






Thank you very much, a very great job.
Do you tried to use OpenSSL to build your own CA and use your CA to sing certifications?
Thanks
Posted by legolas wood on June 14, 2007 at 02:52 PM EDT #
Thank you. No I have not tried that yet. It is a very good idea because that is probably what people will want to do in production environments. I'll consider doing a future post on that.
Thanks,
Ryan
Posted by Ryan de Laplante on June 14, 2007 at 02:58 PM EDT #
[Trackback] I want my Roller install to use LDAP authentication (instead of its own account database). LDAP auth means cleartext passwords, so I need to run the site over SSL. where glassfish keeps SSL certs and keys Each Glassfish domain has it’s own keysto...
Posted by number 9 on June 26, 2007 at 06:42 PM EDT #
Hello,
I'm trying to do the same but the client is a standalone java 6 client et the server is glassfish V1.
But that doesn't work, the server side is not able to retrieve the principals.
Any idea ?
Posted by Flo on June 30, 2007 at 08:43 AM EDT #
Hi Flo,
I would recommend that you post your question on the users@glassfish.dev.java.net mailing list. It is very active and the Glassfish dev team read it. You can sign up from the Mailing Lists page.
Posted by Ryan de Laplante on June 30, 2007 at 11:47 AM EDT #