Thursday, April 28, 2011

Secure your webservices with JBoss

WS-SEC is a vast area in it self and trying to cover the whole topic in one blog post is just not possible. So ill be touching on a glimpse of it by showing you how to achieve ws-sec with Jboss with the least amount of effort.

In simplest terms WS-SEC is more or less the same as what you do when you log into a website by providing your user name and password. Only difference is in this case you provide your credentials to gain access to the web service you are trying to access.

People use WS-SEC in order to protect their web service from being used by unknown third parties. There always might be instances in which you would want to restrict access to certain web services as they process and handle confidential data. For this situation WS-SEC is a must. Adding HTTPS would guarantee transport leve security for your Web service as well. But in this article i will only focus on securing your web service through WS-SEC as there are many articles explaining how to expose it on HTTPS.

Note that i will be creating the web service using the @Webservice annotation(a.k.a code first approach). Following i have given the web service we are going to use for our example;

package com.myservices;
@WebService(serviceName = "MyService", portName = "MyServicePort", targetNamespace = "personlaservices")
@Stateless
@HandlerChain(file = "myconfigs/webservices/security_handler.xml")
public class MySecurityService {

 @WebMethod
 public String sayMyName(){
  return "Iron Man!!!";
 }

}

Note that i have just a simple method with a string output. The important thing to note here is the @HandlerChain attribute. This refers to an XML we define which i will show below which defines the class which will handle the authentication by reading the user name and password which comes in the header part of the soap message enclosed within the ws-sec tag. Note that the xml file path is relative to the classpath. I have placed the folder myconfigs within the conf directory of jboss.

The Configuration XML security_handler.xml is as follows;

<?xml version="1.0" encoding="UTF-8"?>



<handler-chains xmlns="http://java.sun.com/xml/ns/javaee">



 <!-- Note:  The '*" denotes a wildcard. -->



 <handler-chain>

  <handler>

   <handler-name>com.myservice.security.AuthenticateHandler</handler-name>

   <handler-class>com.myservice.security.AuthenticateHandler

   </handler-class>

  </handler>

 </handler-chain>

</handler-chains>


This defines a class called AuthenticationHandler which will retrieve the user name and password contained within the soap message and do the authentication using the JBoss login module with the security realm defined which i will not go into detail as we all know how to handle security within JBoss. but you can use any authentication mechanism you want here.

/**
 * 
 */
package com.myservice.security;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.Set;

import javax.xml.namespace.QName;
import javax.xml.soap.Name;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPPart;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;
import javax.xml.ws.handler.soap.SOAPMessageContext;

import org.apache.log4j.Logger;



/**
 * Handler to authenticate the requests send in by the user from information provided through soap-headers.
 */
public class AuthenticateHandler implements SOAPHandler<SOAPMessageContext> {

    /** The Constant logger. */
    private static final Logger logger = Logger.getLogger(AuthenticateHandler.class);

    /** The Constant USERNAME_TOKEN_STRING. */
    private static final String USERNAME_TOKEN_STRING = "UsernameToken";

    /** The Constant USERNAME_STRING. */
    private static final String USERNAME_STRING = "Username";

    /** The Constant ARG_0_STRING. */
    private static final String ARG_0_STRING = "arg0";

    /** The Constant PASSWORD_STRING. */
    private static final String PASSWORD_STRING = "Password";


    private static final String HIGHPHEN = "-";

    /*
     * (non-Javadoc)
     * @see javax.xml.ws.handler.Handler#handleFault(javax.xml.ws.handler.MessageContext)
     */
    public boolean handleFault(SOAPMessageContext context) {
        // throw new UnsupportedOperationException("Not supported yet.");
        logger.debug("handleFault() is called");
        return true;
    }

    /*
     * (non-Javadoc)
     * @see javax.xml.ws.handler.Handler#handleMessage(javax.xml.ws.handler.MessageContext)
     */
    public boolean handleMessage(SOAPMessageContext smc) {

        SOAPMessage message = smc.getMessage();
        Boolean outboundProperty = (Boolean) smc.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
        boolean authenticated = false;

        try {

            // Let's extract information and try to log XML.
            SOAPPart sp = message.getSOAPPart();
            SOAPEnvelope envelope = sp.getEnvelope();

           
            if (!outboundProperty) {

                SOAPHeader header = envelope.getHeader();

                if (header != null) {
                    authenticated = processSOAPHeader(header);
                }

            }

        } catch (SOAPException se) {
            logger.error("SOAPException occured while processing the message", se);

        }
        return authenticated;
    }

