Thursday, 4 March 2010

Setting up WS-Security with Rampart module in Apache Axis web services framework

NOTE: This is from a work document I wrote that I published to Blogger via Google Docs. I'll edit it later, remove Appendices, make it more blog-y and nicer looking, but the info should be useful to someone out there.

Setting up WS-Security with Rampart module in Axis2, for authentication using Password Digest

Instructions

 

These instructions assume that the user is using Axis2 and is packaging their web service into an .aar package

This has been tested using Axis2 1.4.1 - 1.5.1 on JDK 5.

1. Download these:

Apache Axis2 1.4.1 (or latest version)

Apache Rampart 1.4 (or latest version) - security module for Axis2

Bouncycastle Java Cryptography Extensions (JCE) provider for your Java version
http://www.bouncycastle.org/latest_releases.html


2. Install Bouncycastle in JVM, or copy it to Axis webapp's /WEB-INF/lib/

Bouncycastle library is a Rampart dependency, so we need to include the Bouncycastle JCE provider in either the webapp's /WEB-INF/lib or install it in the JVM

2.1 Adding bouncycastle as a security provider in JVM

2.1.1 Add the bcprov-jdk15-***.jar to your service's / client's classpath.
Don't get the file that ends in "-ext.jar" because these ones contain patented, non-free code that is not needed by Rampart.

bcprov-jdk15 is specified by Rampart in its dependency listing but bcprov-jdk16 should also work,  

2.1.2 Add the following line to  file which can be found in JRE's lib/security/java.security directory as the last line.

security.provider.X=org.bouncycastle.jce.provider.BouncyCastleProvider

- "X" is the next unused number available for the listed security provider)

Also see: http://www.bouncycastle.org/documentation.html


3.1 To modify an existing axis2 webapp: 

Copy the *-codegen-*.jar files from Axis installation /lib into your existing webapp's /WEB-INF/lib directory.


3.2 To create a new axis2.war webapp:

You can use the Ant build.xml provided in the Axis2 installation /webapp directory to create axis2.war, provided you make one change: delete the line <exclude name="axis2-codegen*.jar"/> near the end of the file.

Then open a console to the Axis2 webapp directory and run ant.

After the build.xml runs, you can find the created axis2.war Web application in the Axis2 installation /dist directory


4. Copy from Rampart  /lib  to Axis2 webapp /WEB-INF/lib

opensaml-1.1.jar
rampart-core-1.4.jar
rampart-policy-1.4.jar
rampart-trust-1.4.jar
wss4j-1.5.4.jar
xmlsec-1.4.1.jar


5. Rampart needs the library backport-util-concurrent-3.1.jar. It's no longer present in Axis2 1.5 and higher, so for these versions you'll need to get it from:

http://backport-jsr166.sourceforge.net/

or

Axis2 1.4.1  /lib

And copy the jar file to  /WEB-INF/lib of your Axis2 webapp

In case of any further dependency issues with Rampart, see: http://ws.apache.org/rampart/dependencies.html


6. Copy from Rampart /modules to Axis2 webapp /WEB-INF/modules

rahas{version}.mar
rampart-{version}.mar

ie,

rahas-1.4.mar
rampart-1.4.mar

Then add these filenames to /WEB-INF/modules/modules.list , one per line


7. Create a password callback handler that will be used by Rampart to handle authentication. The class must implement the interface javax.security.auth.callback.CallbackHandler.

If multiple services in different code bases or .aar will be using the same authentication functionality, it is recommended that the callback handler be set up in its own jar file then copied to the Axis2 webapp's /WEB-INF/lib.
 

See Appendix for structure of callback handler.



8. Update the services.xml for each web service and add the following xml fragments between the <service> </service> tags.

The following are the WS-Policy to use for authentication using password plaintext and password digest.

 

The <module> and <ramp:RampartConfig> elements must be present when using Rampart, and are only relevant when using the Axis2 framework.


8.1 WS-Policy using PlainText password - username and password will be sent in soap headers, and password sent in plaintext

<module ref="rampart"/>

