I'm implementing a JAX-WS webservice that will be consumed by external Java and PHP clients.
The clients have to authenticate with a username and password stored in a database per client.
What authentication mechanism is best to use to make sure that misc clients can use it?
Basic WS-Security would work with both Java and PHP clients (amongst others) plugged in to JAAS to provide a database backend . How to implement that kind of depends on your container. Annotate your web service methods with the @RolesAllowed annotation to control which roles the calling user must have. All J2EE containers will provide some mechanism to specify against which JAAS realm users should be authenticated. In Glassfish for example, you can use the admin console to manage realms, users and groups. In your application.xml you then specify the realm and the group to role mappings.
Here are some details of how to achieve this on Glassfish
With JBoss WS in JBoss, it's even easier.
What JAX-WS implementation are you using and in which container?
For our Web Service authentication we are pursuing a twofold approach, in order to make sure that clients with different prerequisites are able to authenticate.
- Authenticate using a username and password parameter in the HTTP Request Header
- Authenticate using HTTP Basic Authentication.
Please note, that all traffic to our Web Service is routed over an SSL secured connection. Thus, sniffing the passwords is not possible. Of course one may also choose HTTP authentication with digest - see this interesting site for more information on this.
But back to our example:
//First, try authenticating against two predefined parameters in the HTTP
//Request Header: 'Username' and 'Password'.
public static String authenticate(MessageContext mctx) {
String s = "Login failed. Please provide a valid 'Username' and 'Password' in the HTTP header.";
// Get username and password from the HTTP Header
Map httpHeaders = (Map) mctx.get(MessageContext.HTTP_REQUEST_HEADERS);
String username = null;
String password = null;
List userList = (List) httpHeaders.get("Username");
List passList = (List) httpHeaders.get("Password");
// first try our username/password header authentication
if (CollectionUtils.isNotEmpty(userList)
&& CollectionUtils.isNotEmpty(passList)) {
username = userList.get(0).toString();
password = passList.get(0).toString();
}
// No username found - try HTTP basic authentication
if (username == null) {
List auth = (List) httpHeaders.get("Authorization");
if (CollectionUtils.isNotEmpty(auth)) {
String[] authArray = authorizeBasic(auth.get(0).toString());
if (authArray != null) {
username = authArray[0];
password = authArray[1];
}
}
}
if (username != null && password != null) {
try {
// Perform the authentication - e.g. against credentials from a DB, Realm or other
return authenticate(username, password);
} catch (Exception e) {
LOG.error(e);
return s;
}
}
return s;
}
/**
* return username and password for basic authentication
*
* @param authorizeString
* @return
*/
public static String[] authorizeBasic(String authorizeString) {
if (authorizeString != null) {
StringTokenizer st = new StringTokenizer(authorizeString);
if (st.hasMoreTokens()) {
String basic = st.nextToken();
if (basic.equalsIgnoreCase("Basic")) {
String credentials = st.nextToken();
String userPass = new String(
Base64.decodeBase64(credentials.getBytes()));
String[] userPassArray = userPass.split(":");
if (userPassArray != null && userPassArray.length == 2) {
String userId = userPassArray[0];
String userPassword = userPassArray[1];
return new String[] { userId, userPassword };
}
}
}
}
return null;
}
The first authentication using our predefined "Username" and "Password" parameters is in particular useful for our integration testers, who are using SOAP-UI (Although I am not entirely sure whether one cannot get to work SOAP-UI with HTTP Basic Authentication too). The second authentication attempt then uses the parameters which are provided by HTTP Basic authentication.
In order to intercept every call to the Web Service, we define a handler on every endpoint:
@HandlerChain(file = "../../../../../handlers.xml")
@SchemaValidation(handler = SchemaValidationErrorHandler.class)
public class DeliveryEndpointImpl implements DeliveryEndpoint {
The handler.xml looks like:
<handler-chains xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee">
<handler-chain>
<handler>
<handler-name>AuthenticationHandler</handler-name>
<handler-class>mywebservice.handler.AuthenticationHandler</handler-class>
</handler>
</handler-chain>
</handler-chains>
As you can see, the handler points to an AuthenticationHandler, which intercepts every call to the Web Service endpoint. Here's the Authentication Handler:
public class AuthenticationHandler implements SOAPHandler<SOAPMessageContext> {
/**
* Logger
*/
public static final Log log = LogFactory
.getLog(AuthenticationHandler.class);
/**
* The method is used to handle all incoming messages and to authenticate
* the user
*
* @param context
* The message context which is used to retrieve the username and
* the password
* @return True if the method was successfully handled and if the request
* may be forwarded to the respective handling methods. False if the
* request may not be further processed.
*/
@Override
public boolean handleMessage(SOAPMessageContext context) {
// Only inbound messages must be authenticated
boolean isOutbound = (Boolean) context
.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
if (!isOutbound) {
// Authenticate the call
String s = EbsUtils.authenticate(context);
if (s != null) {
log.info("Call to Web Service operation failed due to wrong user credentials. Error details: "
+ s);
// Return a fault with an access denied error code (101)
generateSOAPErrMessage(
context.getMessage(),
ServiceErrorCodes.ACCESS_DENIED,
ServiceErrorCodes
.getErrorCodeDescription(ServiceErrorCodes.ACCESS_DENIED),
s);
return false;
}
}
return true;
}
/**
* Generate a SOAP error message
*
* @param msg
* The SOAP message
* @param code
* The error code
* @param reason
* The reason for the error
*/
private void generateSOAPErrMessage(SOAPMessage msg, String code,
String reason, String detail) {
try {
SOAPBody soapBody = msg.getSOAPPart().getEnvelope().getBody();
SOAPFault soapFault = soapBody.addFault();
soapFault.setFaultCode(code);
soapFault.setFaultString(reason);
// Manually crate a failure element in order to guarentee that this
// authentication handler returns the same type of soap fault as the
// rest
// of the application
QName failureElement = new QName(
"http://yournamespacehere.com", "Failure", "ns3");
QName codeElement = new QName("Code");
QName reasonElement = new QName("Reason");
QName detailElement = new QName("Detail");
soapFault.addDetail().addDetailEntry(failureElement)
.addChildElement(codeElement).addTextNode(code)
.getParentElement().addChildElement(reasonElement)
.addTextNode(reason).getParentElement()
.addChildElement(detailElement).addTextNode(detail);
throw new SOAPFaultException(soapFault);
} catch (SOAPException e) {
}
}
/**
* Handles faults
*/
@Override
public boolean handleFault(SOAPMessageContext context) {
// do nothing
return false;
}
/**
* Close - not used
*/
@Override
public void close(MessageContext context) {
// do nothing
}
/**
* Get headers - not used
*/
@Override
public Set<QName> getHeaders() {
return null;
}
}
In the AuthenticationHandler we are calling the authenticate() method, defined further above. Note that we create a manual SOAP fault called "Failure" in case something goes wrong with the authentication.
Is there a way independent on the current container? I'd like to define which class is responsible for authorisation. That class could call database or have password elsewhere.