    /**
     * Gets the sOAP message as string.
     * 
     * @param msg the msg
     * @return the sOAP message as string
     */
    private String getSOAPMessageAsString(SOAPMessage msg) {

        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            msg.writeTo(baos);
            return baos.toString();

        } catch (IOException ioe) {
            logger.warn("Could not extract XML from soap message", ioe);
            return null;
        } catch (SOAPException se) {
            logger.warn("Could not extract XML from soap message", se);
            return null;
        }
    }

    /**
     * Process soap header. This method is called by handleRequest method It retrieves the SOAP headers in the message
     * and authenticates the client.
     * 
     * @param sh the soap header
     * @return true, if successful
     */
    private boolean processSOAPHeader(SOAPHeader sh) {
        boolean authenticated = false;

        // look for authentication header element inside the HEADER block
        Iterator childElems = sh.getChildElements();

        SOAPElement child = extractUserNameInfo(childElems);

        if (child != null) {

            // call method to perform authentication
            authenticated = authenticateRequest(child);
        }
        return authenticated;
    }

    /**
     * Extract user name info.
     * 
     * @param childElems the child elems
     * @return the sOAP element
     */
    private SOAPElement extractUserNameInfo(Iterator childElems) {

        logger.debug("extractUserNameInfo called.");

        SOAPElement child = null;
        Name sName;

        // iterate through child elements
        while (childElems.hasNext()) {
            Object elem = childElems.next();

            if (elem instanceof SOAPElement) {

                // Get child element and its name
                child = (SOAPElement) elem;
                sName = child.getElementName();

                // Check whether there is a UserNameToken element
                if (!USERNAME_TOKEN_STRING.equalsIgnoreCase(sName.getLocalName())) {

                    if (child.getChildElements().hasNext()) { // TODO check logic
                        return extractUserNameInfo(child.getChildElements());
                    }
                }
            }
        }

        return child;
    }

    /**
     * Authenticate request. This method retrieves the authentication information for the request header and validates
     * it.
     * 
     * @param element the element
     * @return true, if successful
     */
    private boolean authenticateRequest(SOAPElement element) {

        logger.debug("authenticateRequest called");
        boolean authenticated = false;

        // variable for user name and password
        String userName = null;
        String password = null;
        Name sName;

        // get an iterator on child elements of SOAP element
        Iterator childElems = element.getChildElements();

        SOAPElement child;
        // loop through child elements

        while (childElems.hasNext()) {
            // get next child element
            Object elem = childElems.next();

            if (elem instanceof SOAPElement) {
                child = (SOAPElement) elem;

                // get the name of SOAP element
                sName = child.getElementName();

                // get the value of username element
                if (USERNAME_STRING.equalsIgnoreCase(sName.getLocalName())) {
                    logger.debug("---UserName =" + child.getValue());
                    userName = child.getValue();
                } else if (PASSWORD_STRING.equalsIgnoreCase(sName.getLocalName())) {
                    // get the value of password element
                    password = child.getValue();
                }

                if (userName != null && password != null) {

                   
   /**
       Note that in this instance i have used my custom used class
       called ClientLoginModule whic wraps a JBossLoginModule instance.
         You can use your own authentication mechanism as you have the user name
        and password at this point.
   **/
                        ClientLoginModule.login("WEBSERVICE" + "^" + userName, password);
                        return true;
                  

                    break;
                }

            }
        }

        if (userName == null || password == null) {
            logger.warn("Username or password is empty. userName : [" + userName + "], password : [" + password + "]");
        }

        return authenticated;

    }

    

    /**
     * Extract TCI info.
     * 
     * @param childElems the child elems
     * @return the sOAP element
     */
    private SOAPElement extractTCIInfo(Iterator childElems) {

        logger.debug("extractTCIInfo called.");

        SOAPElement child = null;
        Name sName;

        // iterate through child elements
        while (childElems.hasNext()) {
            Object elem = childElems.next();

            if (elem instanceof SOAPElement) {

                // Get child element and its name
                child = (SOAPElement) elem;
                sName = child.getElementName();

                // Check whether there is a UserNameToken element
                if (!ARG_0_STRING.equalsIgnoreCase(sName.getLocalName())) {

                    if (child.getChildElements().hasNext()) {
                        return extractTCIInfo(child.getChildElements());
                    }
                }
            }
        }

        return child;
    }

    /*
     * (non-Javadoc)
     * @see javax.xml.ws.handler.soap.SOAPHandler#getHeaders()
     */
    public Set<QName> getHeaders() {
        logger.debug("--- In AuthenticateHandler.getHeaders ()");
        // return headers;
        return null;
    }

    /*
     * (non-Javadoc)
     * @see javax.xml.ws.handler.Handler#close(javax.xml.ws.handler.MessageContext)
     */
    public void close(MessageContext context) {
        logger.debug("close() is called");
        // throw new UnsupportedOperationException("Not supported yet.");
    }

}