<wsp:Policy wsu:Id="UsernameToken" 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">
      <wsp:ExactlyOne>
        <wsp:All>
          <sp:SupportingTokens
              xmlns:sp="http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702">
            <wsp:Policy>
              <sp:UsernameToken sp:IncludeToken=
                  "http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702/IncludeToken/AlwaysToRecipient"/>
            </wsp:Policy>
          </sp:SupportingTokens>

          <ramp:RampartConfig xmlns:ramp="http://ws.apache.org/rampart/policy">
            <ramp:passwordCallbackClass>[REPLACE THIS WITH OUR PASSWORD CALLBACK CLASS, ie, security.package.PWCallBackHandler]</ramp:passwordCallbackClass>
          </ramp:RampartConfig>

        </wsp:All>
      </wsp:ExactlyOne>
</wsp:Policy>



8.2  WS-Policy using PasswordDigest

<module ref="rampart"/>

<wsp:Policy wsu:Id="UsernameToken" 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">
  <wsp:ExactlyOne>
    <wsp:All>
      <sp:SupportingTokens
          xmlns:sp="http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702">
        <wsp:Policy>
          <sp:UsernameToken sp:IncludeToken=
              "http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702/IncludeToken/AlwaysToRecipient">

            <wsp:Policy>
              <sp:HashPassword/>
            </wsp:Policy>

          </sp:UsernameToken>

        </wsp:Policy>
      </sp:SupportingTokens>

      <ramp:RampartConfig xmlns:ramp="http://ws.apache.org/rampart/policy">
        <ramp:passwordCallbackClass>[REPLACE THIS WITH OUR PASSWORD CALLBACK CLASS, ie, security.package.PWCallBackHandler]</ramp:passwordCallbackClass>
      </ramp:RampartConfig>

    </wsp:All>
  </wsp:ExactlyOne>
</wsp:Policy>


9. Test that everything is working by configuring plaintext authentication. Plaintext authentication is easier to test with a tool like SoapUI because you can simply cut and paste XML and enter the login details in the header.

 

Testing of password digest authentication will require code of some kind to generate the various elements and calculate the digest before sending the client request.
 

 

Appendix A.

 

About Password Digest authentication


In the case where the communicating parties - the requester and the service - uses an insecure transport channel they should take steps to protect the passwords being exposed to others. Here the requester creates a digest of the actual password concatenated with a set of random bytes (nonce) and another value that is dependent on the creation time (created).


This digest is computed as follows :

digest = Base64_encode(SHA-1(nonce+created+password))

This is the base 64 encoded, SHA-1 digest of the concatenation of the nonce, created and password values.

The requester will send the username, nonce, created, and the digest values in within
the UsernameToken to the service.

 

To authenticate the request the service will compute
the digest value using the password bound to the received usename and will compare the
received digest value and the computed digest value.

 

An example UsernameToken that uses a digest of the password is shown in following figure:

<soapenv:Header>
     <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" soapenv:mustUnderstand="1">
        <wsse:UsernameToken xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="UsernameToken-26752749">
         <wsse:Username>libuser</wsse:Username>
         <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">/Wt/2yDdZwa8a5qd7U70hrp29/w=</wsse:Password>
         <wsse:Nonce>4ZQz5ytME/RXfChuKJ03iA==</wsse:Nonce>
         <wsu:Created>2009-03-17T11:20:57.467Z</wsu:Created>
      </wsse:UsernameToken>
    </wsse:Security>
 </soapenv:Header>

If password in the username token is digested, the password callback handler only needs to provide the password for the client, to allow Rampart to verify it with the password received with the username token.
 

 

Appendix B


Password Callback handler implementation
 

The general structure of the server-side class will be like this:

public class PWCBHandler implements CallbackHandler {

    public void handle(Callback[] callbacks) throws IOException,
            UnsupportedCallbackException {
        for (int i = 0; i < callbacks.length; i++) {
            WSPasswordCallback pwcb = (WSPasswordCallback)callbacks[i];
            String username = pwcb.getIdentifer();
            String password = pwcb.getPassword();

            int usage = pwcb.getUsage();
 

/**

Usage is WSPasswordCallback.USERNAME_TOKEN for a digested password, and WSPasswordCallback.USERNAME_TOKEN_UNKNOWN for a plaintext password or a password of unknown type.

 

*/


            // used when plaintext password in message
            if (usage == WSPasswordCallback.USERNAME_TOKEN_UNKNOWN) {

/**

Verify username and password with existing authentication service
 

eg, authService.authenticate(username, password); 

If authentication fails, you should throw an UnsupportedCallbackException

If password digest is mandatory, then this section should either be
removed or just throw an UnsupportedCallbackException so that requests using plaintext password will fail.

*/
                             
            // when hashed password in message
            } else if (pwcb.getUsage() == WSPasswordCallback.USERNAME_TOKEN) {

    

/**                       
In actual app, this should be replaced by code that will get the password for given username and specify it in pwcb.setPassword()


eg, pwcb.setPassword(authService.getPassword(id));

 

Rampart will take care of calculating the digest and comparing it with the digest sent in the request.

 

This assumes we have a way of retrieving passwords in plaintext.

 

If the auth system only returns password hashes, then client should use also use password hashIn such cases, the client should use same hash method as the auth system, so if the password is only available as an md5 hash, then the client should also generate an md5 hash of the password before calculating the digest.

 

*/


            }
        }
    }
}

