Thursday, April 21, 2011

SAML support in CXF 2.4.0

The recent Apache CXF 2.4.0 release contains support for creating, securing, processing and validating SAML Assertions according to the WS-Security 1.1 SAML Token Profile. As there is no documentation available as yet on this new feature, in this blog post I will go through a SAML system test in CXF 2.4.0 in detail.

1) Running the Test

To run the SAML system test you can do the following:

svn co https://svn.apache.org/repos/asf/cxf/tags/cxf-2.4.0/systests/ws-security
cd ws-security
mvn compile
mvn test -Dtest=SamlTokenTest

2) The Client

2.1) The Client code

You can view the source of the tests here. There are a number of tests involving creating SAML 1.1 and 2.0 assertions, and sending them to a service provider over various security bindings (Transport/Symmetric/Asymmetric). To simplify things, we will focus on the fourth test named "testSaml2OverAsymmetric". Minus some negative tests, the basic test client invocation code is as simple as:

SpringBusFactory bf = new SpringBusFactory();
URL busFile = SamlTokenTest.class.getResource("client/client.xml");
Bus bus = bf.createBus(busFile.toString());
SpringBusFactory.setDefaultBus(bus);
SpringBusFactory.setThreadDefaultBus(bus);

DoubleItService service = new DoubleItService();
DoubleItPortType saml2Port = service.getDoubleItSaml2AsymmetricPort();
((BindingProvider)saml2Port).getRequestContext().put(
"ws-security.saml-callback-handler", new SamlCallbackHandler()
);
BigInteger result = saml2Port.doubleIt(BigInteger.valueOf(25));
assert result.equals(BigInteger.valueOf(50));

2.2) The WSDL

The service is described in the WSDL here. Take a look at the WS-SecurityPolicy called "DoubleItSaml2AsymmetricPolicy", which defines the security requirements for the "DoubleItSaml2AsymmetricPort". It defines an Asymmetric Binding, where the InitiatorToken (which defines the credential used to sign the request) is always sent to the recipient, and the RecipientToken (which defines the credential used to encrypt the request) is never sent to the recipient. Both Initiator and Recipient tokens are defined as X509 tokens. The input and output policies in the WSDL enforce that the SOAP Body must be signed using the Initiator credential, and encrypted using the Recipient credential.

In addition to specifying an asymmetric binding, the policy also defines a SignedSupportingToken, which contains a SAML (2.0) Token which is always sent to the recipient. In order to successfully invoke on the service, the client must include a SAML 2.0 token in the security header of the request. This policy looks like:

<sp:SignedSupportingTokens>
    <wsp:Policy>
        <sp:SamlToken sp:IncludeToken="...AlwaysToRecipient">
            <wsp:Policy>
                <sp:WssSamlV20Token11/>
            </wsp:Policy>
        </sp:SamlToken>
    </wsp:Policy>
</sp:SignedSupportingTokens>

2.3) The Client configuration

The client.xml referenced in the code block above contains a jaxws:client configuration for the DoubleItSaml2AsymmetricPort. It sets the following relevant jaxws:properties:
  1. ws-security.encryption.properties - The Crypto properties file which describes where to find the service provider's public key.
  2. ws-security.encryption.username -  The alias to use to obtain the service provider's public key from the keystore reference in the Crypto properties file above.
  3. ws-security.callback-handler - A CallbackHandler object which is expected to supply the password used to access the private key for signature creation, or decryption.
  4. ws-security.signature.properties - The Crypto properties file which describes where to find the client's public/private key.
  5. ws-security.signature.username - The alias to use to obtain the client's private key from the keystore reference in the Crypto properties file above.
2.3) Creating a SAML token

CXF 2.4.0 defines a new jaxws:property ("ws-security-saml-callback-handler") which specifies a CallbackHandler instance used to create SAML Assertions. This object is added to the outbound request context above dynamically, however it could also have been configured in the spring bean along with the other ws-security parameters. The CallbackHandler object used in this test can be seen here. The CallbackHandler implementation is expected to obtain a SAMLCallback object, and to set the appropriate values on this object, e.g. SAML version, Subject, issuer, Authentication/Authorization/Attribute Statements, etc. In the example provided in this test, it creates a SAML 2.0 assertion (by default), sets a mock issuer, subject and attribute statement, and sets a subject confirmation method of sender-vouches. Some code in WSS4J then constructs a SAML Assertion by processing this SAMLCallback object. It's easy to construct a SAML Assertion in this way, as the following (edited) code shows:

SAMLCallback callback = (SAMLCallback) callbacks[i];
callback.setSamlVersion(SAMLVersion.VERSION_20);
callback.setIssuer("sts");
String subjectName = "uid=sts-client,o=mock-sts.com";
String subjectQualifier = "www.mock-sts.com";