This class extracts the user name and password as you can see within the method authenticateRequest() and authenticates the user. If authentication fails it will return false from within handleMessage(). Note that i have used a class called ClientLoginModule. This is a class i have written which extends JBoss Login Module. I did not go into much detail with that due to the fact that it is already known to anyone who has dealt with jboss user security handling.

Now that we have these two methods you just need to bundle this up and run jboss which will expose the wsdl of this service. In jboss you can see what current web services are being hosted on your server by going to the URL http://localhost:8080/jbossws/services where 8080 is the port you expose your jboss on.

After you get your wsdl you need to generate the stubs by running any tool such as wsimport which does the wsdl to java transformation for you. Check here for more information.

Assuming you got your stubs generated and constructed a jar containing your code i will show you how to consume this ws-sec enabled webservice through a client.


try {
            URL wsdlLocation = new URL("http://localhost:8080/MyService");

            QName qName = new QName("personlaservices", "MyService");

            Service service = null;
            service = Service.create(wsdlLocation, qName);
            
     /**
  HeaderHandlerResolve will pass this information to a HeaderHandler implementation
  class which will embed the user name and password passed in to the ws-security
  header within the soap header element
     **/
            HeaderHandlerResolver handlerResolver = new HeaderHandlerResolver("myusername","mypassword");
            service.setHandlerResolver(handlerResolver);

            MySecurityService servicePort = service.getPort(MySecurityService.class);

     System.out.println(servicePort.sayMyName());
        } catch (MalformedURLException mue) {
            logger.warn("An error occurred while getting the wsdl location.", mue);
        }

Here i first create an instance of the javax.xml.ws.Service class using the URL, namespace and the service name we provided within our web service implementation we specified earlier. Next the important thing to note here is we define a custom HeaderHandlerResolver which we set to the service. I will show you the implementation of the HeaderhandlerResolver as well as the HeaderHandler which is used within the HeaderHandlerResolver class so that you can understand what happens.

/**
 * 
 */
package com.myservice.client;

import java.util.ArrayList;
import java.util.List;

import javax.xml.ws.handler.Handler;
import javax.xml.ws.handler.HandlerResolver;
import javax.xml.ws.handler.PortInfo;

/**
 * Taken from www.javadb.com and modified.
 * 
 * @author www.javadb.com
 */
public class HeaderHandlerResolver implements HandlerResolver {

    private String userName;

    private String password;

    public HeaderHandlerResolver() {
        super();

    }

    public HeaderHandlerResolver(String userName,String password) {
        super();
 this.userName = userName;
 this.password = password;
    }

    @SuppressWarnings("unchecked")
    public List<Handler> getHandlerChain(PortInfo portInfo) {
        List<Handler> handlerChain = new ArrayList<Handler>();

        HeaderHandler hh = new HeaderHandler(userName,password);

        handlerChain.add(hh);

        return handlerChain;
    }
}


The Header Handler implementation is the one which embeds the ws-security header to the soap header as you can see from the below code;


/**
 * 
 */
package com.myservice.client;

import java.io.ByteArrayOutputStream;
import java.util.Set;

import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPPart;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;
import javax.xml.ws.handler.soap.SOAPMessageContext;

import org.apache.log4j.Logger;



/**
 * Taken from www.javadb.com and modified.
 * 
 * @author www.javadb.com
 */
public class HeaderHandler implements SOAPHandler<SOAPMessageContext> {

    /** The Constant logger. */
    private static final Logger logger = Logger.getLogger(HeaderHandler.class);

    /** The Constant WS_SECURITY_SECEXT_URI. */
    private static final String WS_SECURITY_SECEXT_URI = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";

    /** The Constant WS_SECURITY_UTILITY_URI. */
    private static final String WS_SECURITY_UTILITY_URI = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";

    /** The Constant WS_PASSWORD_TYPE_URI. */
    private static final String WS_PASSWORD_TYPE_URI = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText";

    /** The Constant WSSE_PREFIX. */
    private static final String WSSE_PREFIX = "wsse";

    /** The Constant SECURITY_LOCAL_NAME. */
    private static final String SECURITY_LOCAL_NAME = "Security";

    /** The Constant USERNAME_TOKEN_LOCAL_NAME. */
    private static final String USERNAME_TOKEN_LOCAL_NAME = "UsernameToken";