Appendix C

 

References:

Web Services Security, UsernameToken Profile 1.0
http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0.pdf
Contains explanation for design of password digest.

Java Web services: Axis2 WS-Security basics
http://www.ibm.com/developerworks/webservices/library/j-jws4/index.html


Password Callback Handlers Explained
http://wso2.org/library/3733


UsernameToken Authentication with Rampart
http://wso2.org/library/240#digest

The sample code for this article is incomplete and does not contain examples for password digest.
 

 

Appendix D

 

Examples of various authentication requests and error responses

 

1. Plaintext – simple XML request

 

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">

    <soapenv:Header>
       <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
                 soapenv:mustUnderstand="1">

        <wsse:UsernameToken xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
                    wsu:Id="UsernameToken-1815911473">
                          <wsse:Username>client</wsse:Username>
                       <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">apache</wsse:Password>
         </wsse:UsernameToken>
       </wsse:Security>
    </soapenv:Header>
 

   <soapenv:Body/>
</soapenv:Envelope>
 


2.  Password Digest - Bad Request with correct format but incorrect values.
 

Request:


<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">

    <soapenv:Header>

         <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" soapenv:mustUnderstand="1">
            <wsse:UsernameToken xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="UsernameToken-26752749">
               <wsse:Username>libuser</wsse:Username>
               <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">wOaGIASh06nYuDs6RejYky0nsec=</wsse:Password>
               <wsse:Nonce>ole1X/60hjUqbUt3P2ak6w==</wsse:Nonce>
               <wsu:Created>2009-10-28T06:14:55.092Z</wsu:Created>
            </wsse:UsernameToken>
         </wsse:Security>

    </soapenv:Header>

   <soapenv:Body/>
</soapenv:Envelope>

Response:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
   <soapenv:Body>
      <soapenv:Fault xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
         <faultcode>wsse:InvalidSecurity</faultcode>
         <faultstring>The security token could not be authenticated or authorized</faultstring>
         <detail/>
      </soapenv:Fault>
   </soapenv:Body>
</soapenv:Envelope>

 

3. Password Digest - Bad Request with incorrect format, plaintext password
 

Request:


<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">

    <soapenv:Header>

             <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
                 soapenv:mustUnderstand="1">

               <wsse:UsernameToken xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
                    wsu:Id="UsernameToken-1815911473">
                 <wsse:Username>client</wsse:Username>
                 <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">apache</wsse:Password>
              </wsse:UsernameToken>
            </wsse:Security>

    </soapenv:Header>
   <soapenv:Body/>
</soapenv:Envelope>


Response:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
   <soapenv:Body>
      <soapenv:Fault xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
         <faultcode>wsse:InvalidSecurity</faultcode>
         <faultstring>The security token could not be authenticated or authorized</faultstring>
         <detail/>
      </soapenv:Fault>
   </soapenv:Body>
</soapenv:Envelope>


4. Password Digest - Bad Request, incomplete format, password digest is required, but plaintext is sent instead of digest, and without nonce and createdtime
 

Request:


<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">

    <soapenv:Header>

             <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
                 soapenv:mustUnderstand="1">

               <wsse:UsernameToken xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
                    wsu:Id="UsernameToken-1815911473">
                 <wsse:Username>client</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">plaintext password</wsse:Password>
              </wsse:UsernameToken>
            </wsse:Security>
    </soapenv:Header>
   <soapenv:Body/>
</soapenv:Envelope>

Response:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
   <soapenv:Body>
      <soapenv:Fault xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
         <faultcode>wsse:InvalidSecurity</faultcode>
         <faultstring>An invalid security token was provided (Bad UsernameToken Values)</faultstring>
         <detail/>
      </soapenv:Fault>
   </soapenv:Body>
</soapenv:Envelope>