SubjectBean subjectBean = new SubjectBean(subjectName, subjectQualifier, SAML2Constants.CONF_SENDER_VOUCHES);
callback.setSubject(subjectBean);

AttributeStatementBean attrBean = new AttributeStatementBean();
attrBean.setSubject(subjectBean);
AttributeBean attributeBean = new AttributeBean();
attributeBean.setSimpleName("subject-role");
attributeBean.setAttributeValues(Collections.singletonList("system-user"));
attrBean.setSamlAttributes(Collections.singletonList(attributeBean));
callback.setAttributeStatementData(Collections.singletonList(attrBean));

2.4) The service request

The service request has a security header that contains the following elements:
  1. A BinarySecurityToken which consists of the X509Certificate of the client.
  2. A Timestamp.
  3. An EncryptedKey which consists of a symmetric key encrypted with the public key of the service provider, which is used to encrypt the SOAP Body.
  4. A SAML2 Assertion.
  5. A SecurityTokenReference to the SAML Assertion.
  6. A signature which signs the Timestamp, the SAML Assertion (via the SecurityTokenReference) and the (decrypted) SOAP body. The signing credential is the BinarySecurityToken element described above.
The SAML 2.0 assertion looks like (edited):

<saml2:Assertion ... Version="2.0">
    <saml2:Issuer>sts</saml2:Issuer>
    <saml2:Subject>
      <saml2:NameID ...>uid=sts-client,o=mocksts.com</saml2:NameID>
     <saml2:SubjectConfirmation Method="...:sender-vouches">
     </saml2:SubjectConfirmation>
   </saml2:Subject>
   <saml2:Conditions NotBefore="..." NotOnOrAfter="..."/>
   <saml2:AttributeStatement>
     <saml2:Attribute FriendlyName="subject-role" ...>
       <saml2:AttributeValue...>system-user</saml2:AttributeValue>
     </saml2:Attribute>
   </saml2:AttributeStatement>
</saml2:Assertion>

One thing to note is that as the SAML Assertion has a subject confirmation method of "sender-vouches", the client will automatically add the quality-of-service requirement that the signature which covers the SOAP Body will also cover the SAML Assertion.

3) The Server

3.1) The Server code

The SEI implementation is here, and the Server code itself is here.  The configuration is entirely driven through the WSDL and spring configuration, and so the code is as trivial as (edited):

URL busFile = Server.class.getResource("server.xml");
Bus busLocal = new SpringBusFactory().createBus(busFile);
BusFactory.setDefaultBus(busLocal);
setBus(busLocal);
new Server();

3.2) The Server configuration

The server.xml configuration file referenced above can be seen here. The jaxws:Endpoint configuration for this port should be self-explanatory (edited):

<jaxws:endpoint
       id="Saml2TokenOverAsymmetric"
       address="http://localhost:9001/DoubleItSaml2Asymmetric"
       serviceName="s:DoubleItService"
       endpointName="s:DoubleItSaml2AsymmetricPort"
       xmlns:s="http://WSSec/saml"
       implementor="org.apache.cxf.systest.ws.saml.server.DoubleItImpl"
       wsdlLocation="wsdl_systest_wssec/saml/DoubleItSaml.wsdl">
        
       <jaxws:properties>
           <entry key="ws-security.username" value="bob"/>
           <entry key="ws-security.callback-handler"
                  value="....KeystorePasswordCallback"/>
           <entry key="ws-security.signature.properties"
                  value="...bob.properties"/>
           <entry key="ws-security.encryption.properties"
                  value="...alice.properties"/>
           <entry key="ws-security.encryption.username" value="alice"/>
       </jaxws:properties>
 </jaxws:endpoint>

The server will process the request as per the security policy in the WSDL, checking that there is a signature in the security header, that covers the SOAP Body and SAML Assertion, that the SOAP Body is Encrypted, that a Timestamp is present and valid, and that the SAML Assertion is present, and is the correct version, etc. Authentication is done on the basis of trust verification of the client's X509Certificate, which was used to verify the signature element.

The SAML Assertion is ignored beyond this point for this system test. It is saved in the security processing results, so that a custom interceptor can do some additional validation or processing on it. In a future blog post, I will describe how to validate the Assertion that has been received in some custom manner.

