Glen Mazza's Weblog

« Activating Transport... | Main | Using X.509 security... »

https://web-gmazza.rhcloud.com/blog/date/20170402 Sunday April 02, 2017

Using UsernameToken security with Apache CXF

This tutorial modifies the CXF version of the WSDL-first DoubleIt web service to include WS-Security with UsernameTokens. This profile should be used with transport-layer encryption (i.e., SSL) as a minimum to ensure the username and password are encrypted at least between the client and the first recipient node. Note the X.509 or SAML token approach would be more appropriate instead if any of cases below apply:

  • Clients need to sign the request to guard against it being altered in transit
  • There are intermediary nodes between client and service that would be unencrypting the message (and hence able to read its contents) before forwarding it on to the next node
  • The web service provider has incomplete nonce and timestamp support necessary to guard against replay attacks. CXF has implemented such caching since version 2.6.

CXF provides two main options for adding UsernameToken security headers, both of which will be covered below: WS-SecurityPolicy and manual CXF interceptors. The former relies on the WSDL already having WS-SecurityPolicy elements defined within it to obtain the security requirements. Use the manual CXF interceptor approach when security is not defined in the WSDL or more customized control of the security header construction is desired.

The finished tutorial source code can be obtained from GitHub by using either the download ZIP button or git clone -v git://github.com/gmazza/blog-samples.git command. The download is configured to use WS-SecurityPolicy, if desired make the adjustments specified below to switch to the CXF interceptor approach.

  1. Implement SSL without basic authentication for the web service. Follow the SSL tutorial for this, except with the following changes:

    • Change Step #4 to just require SSL without basic auth by making the below web.xml modification.
      <security-constraint>
          <web-resource-collection>
              <web-resource-name>restricted web services</web-resource-name>
              <url-pattern>/*</url-pattern>
              <http-method>GET</http-method>
              <http-method>POST</http-method>
          </web-resource-collection>
          <user-data-constraint>
              <transport-guarantee>CONFIDENTIAL</transport-guarantee>
          </user-data-constraint>
      </security-constraint>
      
    • Skip Step #5 if you plan on validating users using a service-side callback handler instead of users defined within your servlet or JEE container.
    • Skip Step #10 because we're no longer providing basic auth information (the basic DoubleIt Client class can be used as-is.)

    Make sure the SOAP client works against the SSL-hosted web service before continuing.

  2. Include additional Maven dependencies to handle security processing. The following dependency will need to be added to the project-level pom.xml to activate security processing.

    <dependency>
       <groupId>org.apache.cxf</groupId>
       <artifactId>cxf-rt-ws-security</artifactId>
       <version>${cxf.version}</version>
    </dependency>
    

    Also, since we'll be adding a Spring cxf.xml configuration file to the client to declare the security parameters, we'll need to add a Spring dependency to the client/pom.xml. (See the notes at the bottom of this tutorial for a non-Spring way of doing client configuration that avoids this dependency.)

    <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-context</artifactId>
       <version>${spring.version}</version>
    </dependency>
    
  3. Configure the client to provide the user and password. We'll be introducing a Spring-based cxf.xml configuration file, presently configured for the WS-SecurityPolicy approach but can be switched over to the CXF interceptor approach by following the comments given below in this source file.

    Next supply the password CallbackHandler referenced in the SOAP client. Place this class in the same package as the SOAP client:

    package client;
    
    import java.io.IOException;
    import javax.security.auth.callback.Callback;
    import javax.security.auth.callback.CallbackHandler;
    import javax.security.auth.callback.UnsupportedCallbackException;
    import org.apache.wss4j.common.ext.WSPasswordCallback;
    
    public class ClientPasswordCallback implements CallbackHandler {
    
        public void handle(Callback[] callbacks) throws IOException, 
                UnsupportedCallbackException {
            WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];
    
            if ("joe".equals(pc.getIdentifier())) {
                pc.setPassword("joespassword");
            } // else {...} - can add more users, access DB, etc.
        }
    }
    
  4. Add the WS-SecurityPolicy statements to the WSDL. The Service WSDL in the source code of this tutorial already has WS-Policy statements added to it, primarily using the WSDL policy generation tools available in NetBeans. For production use, you'll probably want to modify the algorithm suite given in the policy statements to something appropriate for your company's security requirements. For this sample, the WSDL policy statements declare Basic256 which will require the unlimited strength encryption JAR for your JDK, alternatively for tutorial purposes you can switch to the weaker Basic128 that does not have this requirement. CXF interceptor method only: We'll need to use a Policy-free WSDL, so remove this DoubleIt.wsdl and rename the Policy-free DoubleItInterceptorMethod.txt found in the service submodule resources folder to DoubleIt.wsdl (it's the same WSDL from the original web service tutorial.)

    NetBean's WSDL tools can generate WS-SecurityPolicy elements. If you'd like to try NetBeans for this purpose, you can create a temporary project in that IDE just to add those elements to the DoubleIt.wsdl file and then place the modified WSDL back into our Maven project. For the below, I'm using Netbeans 8.2 Java EE version, the latest release version as of this writing. Steps:

    1. From the NetBeans Menu Bar, Choose File | New Project. From the Java Web category, choose Web Application and "Apache Tomcat or TomEE" with Java EE 7 Web for the Java EE version. On the frameworks tab, none needed, accept the rest of the defaults.

    2. From the NetBeans Projects view, right click the new project you created and select New | Web Service From WSDL. Browse for the DoubleIt.wsdl file from the web service tutorial's service submodule (service/src/main/resources) and select it. Enter any package name--we won't be using the generated artifacts so what you place here won't matter. Click Finish, and "Yes" to the subsequent question of whether you want to use the sun-jaxws.xml configuration file in lieu of JSR-109 deployment. Note in creating the web service, NetBeans will make its own copy of the WSDL that can be seen in the Project View under Configuration Files/xml-resources/Web Services.

    3. From the Projects View, right-click the Web Application -> Web Services -> DoubleItService entry and select "Edit Web Service Attributes". From the pop-up that appears (see picture below), select the Quality of Service tab. From here:

      • For Version Compatibility, choose the latest version offered, this information is used to determine the version of the WS-Policy namespaces that will be generated.
      • Check the "Secure Service" checkbox and choose "Message Authentication over SSL" as the Security Mechanism.
      • Select the Configure button to the right of the Security Mechanism field and choose Username Token and WSS Version 1.1. Choose your preferred algorithm suite and security header layout. Hit "OK" and the project's copy of the WSDL will be altered with the Policy statements giving the new security requirements.
      Activating UsernameToken profile using NetBeans
    4. From the Projects View, go to Web Application -> Configuration Files -> xml-resources, and drill down to the DoubleIt.wsdl file. Make sure it has the wsp:Policy element at the bottom. Save this DoubleIt.wsdl file back to the Mavenized project, replacing the original DoubleIt.wsdl file in service/src/main/resources.

      The resulting modified WSDL is shown below. NetBeans added a Policy element at the end, as well a policy reference to that element within the wsdl:binding section. (I made one small change to it to accommodate a bug in a library used by CXF, namely giving the sp:HttpsToken element an empty wsp:Policy element.) The policy element contains a UsernameToken Assertion section indicating that this profile is to be used for all SOAP requests. Important: Check the soap:address location to make sure it is correct for your situation, it may get adjusted by NetBeans, for this example, running on localhost Tomcat, I'm using https://localhost:8443/doubleit/services/doubleit.

      This modified WSDL will be used both by the service provider and the SOAP client in our project--the service because this WSDL is the one specified for it, while the client reads it by default because it is hardcoded in by the DoubleItService.java generated JAX-WS class. As a result, the policy information in the WSDL will activate security behavior on both sides--the service will require username tokens for SOAP requests and the client will supply those tokens when making SOAP calls.

  5. Configure the service provider to authenticate the username and password tokens. Here we'll need to modify the cxf-servlet.xml configuration file from Step #6 of the WSDL-first tutorial to add either the CXF inbound interceptor or the WS-SecurityPolicy configuration information (not both). Replace the previous cxf-servlet.xml file with the following, after modifying it based on your security implementation preference:

    Once done we'll need to add the new server-side password callback class referenced above, which provides the correct password for a user, which the CXF framework uses to compare with what is supplied in the client request. Place the following ServerPasswordCallback class to the same package as the service's DoubleItPortTypeImpl SEI implementation class:

    package service;
    
    import java.io.IOException;
    
    import javax.security.auth.callback.Callback;
    import javax.security.auth.callback.CallbackHandler;
    import javax.security.auth.callback.UnsupportedCallbackException;
    import org.apache.wss4j.common.ext.WSPasswordCallback;
    
    public class ServerPasswordCallback implements CallbackHandler {
    
        public void handle(Callback[] callbacks) throws IOException,
                UnsupportedCallbackException {
            WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];
    
            if ("joe".equals(pc.getIdentifier())) {
               pc.setPassword("joespassword"); 
            }
        }
    }
    

    You can now compile and redeploy the web service from the DoubleIt base folder.

  6. Test the client. Run the client as shown in the DoubleIt tutorial. Best to test also with incorrect usernames and passwords to ensure the web service provider is catching these errors.

    You can view the headers being added to the SOAP request if you temporarily switch your web service back to non-SSL usage. This can be done by updating the web.xml and URL within the WSDL, and additionally if you're using WS-SecurityPolicy, removing any WSDL element declaring a requirement for SSL. Once done you can either use Wireshark or activate logging within your WSClient class to see the unencrypted SOAP messages. Running the client from a terminal window will show results similar to the following:

    Oct 1, 20xx 7:47:34 PM
    org.apache.cxf.interceptor.AbstractLoggingInterceptor log
    INFO: Outbound
    Message
    ---------------------------
    ID: 1
    Address:
    http://localhost:8080/doubleit/services/doubleit
    Encoding: UTF-8
    Content-Type: text/xml
    Headers: {Accept=[*/*], SOAPAction=[""]}
    Payload:
    <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
        <soap:Header>
            <wsse:Security
                xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
                xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
                soap:mustUnderstand="1">
                <wsse:UsernameToken wsu:Id="UsernameToken-1">
                    <wsse:Username>joe</wsse:Username>
                    <wsse:Password
                        Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">joespassword</wsse:Password>
                </wsse:UsernameToken>
            </wsse:Security>
        </soap:Header>
        <soap:Body>
            <ns2:DoubleIt xmlns:ns2="http://www.example.org/schema/DoubleIt">
                <numberToDouble>10</numberToDouble>
            </ns2:DoubleIt>
        </soap:Body>
    </soap:Envelope>
    --------------------------------------
    Oct 1, 20xx 7:47:34 PM
    org.apache.cxf.interceptor.AbstractLoggingInterceptor log
    INFO: Inbound
    Message
    ----------------------------
    ID: 1
    Response-Code: 200
    Encoding:
    UTF-8
    Content-Type: text/xml;charset=UTF-8
    Headers: {Content-Length=[238],
    content-type=[text/xml;charset=UTF-8], Date=[Sat, 01 Oct 20xx 23:47:34
    GMT], Server=[Apache-Coyote/1.1]}
    Payload:
    <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
        <soap:Body>
            <ns2:DoubleItResponse xmlns:ns2="http://www.example.org/schema/DoubleIt">
                <doubledNumber>20</doubledNumber>
            </ns2:DoubleItResponse>
        </soap:Body>
    </soap:Envelope>
    --------------------------------------
    The number 10 doubled is 20
    

    If you do this, remember to switch back to SSL for production usage.

