Secure your webservices with JBoss
Join the DZone community and get the full member experience.
Join For FreeWS-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.
From http://dinukaroshan.blogspot.com/2011/04/secure-your-webservices-with-jboss.html
Opinions expressed by DZone contributors are their own.
Comments