blob: c0f66fd10f72f64258205bd683dbf40dc547126e [file] [log] [blame]
/*
* Copyright 2003-2004 The Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.apache.ws.security.handler;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ws.security.SOAPConstants;
import org.apache.ws.security.WSConstants;
import org.apache.ws.security.WSSecurityEngineResult;
import org.apache.ws.security.WSSecurityException;
import org.apache.ws.security.message.token.Timestamp;
import org.apache.ws.security.util.WSSecurityUtil;
import org.apache.xml.security.utils.XMLUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import javax.security.auth.callback.CallbackHandler;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.rpc.Call;
import javax.xml.rpc.JAXRPCException;
import javax.xml.rpc.handler.Handler;
import javax.xml.rpc.handler.HandlerInfo;
import javax.xml.rpc.handler.MessageContext;
import javax.xml.rpc.handler.soap.SOAPMessageContext;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPHeaderElement;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPPart;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.security.cert.X509Certificate;
import java.util.Iterator;
import java.util.Vector;
/**
* Merged and converted the the axis handlers WSDoAllReceiver and WSDoAllSender
* into a single JAX-RPC Handler. All the axis dependencies are removed.
*
* @author Venkat Reddy (vreddyp@gmail.com).
*/
public class WSS4JHandler extends WSHandler implements Handler {
private HandlerInfo handlerInfo;
private static Log log = LogFactory.getLog(WSS4JHandler.class.getName());
private static boolean doDebug = log.isDebugEnabled();
static final String DEPLOYMENT = "deployment";
static final String CLIENT_DEPLOYMENT = "client";
static final String SERVER_DEPLOYMENT = "server";
static final String FLOW = "flow";
static final String REQUEST_ONLY = "request-only";
static final String RESPONSE_ONLY = "response-only";
static final String ALLOW_FORM_OPTIMIZATION = "axis.form.optimization";
/**
* Initializes the instance of the handler.
*/
public void init(HandlerInfo hi) {
handlerInfo = hi;
}
/**
* Destroys the Handler instance.
*/
public void destroy() {
}
public QName[] getHeaders() {
return handlerInfo.getHeaders();
}
public boolean handleRequest(MessageContext mc) {
mc.setProperty(ALLOW_FORM_OPTIMIZATION,
Boolean.TRUE);
try {
return processMessage(mc, true);
} catch (WSSecurityException e) {
if (doDebug) {
log.debug(e.getMessage(), e);
}
throw new JAXRPCException(e);
}
}
public boolean handleResponse(MessageContext mc) {
mc.setProperty(ALLOW_FORM_OPTIMIZATION,
Boolean.TRUE);
try {
return processMessage(mc, false);
} catch (WSSecurityException e) {
if (doDebug) {
log.debug(e.getMessage(), e);
}
throw new JAXRPCException(e);
}
}
/**
* Handles SOAP Faults that may occur during message processing
*/
public boolean handleFault(MessageContext mc) {
if (doDebug) {
log.debug("Entered handleFault");
}
return true;
}
/**
* Switch for transferring control to doReceiver and doSender
*/
public boolean processMessage(MessageContext mc, boolean isRequestMessage) throws WSSecurityException {
RequestData reqData = new RequestData();
reqData.setMsgContext(mc);
doDebug = log.isDebugEnabled();
String deployment = null;
String handleFlow = null;
if ((deployment = (String) getOption(DEPLOYMENT)) == null) {
deployment = (String) mc.getProperty(DEPLOYMENT);
}
if (deployment == null) {
throw new JAXRPCException("WSS4JHandler.processMessage: No deployment defined");
}
if ((handleFlow = (String) getOption(FLOW)) == null) {
handleFlow = (String) mc.getProperty(FLOW);
}
if (handleFlow == null) {
handleFlow = "";
}
// call doSender if we are -
// (handling request and client-side deployment) or (handling response and server-side deployment).
// call doReceiver if we are -
// (handling request and server-side deployment) or (handling response and client-side deployment).
boolean needsHandling = ( isRequestMessage && !handleFlow.equals(RESPONSE_ONLY)) ||
(!isRequestMessage && !handleFlow.equals(REQUEST_ONLY));
try {
if (deployment.equals(CLIENT_DEPLOYMENT) ^ isRequestMessage) {
if (needsHandling) {
return doReceiver(mc, reqData, isRequestMessage);
}
} else {
if (needsHandling) {
return doSender(mc, reqData, isRequestMessage);
}
}
} finally {
reqData.clear();
reqData = null;
}
return true;
}
/**
* Handles incoming web service requests and outgoing responses
*/
public boolean doSender(MessageContext mc, RequestData reqData, boolean isRequest) throws WSSecurityException {
reqData.getSignatureParts().removeAllElements();
reqData.getEncryptParts().removeAllElements();
reqData.setNoSerialization(false);
/*
* Get the action first.
*/
Vector actions = new Vector();
String action = (String) getOption(WSHandlerConstants.SEND + '.' + WSHandlerConstants.ACTION);
if (action == null) {
if ((action = (String) getOption(WSHandlerConstants.ACTION)) == null) {
action = (String) mc.getProperty(WSHandlerConstants.ACTION);
}
}
if (action == null) {
throw new JAXRPCException("WSS4JHandler: No action defined");
}
int doAction = WSSecurityUtil.decodeAction(action, actions);
if (doAction == WSConstants.NO_SECURITY) {
return true;
}
/*
* For every action we need a username, so get this now. The username
* defined in the deployment descriptor takes precedence.
*/
reqData.setUsername((String) getOption(WSHandlerConstants.USER));
if (reqData.getUsername() == null || reqData.getUsername().equals("")) {
reqData.setUsername((String) mc.getProperty(WSHandlerConstants.USER));
mc.setProperty(WSHandlerConstants.USER, null);
}
/*
* Now we perform some set-up for UsernameToken and Signature
* functions. No need to do it for encryption only. Check if username
* is available and then get a password.
*/
if ((doAction & (WSConstants.SIGN | WSConstants.UT | WSConstants.UT_SIGN)) != 0) {
/*
* We need a username - if none throw an JAXRPCException. For encryption
* there is a specific parameter to get a username.
*/
if (reqData.getUsername() == null || reqData.getUsername().equals("")) {
throw new JAXRPCException("WSS4JHandler: Empty username for specified action");
}
}
if (doDebug) {
log.debug("Action: " + doAction);
log.debug("Actor: " + reqData.getActor());
}
/*
* Now get the SOAP part from the request message and convert it into a
* Document.
*
* This forces Axis to serialize the SOAP request into FORM_STRING.
* This string is converted into a document.
*
* During the FORM_STRING serialization Axis performs multi-ref of
* complex data types (if requested), generates and inserts references
* for attachments and so on. The resulting Document MUST be the
* complete and final SOAP request as Axis would send it over the wire.
* Therefore this must shall be the last (or only) handler in a chain.
*
* Now we can perform our security operations on this request.
*/
Document doc = null;
SOAPMessage message = ((SOAPMessageContext)mc).getMessage();
Boolean propFormOptimization = (Boolean)mc.getProperty("axis.form.optimization");
log.debug("Form optimization: " + propFormOptimization);
/*
* If the message context property contains a document then this is a
* chained handler.
*/
SOAPPart sPart = message.getSOAPPart();
if ((doc = (Document) mc.getProperty(WSHandlerConstants.SND_SECURITY))
== null) {
try {
doc = messageToDocument(message);
} catch (Exception e) {
if (doDebug) {
log.debug(e.getMessage(), e);
}
throw new JAXRPCException("WSS4JHandler: cannot get SOAP envlope from message", e);
}
}
if (doDebug) {
log.debug("WSS4JHandler: orginal SOAP request: ");
log.debug(org.apache.ws.security.util.XMLUtils.PrettyDocumentToString(doc));
}
doSenderAction(doAction, doc, reqData, actions, isRequest);
/*
* If required convert the resulting document into a message first. The
* outputDOM() method performs the necessary c14n call. After that we
* extract it as a string for further processing.
*
* Set the resulting byte array as the new SOAP message.
*
* If noSerialization is false, this handler shall be the last (or only)
* one in a handler chain. If noSerialization is true, just set the
* processed Document in the transfer property. The next Axis WSS4J
* handler takes it and performs additional security processing steps.
*
*/
if (reqData.isNoSerialization()) {
mc.setProperty(WSHandlerConstants.SND_SECURITY, doc);
} else {
ByteArrayOutputStream os = new ByteArrayOutputStream();
XMLUtils.outputDOM(doc, os, true);
if (doDebug) {
String osStr = null;
try {
osStr = os.toString("UTF-8");
} catch (UnsupportedEncodingException e) {
if (doDebug) {
log.debug(e.getMessage(), e);
}
osStr = os.toString();
}
log.debug("Send request:");
log.debug(osStr);
}
try {
sPart.setContent(new StreamSource(new ByteArrayInputStream(os.toByteArray())));
} catch (SOAPException se) {
if (doDebug) {
log.debug(se.getMessage(), se);
}
throw new JAXRPCException("Couldn't set content on SOAPPart" + se.getMessage(), se);
}
mc.setProperty(WSHandlerConstants.SND_SECURITY, null);
}
if (doDebug) {
log.debug("WSS4JHandler: exit invoke()");
}
return true;
}
/**
* handle responses
*
* @param mc
* @param reqData
* @return true on successful processing
* @throws WSSecurityException
*/
public boolean doReceiver(MessageContext mc, RequestData reqData, boolean isRequest) throws WSSecurityException {
Vector actions = new Vector();
String action = (String) getOption(WSHandlerConstants.RECEIVE + '.' + WSHandlerConstants.ACTION);
if (action == null) {
if ((action = (String) getOption(WSHandlerConstants.ACTION)) == null) {
action = (String) mc.getProperty(WSHandlerConstants.ACTION);
}
}
if (action == null) {
throw new JAXRPCException("WSS4JHandler: No action defined");
}
int doAction = WSSecurityUtil.decodeAction(action, actions);
String actor = (String) getOption(WSHandlerConstants.ACTOR);
SOAPMessage message = ((SOAPMessageContext)mc).getMessage();
SOAPPart sPart = message.getSOAPPart();
Document doc = null;
try {
doc = messageToDocument(message);
} catch (Exception ex) {
if (doDebug) {
log.debug(ex.getMessage(), ex);
}
throw new JAXRPCException("WSS4JHandler: cannot convert into document",
ex);
}
/*
* Check if it's a fault. Don't process faults.
*
*/
SOAPConstants soapConstants =
WSSecurityUtil.getSOAPConstants(doc.getDocumentElement());
if (WSSecurityUtil
.findElement(doc.getDocumentElement(),
"Fault",
soapConstants.getEnvelopeURI())
!= null) {
return false;
}
/*
* To check a UsernameToken or to decrypt an encrypted message we need
* a password.
*/
CallbackHandler cbHandler = null;
if ((doAction & (WSConstants.ENCR | WSConstants.UT)) != 0) {
cbHandler = getPasswordCB(reqData);
}
/*
* Get and check the Signature specific parameters first because they
* may be used for encryption too.
*/
doReceiverAction(doAction, reqData);
Vector wsResult = null;
try {
wsResult =
secEngine.processSecurityHeader(doc,
actor,
cbHandler,
reqData.getSigCrypto(),
reqData.getDecCrypto());
} catch (WSSecurityException ex) {
if (doDebug) {
log.debug(ex.getMessage(), ex);
}
throw new JAXRPCException("WSS4JHandler: security processing failed",
ex);
}
if (wsResult == null) { // no security header found
if (doAction == WSConstants.NO_SECURITY) {
return true;
} else {
throw new JAXRPCException("WSS4JHandler: Request does not contain required Security header");
}
}
if (reqData.getWssConfig().isEnableSignatureConfirmation() && !isRequest) {
checkSignatureConfirmation(reqData, wsResult);
}
/*
* If we had some security processing, get the original
* SOAP part of Axis' message and replace it with new SOAP
* part. This new part may contain decrypted elements.
*/
ByteArrayOutputStream os = new ByteArrayOutputStream();
XMLUtils.outputDOM(doc, os, true);
try {
sPart.setContent(new StreamSource(new ByteArrayInputStream(os.toByteArray())));
} catch (SOAPException se) {
if (doDebug) {
log.debug(se.getMessage(), se);
}
throw new JAXRPCException(
"Couldn't set content on SOAPPart" + se.getMessage(), se
);
}
if (doDebug) {
log.debug("Processed received SOAP request");
}
/*
* After setting the new current message, probably modified because
* of decryption, we need to locate the security header. That is,
* we force Axis (with getSOAPEnvelope()) to parse the string, build
* the new header. Then we examine, look up the security header
* and set the header as processed.
*
* Please note: find all header elements that contain the same
* actor that was given to processSecurityHeader(). Then
* check if there is a security header with this actor.
*/
SOAPHeader sHeader = null;
try {
sHeader = message.getSOAPPart().getEnvelope().getHeader();
} catch (Exception ex) {
if (doDebug) {
log.debug(ex.getMessage(), ex);
}
throw new JAXRPCException("WSS4JHandler: cannot get SOAP header after security processing", ex);
}
Iterator headers = sHeader.examineHeaderElements(actor);
SOAPHeaderElement headerElement = null;
while (headers.hasNext()) {
SOAPHeaderElement hE = (SOAPHeaderElement) headers.next();
if (hE.getElementName().getLocalName().equals(WSConstants.WSSE_LN)
&& ((Node) hE).getNamespaceURI().equals(WSConstants.WSSE_NS)) {
headerElement = hE;
break;
}
}
/* JAXRPC conversion changes */
headerElement.setMustUnderstand(false); // is this sufficient?
/*
* Now we can check the certificate used to sign the message.
* In the following implementation the certificate is only trusted
* if either it itself or the certificate of the issuer is installed
* in the keystore.
*
* Note: the method verifyTrust(X509Certificate) allows custom
* implementations with other validation algorithms for subclasses.
*/
// Extract the signature action result from the action vector
WSSecurityEngineResult actionResult = WSSecurityUtil.fetchActionResult(wsResult, WSConstants.SIGN);
if (actionResult != null) {
X509Certificate returnCert =
(X509Certificate)actionResult.get(WSSecurityEngineResult.TAG_X509_CERTIFICATE);
if (returnCert != null) {
if (!verifyTrust(returnCert, reqData)) {
throw new JAXRPCException("WSS4JHandler: The certificate used for the signature is not trusted");
}
}
}
/*
* Perform further checks on the timestamp that was transmitted in the header.
* In the following implementation the timestamp is valid if it was
* created after (now-ttl), where ttl is set on server side, not by the client.
*
* Note: the method verifyTimestamp(Timestamp) allows custom
* implementations with other validation algorithms for subclasses.
*/
// Extract the timestamp action result from the action vector
actionResult = WSSecurityUtil.fetchActionResult(wsResult, WSConstants.TS);
if (actionResult != null) {
Timestamp timestamp =
(Timestamp)actionResult.get(WSSecurityEngineResult.TAG_TIMESTAMP);
if (timestamp != null && reqData.getWssConfig().isTimeStampStrict()) {
if (!verifyTimestamp(timestamp, decodeTimeToLive(reqData))) {
throw new JAXRPCException("WSS4JHandler: The timestamp could not be validated");
}
}
}
/*
* now check the security actions: do they match, in right order?
*/
if (!checkReceiverResults(wsResult, actions)) {
throw new JAXRPCException("WSS4JHandler: security processing failed (actions mismatch)");
}
/*
* All ok up to this point. Now construct and setup the
* security result structure. The service may fetch this
* and check it.
*/
Vector results = null;
if ((results = (Vector) mc.getProperty(WSHandlerConstants.RECV_RESULTS))
== null) {
results = new Vector();
mc.setProperty(WSHandlerConstants.RECV_RESULTS, results);
}
WSHandlerResult rResult =
new WSHandlerResult(actor,
wsResult);
results.add(0, rResult);
if (doDebug) {
log.debug("WSS4JHandler: exit invoke()");
}
return true;
}
/**
* Utility method to convert SOAPMessage to org.w3c.dom.Document
*/
public static Document messageToDocument(SOAPMessage message) {
try {
Source content = message.getSOAPPart().getContent();
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
DocumentBuilder builder = dbf.newDocumentBuilder();
return builder.parse(org.apache.ws.security.util.XMLUtils.sourceToInputSource(content));
} catch (Exception ex) {
if (doDebug) {
log.debug(ex.getMessage(), ex);
}
throw new JAXRPCException("messageToDocument: cannot convert SOAPMessage into Document", ex);
}
}
public Object getOption(String key) {
return handlerInfo.getHandlerConfig().get(key);
}
public Object getProperty(Object msgContext, String key) {
return ((MessageContext)msgContext).getProperty(key);
}
public void setProperty(Object msgContext, String key, Object value) {
((MessageContext)msgContext).setProperty(key, value);
}
public String getPassword(Object msgContext) {
return (String) ((MessageContext)msgContext).getProperty(Call.PASSWORD_PROPERTY);
}
public void setPassword(Object msgContext, String password) {
((MessageContext)msgContext).setProperty(Call.PASSWORD_PROPERTY, password);
}
}