Troubleshooting Notes - Error messages that may occur while running the client and probable solutions:

  • org.apache.cxf.ws.policy.attachment.wsdl11.Wsdl11AttachmentPolicyProvider getElementPolicy
    WARNING: Failed to build the policy 'DoubleItBindingPolicy':sp:HttpsToken must have an inner wsp:Policy element
    Exception in thread "main" javax.xml.ws.soap.SOAPFaultException: sp:HttpsToken must have an inner wsp:Policy element
    	at org.apache.cxf.jaxws.JaxWsClientProxy.invoke(JaxWsClientProxy.java:161)
    

    Make sure to modify the sp:HttpsToken element with an empty wsp:Policy element as explained above.

  • 	at client.WSClient.doubleIt(WSClient.java:18)
    	at client.WSClient.main(WSClient.java:12)
    Caused by: javax.net.ssl.SSLException: SSLException invoking https://localhost:8080/doubleit/services/doubleit: Unrecognized SSL message, plaintext connection?
    

    Make sure the soap:address location in the DoubleIt.wsdl is correct (adding "?wsdl" to it should bring up the WSDL in a browser), it will probably be configured to the incorrect one given in the message.

  • SecurityPolicy method:
    WARNING: Interceptor for {http://www.example.org/contract/DoubleIt}DoubleItService#{http://www.example.org/contract/DoubleIt}DoubleIt has thrown exception, unwinding now
    org.apache.cxf.interceptor.Fault: No password available
    
    Interceptor method:
    Caused by: java.lang.IllegalArgumentException: pwd == null but a password is needed
    

    Make sure that the client user in the cxf.xml file has a password defined in ClientPasswordCallback.

  • Exception in thread "main" javax.xml.ws.soap.SOAPFaultException: A security error was encountered when verifying the message
    	at org.apache.cxf.jaxws.JaxWsClientProxy.invoke(JaxWsClientProxy.java:161)
    	at com.sun.proxy.$Proxy33.doubleIt(Unknown Source)
    	at client.WSClient.doubleIt(WSClient.java:18)
    	at client.WSClient.main(WSClient.java:12)
    

    Make sure the client user defined in the cxf.xml file has his correct password defined in the ClientPasswordCallback class (compare it to the one supplied in the server's ServerPasswordCallback)

Notes

  • If desired, you can avoid introducing a Spring dependency in the client by not using a cxf.xml configuration file and instead declaring security using the Java API in the client's WSClient class. A sample alteration demonstrating both the WS-SecurityPolicy and CXF interceptor approaches is as follows:

    ...
    import org.apache.cxf.endpoint.Client;
    import org.apache.cxf.endpoint.Endpoint;
    import org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor;
    import org.apache.ws.security.WSConstants;
    import org.apache.ws.security.handler.WSHandlerConstants;
    ...
    
    public class WSClient {
       
        public static void main(String[] args) {
            DoubleItService service = new DoubleItService();
            DoubleItPortType port = service.getDoubleItPort();
    
            // WS-SecurityPolicy configuration method
    /*
            Map ctx = ((BindingProvider)port).getRequestContext();
            ctx.put("ws-security.username", "joe");
            ctx.put("ws-security.callback-handler", ClientPasswordCallback.class.getName());
            // instead of above line can also do:
            // ctx.put("ws-security.password", "joespassword");
    */
            // Alternative CXF interceptor config method
    /*
            Client client = org.apache.cxf.frontend.ClientProxy.getClient(port);
            Endpoint cxfEndpoint = client.getEndpoint();
            Map outProps = new HashMap();
            outProps.put(WSHandlerConstants.ACTION, WSHandlerConstants.USERNAME_TOKEN);
            outProps.put(WSHandlerConstants.USER, "joe");
            outProps.put(WSHandlerConstants.PASSWORD_TYPE, WSConstants.PW_TEXT);
            outProps.put(WSHandlerConstants.PW_CALLBACK_CLASS,
                ClientPasswordCallback.class.getName());
                
            WSS4JOutInterceptor wssOut = new WSS4JOutInterceptor(outProps);
            cxfEndpoint.getOutInterceptors().add(wssOut);
    */        
            ...
    
  • The CXF project has its own UsernameToken sample you may wish to review.

Comments:

Post a Comment:
  • HTML Syntax: NOT allowed

Valid HTML! Valid CSS!

This is a personal weblog, I do not speak for my employer.