Message-level security with JAX-WS on WebSphere Application Server V7, Part 3
Programmatic client control using Web Services Security APIs
Content series:
This content is part # of # in the series: Message-level security with JAX-WS on WebSphere Application Server V7, Part 3
This content is part of the series:Message-level security with JAX-WS on WebSphere Application Server V7, Part 3
Stay tuned for additional content in this series.
In Part 1, you learned how to build JAX-WS web services that use message-level security by using policy sets that give you a declarative way of specifying how message-level security should be performed. That method works well in most cases and is the preferred way to specify message-level security. However, sometimes the consumers of the service might need more dynamic control in order to programmatically change the UsernameToken value or to dynamically specify which public key to use. The WSS APIs give you this programmatic control over how the consumer builds a SOAP message that adheres to the service provider's WS-Security policy set. Additionally, the WSS APIs allow Spring clients to invoke JAX-WS service providers on WebSphere Application Server V7 using WS-Security.
Create a JAX-WS web service
Let's start with a plain old Java object (POJO) annotated with a web service annotation to define that POJO as a service provider. Simply create a new Dynamic Web Project with a new Java class and copy the code from Listing 1 into that class.
Listing 1. Server-side code
package com.ibm.dwexample; import javax.jws.WebService; @WebService public class HelloWorldProvider { public String sayHello(String msg) { System.out.println("[helloworld service] received " + msg); return "Hello " + msg; } }
After you have saved the code, you should see something like Figure 1 with
the HelloWorldProvider
class created.
Figure 1. Service provider dynamic web project