14 comments:

  1. Hi Colm,

    I was trying the same approach, I ran in to issue I see that I am observing the following following stack trace :
    Caused by: org.apache.ws.security.WSSecurityException: WSHandler: application provided null or empty password
    at org.apache.ws.security.handler.WSHandler.getPasswordCB(WSHandler.java:935)
    at org.apache.ws.security.action.SAMLTokenSignedAction.execute(SAMLTokenSignedAction.java:70)
    at org.apache.ws.security.handler.WSHandler.doSenderAction(WSHandler.java:202)
    at org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor.access$200(WSS4JOutInterceptor.java:52)
    at org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor$WSS4JOutInterceptorInternal.handleMessage(WSS4JOutInterceptor.java:260)
    ... 30 more

    Perhaps I am missing to configure the password in one of the properties files.I was wondering if you could provide a working maven project so that we can following the same and understand the implementation much further.Please let me know if you have concerns.

    ReplyDelete
  2. Hi,

    The post above links to a system test that can be run with maven, i.e.:

    svn co https://svn.apache.org/repos/asf/cxf/tags/cxf-2.4.0/systests/ws-security
    cd ws-security
    mvn test -Dtest=SamlTokenTest

    If you are looking for a sample you could download the Talend Service Factory plus examples, see:

    http://coheigea.blogspot.com/2011/04/talend-service-factory-240-released.html

    Colm.

    ReplyDelete
  3. Hi Colm,

    I was finally able to get the source code from
    svn co https://svn.apache.org/repos/asf/cxf/tags/cxf-2.4.0/systests/ws-security

    I have got maven set up too and it was able get the workspace configured but for some reason I see there are missing classes :
    org.apache.cxf.policytest.doubleit.DoubleIt;
    org.apache.cxf.policytest.doubleit.DoubleItFault_Exception;
    org.apache.cxf.policytest.doubleit.DoubleItPortType;
    org.apache.cxf.policytest.doubleit.DoubleItPortTypeHeader;
    org.apache.cxf.policytest.doubleit.DoubleItResponse;
    org.apache.cxf.policytest.doubleit.DoubleItService;

    ReplyDelete
  4. Try doing a "mvn compile" before running the test.

    Colm.

    ReplyDelete
  5. Hi Colm,

    I was able to get the workspace up and running.in the org.apache.cxf.systest.ws.saml.SamlTokenTest.testSaml1OverTransport() method I see that you are explicitly attaching
    ((BindingProvider)saml1Port).getRequestContext().put(
    "ws-security.saml-callback-handler", new SamlCallbackHandler()
    );
    The callback handler and associated properties file.I was wondering if you could do it more elegantly with a

    jaxws:outInterceptors and I chose to use the wssjinterceptor and here is the config code
    <---------------------------------------->
















    <----------------------------------------->

    I am able to invoke the my custom callbackhandler but I am observing the following exception and finally get a soap fault.

    Here is the error :
    Caused by: org.apache.ws.security.WSSecurityException: WSHandler: application provided null or empty password
    at org.apache.ws.security.handler.WSHandler.getPasswordCB(WSHandler.java:935)
    at org.apache.ws.security.action.SAMLTokenSignedAction.execute(SAMLTokenSignedAction.java:70)
    at org.apache.ws.security.handler.WSHandler.doSenderAction(WSHandler.java:202)
    at org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor.access$200(WSS4JOutInterceptor.java:52)
    at org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor$WSS4JOutInterceptorInternal.handleMessage(WSS4JOutInterceptor.java:260)

    also is there a personal email address and its hard to paste config info in this box as it is skipping the elements in the post.

    ReplyDelete
  6. Hi Colm,

    Never mind I worked on it over the weekend and all it needed was some code debugging and patience my implementation with saml is working pefectly fine with cxf 2.40 and wss4j 1.6.0 Thanks all the help.

    ReplyDelete
  7. Hi Colm,

    I am new to CXF and SAML, in the client section of your article, you mentioned the client.xml file, I have a question on where the client.xml file should be placed, and what mechanism the client application uses to retrieve the configuarations set in the file.

    Thanks,

    Raymond

    ReplyDelete
  8. Hi Raymond,

    See this wiki page for more information:

    http://cxf.apache.org/docs/configuration.html

    Colm.

    ReplyDelete
  9. Hi Colm,

    Thanks for the link.
    I am currently running into another problem creating a web service client app based on your sample.
    I have created java artifacts for the web service using wsimport tool of Java 6, wrote a little code to create a client class to comsume the web service, all is fine and tested, and this is done in Eclipse. Then I downloaded and installed CXF 2.4.1 to add SAML support to the code as shown in your sample. I added cxf-manifest.jar to the Java Build Path of the Eclipse project. When I ran I got an exception error as follows

    Jul 19, 2011 10:46:43 AM org.apache.cxf.service.factory.ReflectionServiceFactoryBean buildServiceFromWSDL
    INFO: Creating Service {http://ch33.va.gov/contract/Claimant}ClaimantService from WSDL: file:/C:/LTS/ClaimantWS/Claimant.wsdl
    Exception in thread "main" javax.xml.ws.soap.SOAPFaultException: Reference to policy #Claimant_PortBindingPolicy could not be resolved.
    at org.apache.cxf.jaxws.JaxWsClientProxy.invoke(JaxWsClientProxy.java:146)
    at $Proxy26.getClaimant(Unknown Source)
    at wsGetClaimantClient.GetClaimantClient.main(GetClaimantClient.java:38)
    Caused by: org.apache.cxf.ws.policy.PolicyException: Reference to policy #Claimant_PortBindingPolicy could not be resolved.
    at org.apache.cxf.ws.policy.attachment.AbstractPolicyProvider.checkResolved(AbstractPolicyProvider.java:93)
    at org.apache.cxf.ws.policy.attachment.wsdl11.Wsdl11AttachmentPolicyProvider.resolveReference(Wsdl11AttachmentPolicyProvider.java:266)
    at org.apache.cxf.ws.policy.attachment.wsdl11.Wsdl11AttachmentPolicyProvider.getElementPolicy(Wsdl11AttachmentPolicyProvider.java:216)
    at org.apache.cxf.ws.policy.attachment.wsdl11.Wsdl11AttachmentPolicyProvider.getElementPolicy(Wsdl11AttachmentPolicyProvider.java:170)
    at org.apache.cxf.ws.policy.attachment.wsdl11.Wsdl11AttachmentPolicyProvider.getElementPolicy(Wsdl11AttachmentPolicyProvider.java:163)
    at org.apache.cxf.ws.policy.attachment.wsdl11.Wsdl11AttachmentPolicyProvider.getEffectivePolicy(Wsdl11AttachmentPolicyProvider.java:100)
    at org.apache.cxf.ws.policy.PolicyEngineImpl.getAggregatedEndpointPolicy(PolicyEngineImpl.java:401)
    at org.apache.cxf.ws.policy.EndpointPolicyImpl.initializePolicy(EndpointPolicyImpl.java:150)
    at org.apache.cxf.ws.policy.EndpointPolicyImpl.initialize(EndpointPolicyImpl.java:139)
    at org.apache.cxf.ws.policy.PolicyEngineImpl.createEndpointPolicyInfo(PolicyEngineImpl.java:533)
    at org.apache.cxf.ws.policy.PolicyEngineImpl.getEndpointPolicy(PolicyEngineImpl.java:285)
    at org.apache.cxf.ws.policy.PolicyEngineImpl.getClientEndpointPolicy(PolicyEngineImpl.java:267)
    at org.apache.cxf.transport.http.policy.PolicyUtils.getClient(PolicyUtils.java:150)
    at org.apache.cxf.transport.http.HTTPConduit.(HTTPConduit.java:300)
    at org.apache.cxf.transport.http.HTTPTransportFactory.getConduit(HTTPTransportFactory.java:246)
    at org.apache.cxf.transport.http.HTTPTransportFactory.getConduit(HTTPTransportFactory.java:233)
    at org.apache.cxf.binding.soap.SoapTransportFactory.getConduit(SoapTransportFactory.java:228)
    at org.apache.cxf.endpoint.AbstractConduitSelector.getSelectedConduit(AbstractConduitSelector.java:81)
    at org.apache.cxf.endpoint.UpfrontConduitSelector.prepare(UpfrontConduitSelector.java:61)
    at org.apache.cxf.endpoint.ClientImpl.prepareConduitSelector(ClientImpl.java:809)
    at org.apache.cxf.endpoint.ClientImpl.doInvoke(ClientImpl.java:505)
    at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:440)
    at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:343)
    at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:295)
    at org.apache.cxf.frontend.ClientProxy.invokeSync(ClientProxy.java:73)
    at org.apache.cxf.jaxws.JaxWsClientProxy.invoke(JaxWsClientProxy.java:124)
    ... 2 more


    Is there compatibility issue here ?

    ReplyDelete
  10. Hi Raymond,

    It's impossible to say without seeing the WSDL. Are you sure the policy "Claimant_PortBindingPolicy" is available in the WSDL? If so, could you create a JIRA in CXF here:

    https://issues.apache.org/jira/browse/CXF

    and attach a sample WSDL that shows the problem?

    Colm.

    ReplyDelete
  11. Hi Colm,

    May be I should rephrase my question, I have CXF 2.4.1 downloaded and installed, this time I was using wsdl2java tool from CXF to create the stubs, and I was getting a different error, how should I configure Eclipse to run your sample(the client)
    Thanks

    Ray

    ReplyDelete
  12. Hi Raymond,

    What error are you getting from wsdl2java? Could you post to the CXF users list instead of this blog, and give a sample WSDL that shows the problem?

    Colm.

    ReplyDelete
  13. This comment has been removed by the author.

    ReplyDelete
  14. This comment has been removed by the author.

    ReplyDelete