    /** The Constant LOCAL_PART_XMLNS_WSU. */
    private static final String LOCAL_PART_XMLNS_WSU = "wsu";

    /** The Constant USERNAME_LOCAL_NAME. */
    private static final String USERNAME_LOCAL_NAME = "Username";

    /** The Constant PASSWORD_LOCAL_NAME. */
    private static final String PASSWORD_LOCAL_NAME = "Password";

    /** The Constant PASSWORD_ATTRIBUTE_TYPE. */
    private static final String PASSWORD_ATTRIBUTE_TYPE = "Type";

    private static final String HIGHPHEN = "-";

    private String userName;

    private String password;

    public HeaderHandler(String userName,String password) {
        this.userName = userName;
 this.password = password;
    }

    /*
     * (non-Javadoc)
     * @see javax.xml.ws.handler.Handler#handleMessage(javax.xml.ws.handler.MessageContext)
     */
    public boolean handleMessage(SOAPMessageContext smc) {

        Boolean outboundProperty = (Boolean) smc.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
        SOAPMessage message = smc.getMessage();

        try {

            // Let's extract information and try to log XML.
            SOAPPart sp = message.getSOAPPart();
            SOAPEnvelope envelope = sp.getEnvelope();

         

            if (outboundProperty.booleanValue()) {

                SOAPHeader header = envelope.getHeader();

                if (header != null) {
                    header.detachNode();
                }

                header = envelope.addHeader();

                SOAPElement security = header.addChildElement(SECURITY_LOCAL_NAME, WSSE_PREFIX, WS_SECURITY_SECEXT_URI);

                SOAPElement usernameToken = security.addChildElement(USERNAME_TOKEN_LOCAL_NAME, WSSE_PREFIX);

                QName qName = new QName(XMLConstants.NULL_NS_URI, LOCAL_PART_XMLNS_WSU);
                usernameToken.addAttribute(qName, WS_SECURITY_UTILITY_URI);

                SOAPElement username = usernameToken.addChildElement(USERNAME_LOCAL_NAME, WSSE_PREFIX);

                username.addTextNode(userName);

                SOAPElement password = usernameToken.addChildElement(PASSWORD_LOCAL_NAME, WSSE_PREFIX);
                password.setAttribute(PASSWORD_ATTRIBUTE_TYPE, WS_PASSWORD_TYPE_URI);

                password.addTextNode(password);

            }

        } catch (SOAPException se) {
            logger.error("SOAPException occured while processing the message", se);

        }

        return outboundProperty;

    }

    /**
     * Gets the sOAP message as string.
     * 
     * @param msg the msg
     * @return the sOAP message as string
     */
    private String getSOAPMessageAsString(SOAPMessage msg) {

        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            msg.writeTo(baos);
            return baos.toString();

        } catch (Exception e) {
            logger.warn("Could not extract XML from soap message");
            return null;
        }
    }

    /*
     * (non-Javadoc)
     * @see javax.xml.ws.handler.soap.SOAPHandler#getHeaders()
     */
    public Set<QName> getHeaders() {
        // throw new UnsupportedOperationException("Not supported yet.");
        logger.info("getHeaders() is called");
        return null;
    }

    /*
     * (non-Javadoc)
     * @see javax.xml.ws.handler.Handler#handleFault(javax.xml.ws.handler.MessageContext)
     */
    public boolean handleFault(SOAPMessageContext context) {
        // throw new UnsupportedOperationException("Not supported yet.");
        logger.info("handleFault() is called");
        return true;
    }

    /*
     * (non-Javadoc)
     * @see javax.xml.ws.handler.Handler#close(javax.xml.ws.handler.MessageContext)
     */
    public void close(MessageContext context) {
        logger.info("close() is called");
        // throw new UnsupportedOperationException("Not supported yet.");
    }
}


Thats about it guys. Withing the handleMessage() method in the above class you can see we are embedding the user name and password to the ws-security header element which is part of the soap header. So now when you call your web service the message wil go through the HeaderHandlerResolver which will pass it on to the Header Handler which in turn will embed the ws-security header before passing on the soap request.

If you have any questions or if there are any areas of improvement you see please do leave a comment which is as always highly appreciated.


Cheers Guys!!!

2 comments:

  1. thanks a lot i reading your blog can you give me source for this samples
    ,i suggest that if you add sample source to your post is very good

    aa.azizkhani@gmail.com

    ReplyDelete
  2. Hi Ali,

    I will check if i can find the sample code for this since this is a bit of an old post. If you require help with code you are writing, i am glad to help you out.

    ReplyDelete