That's it! A simple POJO with the @WebService
annotation is all that is necessary to create a JAX-WS web service.
Secure the service provider
In this article, we use WS-Security policy sets to require that SOAP messages are encrypted and signed. Rational Application Developer V7.5 comes with a set of default policy sets; the examples in this article use the Username WS-Security default policy set. To secure the server provider, do the following:
- Right-click the service provider and select Manage Policy Set
Attachment as shown in Figure 2.
Figure 2. Attach policy set to service provider
- In the End Point Definition dialog, shown in Figure
3, verify that HelloWorldProviderService is the
selected service and select <all endpoints> as
the list of endpoints that will attach to the policy set. Select
Username WSSecurity default and Provider
sample as the desired policy set and policy set binding.
Click OK, then Finish.
Figure 3. Set Username WSSecurity default policy set
- Deploy the project to WebSphere Application Server V7.
- Verify that the policy set and bindings were successfully attached to
the service provider by opening the administrative console of your
WebSphere Application Server V7 runtime, as shown in Figure 4.
Figure 4. Run administrative console
- Select Services => Service providers =>
HelloWorldProviderService, as shown in Figure 5. Notice
that the Username WSSecurity default policy set is
attached along with the Provider sample bindings.
Figure 5. Verify policy set and bindings
- You've now configured the service provider to require WS-Security. The
Username WSSecurity default policy set uses the Username Token for
authentication, so you also need to enable security in the
administrative console. To do this, select Security =>
Global security and verify that both Enable
administrative security and Enable application
security are selected, as shown in Figure 6.
Tip: You need to restart WebSphere Application Server if you had to enable security.
Figure 6. Enable application security
Next we'll look at how to configure the client consumer to invoke this service provider with WS-Security using the WSS APIs.
Generate a JAX-WS consumer
Now that you have the service provider running on WebSphere Application Server V7 with the WS-Security default policy set and sample bindings, you can create a JAX-WS consumer that invokes this service provider by doing the following:
- From Rational Application Developer, create a new Java project with
the name
HelloWorldConsumer
. - Select the service provider from which Rational Application Developer
will generate a client proxy as shown in Figure 7.
Figure 7. Generate JAX-WS client proxy
- From the Web Service Client wizard, ensure that IBM WebSphere JAX-WS is the chosen web service runtime then click the Client project: link.
- Select
HelloWorldConsumer
as the client project. - Accept the defaults and click the Finish button, which results in Rational Application Developer generating a JAX-WS client proxy class and supporting classes.
Provide client credentials programmatically
Now let's create a Java class that uses the generated JAX-WS proxy to invoke the JAX-WS service provider. Because the service provider is protected according to the specifications of the Username WS-Security default policy set, you need to use the WSS APIs to build a SOAP message programmatically that adheres to the service provider's policy set.
From the HelloWorldConsumer project, create a new Java
class called ClientTest
and replace the
generated code with the code from Listing 2. For your convenience, a
project export of all the code in this article is included in the Download section.
Listing 2. ClientTest.java
package com.ibm.dwexample.client; import java.io.FileInputStream; import java.security.cert.CertStore; import java.security.cert.CertificateFactory; import java.security.cert.CollectionCertStoreParameters; import java.security.cert.X509Certificate; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.xml.ws.BindingProvider; import com.ibm.dwexample.HelloWorldProvider; import com.ibm.dwexample.HelloWorldProviderService; import com.ibm.websphere.wssecurity.callbackhandler.UNTGenerateCallbackHandler; import com.ibm.websphere.wssecurity.callbackhandler.X509ConsumeCallbackHandler; import com.ibm.websphere.wssecurity.callbackhandler.X509GenerateCallbackHandler; import com.ibm.websphere.wssecurity.wssapi.WSSConsumingContext; import com.ibm.websphere.wssecurity.wssapi.WSSFactory; import com.ibm.websphere.wssecurity.wssapi.WSSGenerationContext; import com.ibm.websphere.wssecurity.wssapi.decryption.WSSDecryption; import com.ibm.websphere.wssecurity.wssapi.encryption.WSSEncryption; import com.ibm.websphere.wssecurity.wssapi.signature.WSSSignature; import com.ibm.websphere.wssecurity.wssapi.token.SecurityToken; import com.ibm.websphere.wssecurity.wssapi.token.UsernameToken; import com.ibm.websphere.wssecurity.wssapi.token.X509Token; import com.ibm.websphere.wssecurity.wssapi.verification.WSSVerification; public class ClientTest { public static void main(String[] args) { final String KEY_PATH = "C:/Program Files/IBM/SDP/runtimes/base_v7/profiles /was70profile2/etc/ws-security/samples/"; try { // JAX-WS proxy client generated from RAD HelloWorldProviderService srv = new HelloWorldProviderService(); HelloWorldProvider port = srv.getHelloWorldProviderPort(); // use BindingProvider to hook into message context BindingProvider bp = (BindingProvider) port; Map<String,Object> requestContext = bp.getRequestContext(); WSSFactory wssfactory = WSSFactory.getInstance(); WSSGenerationContext generationContext = wssfactory.newWSSGenerationContext(); // Attach the username token to the message UNTGenerateCallbackHandler untCallback = new UNTGenerateCallbackHandler("admin", "admin",true,true); SecurityToken unt = wssfactory.newSecurityToken(UsernameToken.class, untCallback); generationContext.add(unt); // specify key for signing to match the configuration of the ‘provider sample’ // that is attached to service provider X509GenerateCallbackHandler signerCallback = new X509GenerateCallbackHandler("", KEY_PATH + "dsig-sender.ks", "jks", "client".toCharArray(),"soaprequester", "client".toCharArray(), "CN=SOAPRequester, OU=TRL, O=IBM, ST=Kanagawa, C=JP", null); SecurityToken signerToken=wssfactory.newSecurityToken(X509Token.class, signerCallback); WSSSignature signature = wssfactory.newWSSSignature(signerToken); // specify what to sign signature.addSignPart(WSSSignature.BODY); signature.addSignPart(WSSSignature.ADDRESSING_HEADERS); signature.addSignPart(WSSSignature.TIMESTAMP); signature.addSignPart(unt); generationContext.add(signature); // specify key to use for encryption (same key service provider expecting) X509GenerateCallbackHandler encryptionCallback = new X509GenerateCallbackHandler("", KEY_PATH + "enc-sender.jceks", "jceks", "storepass".toCharArray(),"bob", null, "CN=Bob, O=IBM, C=US",null); SecurityToken encryptingToken = wssfactory.newSecurityToken(X509Token.class, encryptionCallback); WSSEncryption encryption = wssfactory.newWSSEncryption(encryptingToken); encryption.setKeyEncryptionMethod(WSSEncryption.KW_RSA15); // specify what to encrypt encryption.addEncryptPart(unt, false); encryption.addEncryptPart(WSSEncryption.BODY_CONTENT); encryption.addEncryptPart(WSSEncryption.SIGNATURE); generationContext.add(encryption); // encode message according to generationContext setting generationContext.process(requestContext); // specify key to be used for decryption WSSConsumingContext consumerContext = wssfactory.newWSSConsumingContext(); X509ConsumeCallbackHandler decryptionCallback = new X509ConsumeCallbackHandler( "", KEY_PATH + "enc-receiver.jceks", "jceks", "storepass".toCharArray(), "bob", "keypass".toCharArray(), "CN=Bob, O=IBM, C=US"); WSSDecryption decryption = wssfactory.newWSSDecryption(X509Token.class, decryptionCallback); decryption.addAllowedKeyEncryptionMethod(WSSEncryption.KW_RSA15); decryption.addRequiredDecryptPart(WSSDecryption.BODY_CONTENT); decryption.addRequiredDecryptPart(WSSDecryption.SIGNATURE); consumerContext.add(decryption); // Use intermediate certificate authority to verify certificate CertificateFactory cf = CertificateFactory.getInstance("X.509"); X509Certificate cacert = (X509Certificate) cf.generateCertificate(new FileInputStream(KEY_PATH + "intca2.cer")); Set<Object> eeCerts = new HashSet<Object>(); eeCerts.add(cacert); java.util.List<CertStore> certList = new java.util.ArrayList<CertStore>(); CollectionCertStoreParameters certparam = new CollectionCertStoreParameters(eeCerts); CertStore cert = CertStore.getInstance("Collection", certparam, "IBMCertPath"); certList.add(cert); // specify signature verification key X509ConsumeCallbackHandler verificationHandler = new X509ConsumeCallbackHandler( KEY_PATH + "dsig-receiver.ks", "jks", "server".toCharArray(), certList, java.security.Security.getProvider("IBMCertPath")); WSSVerification verification = wssfactory.newWSSVerification(X509Token.class,verificationHandler); consumerContext.add(verification); // decode message according to consumerContext settings consumerContext.process(requestContext); String resp = port.sayHello("Griffith"); System.out.println("[response] " + resp); } catch(Exception e) { e.printStackTrace(); } } }
Be sure to correct the KEY_PATH
value as
required on your system.
The code sample in Listing 2 is configured to match the provider sample binding that you attached to the service provider. If you examine the binding's configuration in the WebSphere Application Server administrative console, you'see settings for the keys used for encryption, decryption, and signing. In a production setting, you should customize the keys as demonstrated in Part 1 of this article series.
Test and verify
Now that you have secured the service provider and developed a service consumer that uses the WSS APIs to adhere to the policy set requirements of the service provider, you can verify that the code works.
- From Rational Application Developer, right-click
ClientTest.java and select Run As =>
Run Configurations. You'll see the Run Configurations
dialog, as shown in Figure 8.
Figure 8. Set JAAS arguments for ClientTest
- The consumer needs to use Java Authentication and Authorization
Service (JAAS) to pass in the username credentials, so you need to
specify the following VM argument to point to the JAAS login
configuration file; for example, by default on the Windows platform:
-Djava.security.auth.login.config=”C:\Program Files\IBM\SDP\runtimes\base_v7\profiles\was70profile1\properties \wsjaas_client.conf”
If you examine the wsjaas_client.conf file, you'll see that it only specifies how to find the classes used by JAAS and does not contain any user credentials or WS-Security settings; that is all done programmatically through the WSS APIs shown in Listing 2. - Now click Run to generate client results, as shown in
Figure 9, and server-side results, as shown in Figure 10.
Figure 9. JAX-WS consumer results
Figure 10. JAX-WS provider results
- You can also use the TCP/IP monitor (see Part 1 for how to configure) to see the SOAP request and
response messages, as shown in Figure 11.
Figure 11. SOAP request and response seen through TCP/IP monitor
Add Spring to the mix
Many clients use the Spring framework to create web service clients, or consumers. The Spring remoting capability provides that ability through a configuration such as the one shown in Listing 3. Spring then essentially creates a dynamic proxy on the fly by which it invokes a web service as defined by the Web Service Definition Language (WSDL) in the configuration. However, because this process is done dynamically at runtime, the "attach policy set" to the client proxy capability demonstrated in Part 1 doesn't have a client proxy to select in Rational Application Developer that can attach to the policy set. Therefore, you can use the WSS APIs to enable Spring clients to connect to a WebSphere Application Server V7 web service protected by WS-Security policy sets.
To do this, you can use JAX-WS handlers to intercept the dynamic Spring
proxy before it sends a SOAP request to the provider and modify the
request as required to comply with the service provider's policy set. As
shown in Listing 3, the Spring configuration contains a
handlerResolver
property that specifies a
callback class. Also note in Listing 3 that the
serviceInterface
used is the one generated by
Rational Application Developer V7.5.
Listing 3. Spring configuration (config/clientSpring.xml)
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns=http://www.springframework.org/schema/beans xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <bean id="myHandler" class="com.ibm.dwexample.client.MyHandlerResolver"/> <bean id="helloProxy" class="org.springframework.remoting.jaxws.JaxWsPortProxyFactoryBean"> <property name="serviceInterface" value="com.ibm.dwexample.HelloWorldProvider"/> <property name="wsdlDocumentUrl" value="http://localhost:9080/HelloWorldProject/HelloWorldProviderService?WSDL" /> <property name="namespaceUri" value="http://dwexample.ibm.com/" /> <property name="serviceName" value="HelloWorldProviderService" /> <property name="portName" value="HelloWorldProviderPort" /> <property name="handlerResolver" ref="myHandler"/> </bean> </beans>
The custom handler simply provides a list of handlers to invoke as shown in Listing 4.
Listing 4. Client-side custom handler list
package com.ibm.dwexample.client; public class MyHandlerResolver implements HandlerResolver { public java.util.List<Handler> getHandlerChain(PortInfo portInfo) { List<Handler> handlerChain = new ArrayList<Handler>(); CredsHandler credsHandler = new CredsHandler(); handlerChain.add(credsHandler); return handlerChain; } }
Since only one handler is being used, Listing 4 shows the handler that uses the WSS APIs to adhere to the Username WSSecurity default policy set attached to the service provider. The code has comments to better explain each part of the Java code. Note that this code is essentially the code in Listing 2, but packaged in a SOAPHandler fashion.
Listing 5. CredsHandler Java class
package com.ibm.dwexample.client; public class CredsHandler implements SOAPHandler<SOAPMessageContext> { // TODO: must point to keys on your system final String KEY_PATH = "C:/Program Files/IBM/SDP/runtimes/base_v7/profiles/was70profile2/etc/ws-security/samples/"; public void close(MessageContext messagecontext) {} public Set<QName> getHeaders() {return null;} public boolean handleFault(SOAPMessageContext messagecontext){return true;} public boolean handleMessage(SOAPMessageContext messagecontext) { Boolean outbound = (Boolean) messagecontext.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY); // only concerned with outbound if (outbound) { System.out.println("[CredsHandler] " + messagecontext); try { WSSFactory wssfactory = WSSFactory.getInstance(); WSSGenerationContext generationContext = wssfactory.newWSSGenerationContext(); // Attach the username token to the message UNTGenerateCallbackHandler untCallback = new UNTGenerateCallbackHandler("admin", "admin",true,true); SecurityToken unt = wssfactory.newSecurityToken(UsernameToken.class, untCallback); generationContext.add(unt); // specify key for signing to match the configuration of the 'provider sample' // that is attached to the service provider X509GenerateCallbackHandler signerCallback = new X509GenerateCallbackHandler("", KEY_PATH + "dsig-sender.ks", "jks", "client".toCharArray(),"soaprequester", "client".toCharArray(), "CN=SOAPRequester, OU=TRL, O=IBM, ST=Kanagawa, C=JP", null); SecurityToken signerToken = wssfactory.newSecurityToken(X509Token.class, signerCallback); WSSSignature signature = wssfactory.newWSSSignature(signerToken); // specify what to sign signature.addSignPart(WSSSignature.BODY); signature.addSignPart(WSSSignature.ADDRESSING_HEADERS); signature.addSignPart(WSSSignature.TIMESTAMP); signature.addSignPart(unt); generationContext.add(signature); // specify key to use for encryption (same key service provider expecting) X509GenerateCallbackHandler encryptionCallback = new X509GenerateCallbackHandler("", KEY_PATH + "enc-sender.jceks", "jceks", "storepass".toCharArray(),"bob", null, "CN=Bob, O=IBM, C=US",null); SecurityToken encryptingToken = wssfactory.newSecurityToken(X509Token.class, encryptionCallback); WSSEncryption encryption = wssfactory.newWSSEncryption(encryptingToken); encryption.setKeyEncryptionMethod(WSSEncryption.KW_RSA15); // specify what to encrypt encryption.addEncryptPart(unt, false); encryption.addEncryptPart(WSSEncryption.BODY_CONTENT); encryption.addEncryptPart(WSSEncryption.SIGNATURE); generationContext.add(encryption); // encode message according to generationContext settings generationContext.process(messagecontext); // specify key to be used for decryption WSSConsumingContext consumerContext = wssfactory.newWSSConsumingContext(); X509ConsumeCallbackHandler decryptionCallback = new X509ConsumeCallbackHandler( "", KEY_PATH + "enc-receiver.jceks", "jceks", "storepass".toCharArray(), "bob", "keypass".toCharArray(), "CN=Bob, O=IBM, C=US"); WSSDecryption decryption = wssfactory.newWSSDecryption(X509Token.class, decryptionCallback); decryption.addAllowedKeyEncryptionMethod(WSSEncryption.KW_RSA15); decryption.addRequiredDecryptPart(WSSDecryption.BODY_CONTENT); decryption.addRequiredDecryptPart(WSSDecryption.SIGNATURE); consumerContext.add(decryption); // use intermediate certificate authority to verify certificate CertificateFactory cf = CertificateFactory.getInstance("X.509"); X509Certificate cacert = (X509Certificate) cf.generateCertificate(new FileInputStream(KEY_PATH + "intca2.cer")); Set<Object> eeCerts = new HashSet<Object>(); eeCerts.add(cacert); java.util.List<CertStore> certList = new java.util.ArrayList<CertStore>(); CollectionCertStoreParameters certparam = new CollectionCertStoreParameters(eeCerts); CertStore cert = CertStore.getInstance("Collection", certparam, "IBMCertPath"); certList.add(cert); // specify signature verification key X509ConsumeCallbackHandler verificationHandler = new X509ConsumeCallbackHandler( KEY_PATH + "dsig-receiver.ks", "jks", "server".toCharArray(), certList, java.security.Security.getProvider("IBMCertPath")); WSSVerification verification = wssfactory.newWSSVerification(X509Token.class,verificationHandler); consumerContext.add(verification); // decode message according to consumerContext settings consumerContext.process(messagecontext); } catch (Exception e) { e.printStackTrace(); } } return true; }}
The handler simply intercepts the outbound message and adds a UNT, then signs and encrypts the various parts of the SOAP message as required by the service provider. The keys used match the sample keys that were attached to the service provider, but you can refer to Part 1 for information on how to customize the keys.
Once the handler is in place, you can create a Spring client that pulls in the dynamic proxy that points to the web service provider deployed on WebSphere Application Server V7. When this Spring bean is invoked, the handler massages the outgoing message:
Listing 6. Client-side SpringTest
package com.ibm.example.client; import com.ibm.example.proxy.HelloWorld; import com.ibm.example.proxy.HelloWorldService; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class SpringTest { public static void main(String[] args) { try { ApplicationContext springCTX = new ClassPathXmlApplicationContext("config/clientSpring.xml"); HelloWorldProvider port = (HelloWorldProvider) springCTX.getBean("helloProxy"); String resp = port.sayHello("Griffith"); System.out.println("[response] " + resp); } catch(Exception e) { e.printStackTrace(); } } }
Note: The SpringTest client also needs the JAAS configuration shown Figure 8.
Summary
Message-level security is necessary for a variety of projects, and this article series has taught you many facets of using WebSphere Application Server V7 to support your message-level security needs. This third article showed how to use the WSS APIs to take programmatic control over how the client builds SOAP messages that comply with the security requirements of a web service provider. That process allows you to dynamically change the username/password pair, for example, in the SOAP header, or perhaps to dynamically choose keys based on the user instance. Additionally, this article showed how Spring clients can use the WSS APIs to consume web services protected with message-level security.
Acknowledgment
The authors would like to give special thanks to Bill Dodd and Ching-Yun Chao for their diligent review of this article and code.
Downloadable resources
- PDF of this content
- Sample project code (jaxws_wssapi.zip | 2.5MB)
Related topics
- Message-level security with JAX-WS on WebSphere Application Server V7, Part 1: Using Rational Application Developer V7.5.2 to build secure JAX-WS Web services (developerWorks, May 2009)
- Message-level security with JAX-WS on WebSphere Application Server V7, Part 2: Integrating JEE authorization (developerWorks, January 2010)
- Build secure Web services using Rational Application Developer V7.0 (developerWorks, April 2007)
- IBM Redbooks: Web Services Feature Pack for WebSphere Application Server V6.1
- IBM Redbooks: IBM WebSphere Application Server V6.1 Security Handbook
- IBM Redbooks: WebSphere Version 6 Web Services Handbook Development and Deployment
- IBM Redbooks: Web Services Handbook for WebSphere Application Server 6.1
- WebSphere Application Server V7 Information Center