Implementing WS-Security with CXF in a WSDL-First Web Service
Security is one of the most common requirements for SOAP-based web services. Several standards exist, among them WS-Security and WS-SecurityPolicy. They can be hard to implement, and they are often ignored in favor of a more ad hoc security standard, most often using password authentication in the message itself and SSL for transport layer security.
Trying to implement these standards recently, I had a very hard time finding a consistent and complete guide for doing so, or even a good explanation of the standards themselves. I did find good information on Glen Mazza's Blog, and my implementation and this tutorial owe much to that information. But that tutorial is based on another one which is in turn based on another one. I found it difficult to filter through the layers to find what was necessary. Thus, I wrote this to provide a more complete and easy to use guide.
This tutorial will try to take you step-by-step through adding a security policy to an existing working web service WSDL as well as adding the additional CXF and Spring configuration necessary to make it work. It will not tell you how to build a CXF web service to start with, or how to configure Spring to make it work.
Tools Needed
- Maven, 2.2.1 or better
- JDK 1.6 or better
Code
You can find the code necessary for the tutorial here:
Technologies and Techniques Used
This tutorial uses Apache CXF to provide the backing for a JAX-WS web service which is built WSDL-First.
It uses CXF instead of the Glassfish jaxws-ri implementation or the embedded JDK implementation because I found getting jaxws-ri to do the same thing very cumbersome: it needed to reside in an endorsed standards directory (which puts an installation burden on any system administrators using the product); it requires annotations in the WSDL to work correctly; it requires different annotations for the client and server, so two WSDL versions need maintenance; and it failed with a fatal bug when SOAP faults were returned. CXF exhibited none of these problems, and was easy to integrate with Spring. That said, we generate the JAX-WS and JAXB code with Sun/Oracle's standard tools to make sure they're compliant.
The service is built WSDL-first because I believe that this is the most implementation-independent way of producing a SOAP-based web service, and because I think it gives you better interfaces by forcing you to think of them as services, rather than as java methods. It also allows us to clearly specify the security policy, which makes it easier for service consumers to comply.
This example also uses a multi-module Maven project which separates the WSDL, the generated JAX-WS code, and the service implementation/WAR into separate modules, which allows for easy re-use of the WSDL and/or the generated code.
The tutorial example also uses Spring, and the starting code consists of a complete working web service, packaged as a WAR, configured via Spring. Although various techniques are used to construct the configuration, I won't be explaining the base Maven or Spring configuration in detail.
That said, there are some "tricks" in the code that might cause problems moving this example into an existing web service project:
- The WSSecurityTutorialJaxWs project uses binding customizations to make the generated code more Java-friendly. These are like any other standard JAX-WS binding customizations, but you should note they exist.
- The WSSecurityTutorialJaxWs unpacks the WSDL into a temporary directory for generation; it also unpacks the WSDL into the target/classes directory so that it ends up in the final WAR. This is because various tools, including CXF, can load the WSDL from the classpath rather than from the endpoint server, and so it is added to the jar as a convenience.
- The WSSecurityTutorialWAR module is configured by various files through Spring, using an extension of Spring's property placeholder functionality which will, if necessary, read properties from system property or JNDI env values. There are three tiers of property configuration files: a default one, a deployment one, and a test one. The intent is for the default one (in src/main/resources) to be rolled into the WAR, for the deployment one to be modified and deployed to the deployment server's file system, and its location specified via a system property or JNDI value.
- SLF4J is used for logging, and configuration files in the META-INF directories of the WAR and test classpaths force CXF to use SLF4J as well.
- The WAR module also uses TestNG instead of JUnit, which allows us to "group" tests. A normal build will run the "unit" and "local-integration" groups. Adding the "integration-test" profile to the build (e.g., 'mvn clean install -Pintegration-test') executes the "remote-integration" group and uses a plugin to start Tomcat so that the service can be tested running in a container.
Getting Started
You can download the starting code here. If you unzip that, you should be able to CD, on the command line, into the WSSecurityTutorialParent module and execute "mvn clean install -Pintegration-test" successfully. If not, you have something wrong with your environment, and you will have to diagnose it before you can continue.
Altering the WSDL
To begin, you have to decide what the service's security policy will actually be, and modify the WSDL to specify it.
Aside from the specifications themselves, there seems to be precious little information about the security specification standard (WS-SecurityPolicy) available. Some information can be found http://wso2.org/library/3132 here, here, and here.
Basically to declare a security policy for your web service, you have to define the policy using the http://schemas.xmlsoap.org/ws/2004/09/policy (wsp) and http://schemas.xmlsoap.org/ws/2005/07/securitypolicy (sp) schemas in your WSDL, and then attach the policy declarations to the service, operation, and/or input/output bindings that you want controlled by that policy.
A policy is declared with the "WS-Policy" schema/vocabulary (http://schemas.xmlsoap.org/ws/2004/09/policy wsp), and looks like this, basically:
WS-Policy Declaration
<wsp:Policy wsu:Id="UniqueIdentifier">
<wsp:ExactlyOne>
...
</wsp:ExactlyOne>
</wsp:Policy>
Inside the policy declaration, which in itself doesn't define what the policy is, you need to add security policy declarations. These are defined by the (http://schemas.xmlsoap.org/ws/2005/07/securitypolicy sp) schema, and there are a large number of variations, as defined in the specification linked above.
Basically, for our tutorial, we want to require that the body and custom headers of our messages are signed with a X.509 certificate (for source authentication), and that the body of our messages is encrypted with an X.509 certificate (for message privacy).
A policy to encrypt an input or output message is pretty simple, and looks basically like this:
WS-SecurityPolicy Input/Output Declaration
<wsp:Policy wsu:Id="InputOutputUniqueIdentifier">
<wsp:ExactlyOne>
<wsp:All>
<sp:EncryptedParts>
<sp:Body />
</sp:EncryptedParts>
<sp:SignedParts>
<sp:Body />
<sp:Header Namespace="http://example.com/tutotial/"/>
</sp:SignedParts>
</wsp:All>
</wsp:ExactlyOne>
</wsp:Policy>
This says any operation whose input or output is linked to InputOutputUniqueIdentifier must have an encrypted body and must have a signed body and headers (the signed headers are all in the given namespace).
In theory we could require that the headers also be encrypted, but there is a CXF bug which prevents this from working (CXF-3452; also see related CXF-3453).
We then need to declare, for the entire service binding, how the input/output binding will take place (what kinds of tokens, how the tokens are exchanged, etc.). The options here are complex, and aside from the rather opaque specification, there's not much explanatory documentation available.
WS-SecurityPolicy Binding Policy Declaration
<wsp:Policy wsu:Id="UniqueBindingPolicyIdentifier">
<wsp:ExactlyOne>
<wsp:All>
<sp:AsymmetricBinding>
<wsp:Policy>
<sp:InitiatorToken>
<wsp:Policy>
<sp:X509Token sp:IncludeToken="http://schemas.xmlsoap.org/ws/2005/07/securitypolicy/IncludeToken/AlwaysToRecipient">
<wsp:Policy>
<sp:WssX509V3Token11 />
</wsp:Policy>
</sp:X509Token>
</wsp:Policy>
</sp:InitiatorToken>
<sp:RecipientToken>
<wsp:Policy>
<sp:X509Token sp:IncludeToken="http://schemas.xmlsoap.org/ws/2005/07/securitypolicy/IncludeToken/Never">
<wsp:Policy>
<sp:WssX509V3Token11 />
<sp:RequireIssuerSerialReference />
</wsp:Policy>
</sp:X509Token>
</wsp:Policy>
</sp:RecipientToken>
<sp:Layout>
<wsp:Policy>
<sp:Strict />
</wsp:Policy>
</sp:Layout>
<sp:IncludeTimestamp />
<sp:OnlySignEntireHeadersAndBody />
<sp:AlgorithmSuite>
<wsp:Policy>
<sp:Basic128 />
</wsp:Policy>
</sp:AlgorithmSuite>
<sp:EncryptSignature />
</wsp:Policy>
</sp:AsymmetricBinding>
<sp:Wss11>
<wsp:Policy>
<sp:MustSupportRefIssuerSerial />
</wsp:Policy>
</sp:Wss11>
</wsp:All>
</wsp:ExactlyOne>
</wsp:Policy>
This says an AsymmetricBinding will be used (asymmetric or public/private keys rather than symmetric encryption); the initiator must always include an X.509 token; the return message will also be signed/encrypted with an X.509 certificate, but the token itself will not be included and instead an issuer serial # reference will be included. Additionally, strict header layout is used; a timestamp is included and messages will be rejected if the timestamp is too far out-of-date (to avoid replay attacks); only complete headers and bodies must be signed rather than child elements of either; the "Basic128" algorithm suite is used; the signature itself must be encrypted; and the caller must support issuer serial references.
If we wanted to include a further layer of security for message transport, or wanted to use transport encryption instead of message-level encryption, we could add something like:
HTTPS Transport Policy Declaration
<sp:TransportToken>
<wsp:Policy>
<sp:HttpsToken />
</wsp:Policy>
</sp:TransportToken>
So to implement these assertions, you should do the following:
Add to the attributes of your wsdl:definitions element:
- xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
- xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"
- xmlns:sp="http://schemas.xmlsoap.org/ws/2005/07/securitypolicy"
I also added, for editor convenience:
- xsi:schemaLocation="http://schemas.xmlsoap.org/ws/2005/07/securitypolicy/ws-securitypolicy.xsd"
Add the complete set of declarations to your WSDL (I added them as the last elements in the WSDL):
Complete Tutorial Binding Assertion
<wsp:Policy wsu:Id="TutorialBindingPolicy">
<wsp:ExactlyOne>
<wsp:All>
<sp:AsymmetricBinding>
<wsp:Policy>
<sp:InitiatorToken>
<wsp:Policy>
<sp:X509Token sp:IncludeToken="http://schemas.xmlsoap.org/ws/2005/07/securitypolicy/IncludeToken/AlwaysToRecipient">
<wsp:Policy>
<sp:WssX509V3Token11 />
</wsp:Policy>
</sp:X509Token>
</wsp:Policy>
</sp:InitiatorToken>
<sp:RecipientToken>
<wsp:Policy>
<sp:X509Token sp:IncludeToken="http://schemas.xmlsoap.org/ws/2005/07/securitypolicy/IncludeToken/Never">
<wsp:Policy>
<sp:WssX509V3Token11 />
<sp:RequireIssuerSerialReference />
</wsp:Policy>
</sp:X509Token>
</wsp:Policy>
</sp:RecipientToken>
<sp:Layout>
<wsp:Policy>
<sp:Strict />
</wsp:Policy>
</sp:Layout>
<sp:IncludeTimestamp />
<sp:OnlySignEntireHeadersAndBody />
<sp:AlgorithmSuite>
<wsp:Policy>
<sp:Basic128 />
</wsp:Policy>
</sp:AlgorithmSuite>
<sp:EncryptSignature />
</wsp:Policy>
</sp:AsymmetricBinding>
<sp:Wss11>
<wsp:Policy>
<sp:MustSupportRefIssuerSerial />
</wsp:Policy>
</sp:Wss11>
</wsp:All>
</wsp:ExactlyOne>
</wsp:Policy>
<wsp:Policy wsu:Id="TutorialInputBindingPolicy">
<wsp:ExactlyOne>
<wsp:All>
<sp:EncryptedParts>
<sp:Body />
</sp:EncryptedParts>
<sp:SignedParts>
<sp:Body />
<sp:Header Namespace="http://example.com/tutotial/"/>
</sp:SignedParts>
</wsp:All>
</wsp:ExactlyOne>
</wsp:Policy>
<wsp:Policy wsu:Id="TutorialOutputBindingPolicy">
<wsp:ExactlyOne>
<wsp:All>
<sp:EncryptedParts>
<sp:Body />
</sp:EncryptedParts>
<sp:SignedParts>
<sp:Body />
<sp:Header Namespace="http://example.com/tutotial/"/>
</sp:SignedParts>
</wsp:All>
</wsp:ExactlyOne>
</wsp:Policy>
You then must "reference" the policy declarations where you want them used. To each wsdl:binding element where the binding policy should apply, add:
Binding Policy Reference
<wsp:PolicyReference URI="#TutorialBindingPolicy" />
For each input element where the policy should apply, add:
Input Policy Reference
<wsp:PolicyReference URI="#TutorialInputBindingPolicy"/>
For each output element where the policy should apply, add:
Output Policy Reference
<wsp:PolicyReference URI="#TutorialOutputBindingPolicy"/>
So, for instance, the tutorial's code:
Complete Tutorial Binding
<wsdl:binding name="TutorialWebServiceSOAP" type="tns:TutorialWebService">
<wsp:PolicyReference URI="#TutorialBindingPolicy" />
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http" />
<wsdl:operation name="sendTutorialMessage">
<soap:operation soapAction="http://example.com/tutotial/sendTutorialMessage" />
<wsdl:input>
<wsp:PolicyReference URI="#TutorialInputBindingPolicy"/>
<soap:body use="literal" parts="parameters" />
<soap:header use="literal" part="source" message="tns:TutorialRequest"/>
</wsdl:input>
<wsdl:output>
<wsp:PolicyReference URI="#TutorialOutputBindingPolicy"/>
<soap:body use="literal" parts="response"/>
<soap:header use="literal" part="acknowledgment" message="tns:TutorialResponse"/>
</wsdl:output>
<soap:address location="http://localhost/" />
</wsdl:port>
</wsdl:service>
<wsp:Policy wsu:Id="UniqueIdentifier"> <wsp:ExactlyOne> ... </wsp:ExactlyOne> </wsp:Policy>
So to implement these assertions, you should do the following:
No comments:
Post a Comment