org.jasig.portal.security.provider.saml.SAMLDelegatedAuthenticationService.java Source code

Java tutorial

Introduction

Here is the source code for org.jasig.portal.security.provider.saml.SAMLDelegatedAuthenticationService.java

Source

/**
 * Licensed to Jasig under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Jasig licenses this file to you 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.jasig.portal.security.provider.saml;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.params.HttpClientParams;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HTTP;
import org.apache.xerces.parsers.DOMParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.*;
import org.w3c.dom.Node;
import org.w3c.dom.bootstrap.DOMImplementationRegistry;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSSerializer;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;

import javax.xml.soap.*;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
import java.util.UUID;

/**
 * <p>This class implements the delegated SAML authentication protocol.  Delegated
 * SAML authentication is most useful for portals, which often act as proxies
 * on behalf of the logged on users.  The portal can use its own SAML assertion
 * to request a "proxy" or "delegated" SAML assertion to present to a "downstream"
 * Web Service Provider (WSP) for authentication.</p>
 * <p>While this class implements the business logic for obtaining a delegated
 * SAML assertion, it is the {@link SAMLSession} class that is used to retain the
 * state of the authentication and the connection to the WSP.  Since this class
 * is not stateful, it can be considered thread-safe.</p>
 * 
 * @author Adam Rybicki
 */
public class SAMLDelegatedAuthenticationService {

    // XML namespace context
    private static final SAMLNamespaceContext NAMESPACE_CONTEXT = new SAMLNamespaceContext();
    private static final XPathExpressionExecutor EXPRESSION_POOL = new XPathExpressionPool(NAMESPACE_CONTEXT);

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    private DOMImplementationLS domLoadSaveImpl = null;
    private static final String SOAP_PREFIX = "soap";

    /**
     * Public default constructor that performs basic initialization
     */
    public SAMLDelegatedAuthenticationService() {
        DOMImplementationRegistry registry;
        try {
            registry = DOMImplementationRegistry.newInstance();
            domLoadSaveImpl = (DOMImplementationLS) registry.getDOMImplementation("LS");
        } catch (ClassCastException ex) {
            logger.error(
                    "Unable to initialize XML serializer implementation.  Make sure that the correct jar files are present.",
                    ex);
        } catch (ClassNotFoundException ex) {
            logger.error(
                    "Unable to initialize XML serializer implementation.  Make sure that the correct jar files are present.",
                    ex);
        } catch (InstantiationException ex) {
            logger.error(
                    "Unable to initialize XML serializer implementation.  Make sure that the correct jar files are present.",
                    ex);
        } catch (IllegalAccessException ex) {
            logger.error(
                    "Unable to initialize XML serializer implementation.  Make sure that the correct jar files are present.",
                    ex);
        }
    }

    /**
     * <p>This method should be used to authenticate to and get a resource from
     * a Shibboleth-protected Web Service.  Because it establishes a SAML session,
     * this method is processing-intensive, as it makes several HTTP connections
     * to complete delegated authentication with the IdP.  Once the authentication
     * succeeds, the client of the library should use the HttpClient available
     * by calling {@link SAMLSession#getHttpClient()}</p>
     * 
     * <p>Calling this method should only be done in exceptional cases.  THis is
     * because the request and response interceptors installed on the HttpClient
     * by {@link SAMLSession} should be able to perform authentication
     * automatically.</p>
     * 
     * @param samlSession   SAML session
     * @param resource      a Resource object whose URL member is set to represent
     *                      the resource to retrieve.  Upon successful return the
     *                      Resource object will contain a String representing
     *                      the retrieved resource.  However, if this method returns
     *                      a non-null value, the returned value means should be used
     *                      to request the resource.
     * @return HttpResponse from the WSP after authentication.  Depending on the HTTP method used, this will
     *                      either include an HTTP 302 redirect to the originally requested resource or a result
     *                      of submitting form data in case if the initial request was from HTTP POST.
     */
    public HttpResponse authenticate(SAMLSession samlSession, Resource resource) {

        if (samlSession.getSamlAssertion() == null) {
            String message = "SAML assertion not present.";
            logger.error(message);
            throw new DelegatedAuthenticationRuntimeException(message);
        }
        if (samlSession.getPortalEntityID() == null) {
            String message = "Portal entity ID not present.";
            logger.error(message);
            throw new DelegatedAuthenticationRuntimeException(message);
        }
        DelegatedSAMLAuthenticationState authnState = new DelegatedSAMLAuthenticationState();
        // The following represents the entire delegated authentication flow
        if (getSOAPRequest(samlSession, resource, authnState)) {
            return this.authenticate(samlSession, authnState);
        }

        return null;
    }

    /**
     * <p>This method authenticates to a WPS as a result of intercepting a blocked
     * access for a resource and getting a SOAP request for delegated SAML
     * authentication.</p>
     * 
     * <p>This method is called by the {@link org.apache.http.HttpResponseInterceptor}
     * when the interceptor determines that the WSP requires authentication.</p>
     * 
     * @param samlSession SAML session
     * @param paosBytes SOAP request for authentication
     * @return HttpResponse from the WSP after authentication.  Depending on the HTTP method used, this will
     *                      either include an HTTP 302 redirect to the originally requested resource or a result
     *                      of submitting form data in case if the initial request was from HTTP POST.
     */
    public HttpResponse authenticate(SAMLSession samlSession, byte[] paosBytes) {
        if (samlSession.getSamlAssertion() == null) {
            String message = "SAML assertion not present.";
            logger.error(message);
            throw new DelegatedAuthenticationRuntimeException(message);
        }
        if (samlSession.getPortalEntityID() == null) {
            String message = "Portal entity ID not present.";
            logger.error(message);
            throw new DelegatedAuthenticationRuntimeException(message);
        }
        DelegatedSAMLAuthenticationState authnState = new DelegatedSAMLAuthenticationState();
        authnState.setSoapRequest(paosBytes);
        return this.authenticate(samlSession, authnState);
    }

    private HttpResponse authenticate(SAMLSession samlSession, DelegatedSAMLAuthenticationState authnState) {
        // The following represents the entire delegated authentication flow
        if (getIDP(samlSession, authnState) && validateIDP(samlSession, authnState)
                && processSOAPRequest(samlSession, authnState) && getSOAPResponse(samlSession, authnState)
                && processSOAPResponse(samlSession, authnState)) {

            HttpResponse response = sendSOAPResponse(samlSession, authnState);
            return response;
        }
        return null;
    }

    /**
     * This method makes a request for a resource, but assuming that the resource
     * is protected, it actually expects to receive a SOAP request for authentication.
     * This is referred to as a PAOS (reversed SOAP) request because the SOAP
     * request is returned as an http response. 
     * @param samlSession
     * @param resource
     * @param authnState
     * @return
     */
    private boolean getSOAPRequest(SAMLSession samlSession, Resource resource,
            DelegatedSAMLAuthenticationState authnState) {
        logger.debug("getSOAPRequest from {}", resource.getResourceUrl());
        HttpGet method = new HttpGet(resource.getResourceUrl());

        try {
            resource.setupWSPClientConnection(samlSession);
            // There is no need to check the HTTP response status because the HTTP
            // client will handle normal HTTP protocol flow, including redirects
            // In case of error, HTTP client will throw an exception
            HttpResponse response = samlSession.getHttpClient().execute(method);
            HttpEntity entity = response.getEntity();
            long contentLength = entity.getContentLength();
            ByteArrayOutputStream os = new ByteArrayOutputStream((int) contentLength);
            entity.writeTo(os);
            os.close();
            byte[] paosBytes = os.toByteArray();
            authnState.setSoapRequest(paosBytes);
        } catch (Exception ex) {
            // There is nothing that can be done about this exception other than to log it
            // Exception must be caught and not rethrown to allow normal processing to continue
            logger.error("Exception caught when trying to retrieve the resource.", ex);
            throw new DelegatedAuthenticationRuntimeException(
                    "Exception caught when trying to retrieve the resource.", ex);
        }
        return true;
    }

    /**
     * This method validates that the IDP in the SOAP request received from WSP
     * matches the one in the SAML assertion.
     * @param authnState 
     */
    private boolean validateIDP(SAMLSession samlSession, DelegatedSAMLAuthenticationState authnState) {
        this.logger.debug("Step 2 of 5: Validate against SOAP request");
        InputStream is = null;

        try {
            is = new ByteArrayInputStream(authnState.getSoapRequest());
            InputSource source = new InputSource(is);
            DOMParser parser = new DOMParser();
            parser.setFeature("http://xml.org/sax/features/namespaces", true);
            parser.parse(source);
            Document doc = parser.getDocument();

            if (samlSession.isSkipValidateIdp()) {
                logger.debug("skipValidateIdp is set to true, setting soap request DOM");
                authnState.setSoapRequestDom(doc);
                return true;
            }

            String expression = "/S:Envelope/S:Header/ecp:Request/samlp:IDPList/samlp:IDPEntry[@ProviderID='"
                    + authnState.getIdp() + "']";
            NodeList nodes = EXPRESSION_POOL.evaluate(expression, doc, XPathConstants.NODESET);

            if (nodes.getLength() > 0) {
                logger.debug("Found matching IDP using expression {}", expression);
                authnState.setSoapRequestDom(doc);
                return true;
            }
            logger.debug("No matching IDP found using expression {}", expression);
        } catch (XPathExpressionException ex) {
            logger.error("Programming error.  Invalid XPath expression.", ex);
            throw new DelegatedAuthenticationRuntimeException("Programming error.  Invalid XPath expression.", ex);
        } catch (SAXException ex) {
            logger.error("XML error.", ex);
            throw new DelegatedAuthenticationRuntimeException("XML error.", ex);
        } catch (IOException ex) {
            logger.error("Unexpected error.  This method performs no I/O!", ex);
            throw new DelegatedAuthenticationRuntimeException("Unexpected error.  This method performs no I/O!",
                    ex);
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException ex) {
                    //safe to ignore during cleanup
                }
            }
        }
        return false;
    }

    /**
     * This method extracts the IDP from the SAML assertion
     * 
     * @param samlSession
     * @param authnState 
     * @return true, if successful
     */
    private boolean getIDP(SAMLSession samlSession, DelegatedSAMLAuthenticationState authnState) {
        this.logger.debug("Step 1 of 5: get IDP from SAML Assertion");
        InputStream is = null;
        try {
            if (samlSession.getSamlAssertionDom() == null) {
                is = new ByteArrayInputStream(samlSession.getSamlAssertion().getBytes());
                InputSource source = new InputSource(is);
                DOMParser parser = new DOMParser();
                parser.setFeature("http://xml.org/sax/features/namespaces", true);
                parser.parse(source);
                Document doc = parser.getDocument();
                samlSession.setSamlAssertionDom(doc);
            }
            String expression = "/saml2:Assertion/saml2:Issuer";
            Node node = EXPRESSION_POOL.evaluate(expression, samlSession.getSamlAssertionDom(),
                    XPathConstants.NODE);

            if (node != null) {
                String idp = node.getTextContent();
                logger.debug("Found IDP {} using expression {}", idp, expression);
                authnState.setIdp(idp);

                if (samlSession.getIdpResolver() == null) {
                    samlSession.setIdpResolver(new AssertionIdpResolverImpl(EXPRESSION_POOL));
                }

                samlSession.getIdpResolver().resolve(samlSession, authnState);
                return true;
            }

            logger.debug("No IDP found using expression {}", expression);
        } catch (XPathExpressionException ex) {
            logger.error("Programming error.  Invalid XPath expression.", ex);
            throw new DelegatedAuthenticationRuntimeException("Programming error.  Invalid XPath expression.", ex);
        } catch (SAXException ex) {
            logger.error("XML error.", ex);
            logger.trace("XML parsing error when parsing the SAML assertion.  The assertion was: ["
                    + samlSession.getSamlAssertion() + "].");
            throw new DelegatedAuthenticationRuntimeException("XML error.", ex);
        } catch (IOException ex) {
            logger.error("Unexpected error.  This method performs no I/O!", ex);
            throw new DelegatedAuthenticationRuntimeException("Unexpected error.  This method performs no I/O!",
                    ex);
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException ex) {
                    //safe to ignore during cleanup
                }
            }
        }
        return false;
    }

    /**
     * This method takes the SOAP request that come from the WSP and removes
     * the elements that need to be removed per the SAML Profiles spec.
     * 
     * @param samlSession
     * @param authnState 
     * @return true, if successful
     */
    private boolean processSOAPRequest(SAMLSession samlSession, DelegatedSAMLAuthenticationState authnState) {
        this.logger.debug("Step 3 of 5: Process SOAP Request");
        try {
            String expression = "/S:Envelope/S:Header/paos:Request";
            Document dom = authnState.getSoapRequestDom();
            Node node = EXPRESSION_POOL.evaluate(expression, dom, XPathConstants.NODE);

            if (node != null) {
                // Save the response consumer URL to samlSession
                String responseConsumerURL = node.getAttributes().getNamedItem("responseConsumerURL")
                        .getTextContent();
                logger.debug("Loaded response consumer URL {}", responseConsumerURL);
                authnState.setResponseConsumerURL(responseConsumerURL);
                // Save the PAOS MessageID, if present
                Node paosMessageID = node.getAttributes().getNamedItem("messageID");

                if (paosMessageID != null)
                    authnState.setPaosMessageID(paosMessageID.getTextContent());
                else
                    authnState.setPaosMessageID(null);

                // This removes the paos:Request node
                node.getParentNode().removeChild(node);

                // Retrieve the RelayState cookie for sending it back to the WSP with the SOAP Response
                expression = "/S:Envelope/S:Header/ecp:RelayState";
                node = EXPRESSION_POOL.evaluate(expression, dom, XPathConstants.NODE);
                if (node != null) {
                    Element relayStateElement = (Element) node;
                    authnState.setRelayStateElement(relayStateElement);
                    node.getParentNode().removeChild(node);
                }

                // On to the ecp:Request for removal
                expression = "/S:Envelope/S:Header/ecp:Request";
                node = EXPRESSION_POOL.evaluate(expression, dom, XPathConstants.NODE);
                node.getParentNode().removeChild(node);

                // Now add some namespace bindings to the SOAP Header
                expression = "/S:Envelope/S:Header";
                Element soapHeader = EXPRESSION_POOL.evaluate(expression, dom, XPathConstants.NODE);

                // Add new elements to S:Header
                Element newElement = dom.createElementNS(NAMESPACE_CONTEXT.getNamespaceURI("sbf"), "sbf:Framework");
                newElement.setAttribute("version", "2.0");
                soapHeader.appendChild(newElement);
                newElement = dom.createElementNS(NAMESPACE_CONTEXT.getNamespaceURI("sb"), "sb:Sender");
                newElement.setAttribute("providerID", samlSession.getPortalEntityID());
                soapHeader.appendChild(newElement);
                newElement = dom.createElementNS(NAMESPACE_CONTEXT.getNamespaceURI("wsa"), "wsa:MessageID");
                String messageID = generateMessageID();
                newElement.setTextContent(messageID);
                soapHeader.appendChild(newElement);
                newElement = dom.createElementNS(NAMESPACE_CONTEXT.getNamespaceURI("wsa"), "wsa:Action");
                newElement.setTextContent("urn:liberty:ssos:2006-08:AuthnRequest");
                soapHeader.appendChild(newElement);

                // This is the wsse:Security element 
                Element securityElement = dom.createElementNS(
                        "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
                        "wsse:Security");
                securityElement.setAttribute(soapHeader.getPrefix() + ":mustUnderstand", "1");
                Element createdElement = dom.createElement("wsu:Created");
                // The examples use Zulu time zone, not local
                TimeZone zuluTimeZone = TimeZone.getTimeZone("Zulu");
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SS'Z'");
                sdf.setTimeZone(zuluTimeZone);
                createdElement.setTextContent(sdf.format(new Date()));
                newElement = dom.createElementNS(
                        "http://www.docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd",
                        "wsu:Timestamp");
                newElement.appendChild(createdElement);
                securityElement.appendChild(newElement);
                // Finally, insert the original SAML assertion
                Node samlAssertionNode = dom.importNode(samlSession.getSamlAssertionDom().getDocumentElement(),
                        true);
                securityElement.appendChild(samlAssertionNode);
                soapHeader.appendChild(securityElement);

                // Store the modified SOAP Request in the SAML Session
                String modifiedSOAPRequest = writeDomToString(dom);
                authnState.setModifiedSOAPRequest(modifiedSOAPRequest);
                logger.debug("Completed processing of SOAP request");
                return true;
            }
            logger.debug("Failed to process SOAP request using expression {}", expression);
        } catch (XPathExpressionException ex) {
            logger.error("Programming error.  Invalid XPath expression.", ex);
            throw new DelegatedAuthenticationRuntimeException("Programming error.  Invalid XPath expression.", ex);
        }
        return false;
    }

    /**
     * @return String containing a UUID
     */
    private String generateMessageID() {
        UUID uuid = UUID.randomUUID();
        String messageID = "urn:uuid:" + uuid.toString();
        return messageID;
    }

    /**
     * This method takes the SOAP AuthnRequest, sends it to the IdP, and retrieves
     * the result.  This method does not process the result.
     * 
     * @param samlSession SAML session
     * @param authnState 
     * @return true, if successful
     */
    private boolean getSOAPResponse(SAMLSession samlSession, DelegatedSAMLAuthenticationState authnState) {
        this.logger.debug("Step 4 of 5: Get SOAP response from IDP");

        String result = null;
        HttpParams params = new BasicHttpParams();
        HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
        HttpProtocolParams.setContentCharset(params, "UTF-8");
        params.setParameter("SOAPAction", "urn:liberty:ssos:2006-08:AuthnRequest");
        HttpClient client = new DefaultHttpClient(params);

        try {
            logger.debug("Getting SOAP response from {} with POST body:\n{}", authnState.getIdpEndpoint(),
                    authnState.getModifiedSOAPRequest());
            setupIdPClientConnection(client, samlSession, authnState);
            HttpPost method = new HttpPost(authnState.getIdpEndpoint());
            method.setHeader("Content-Type", "text/xml");
            StringEntity postData = new StringEntity(authnState.getModifiedSOAPRequest(), HTTP.UTF_8);
            method.setEntity(postData);
            HttpResponse httpResponse = client.execute(method);
            int resultCode = httpResponse.getStatusLine().getStatusCode();

            if (resultCode >= HttpStatus.SC_OK && resultCode < 300) {
                HttpEntity httpEntity = httpResponse.getEntity();
                ByteArrayOutputStream output = new ByteArrayOutputStream();
                httpEntity.writeTo(output);
                result = output.toString();
                logger.debug("Got SOAP response:\n{}", result);
                authnState.setSoapResponse(result);
                return true;
            }

            logger.error("Unsupported HTTP result code when retrieving the resource: " + resultCode + ".");
            throw new DelegatedAuthenticationRuntimeException(
                    "Unsupported HTTP result code when retrieving the resource: " + resultCode + ".");
        } catch (Exception ex) {
            logger.error("Exception caught when trying to retrieve the resource.", ex);
            throw new DelegatedAuthenticationRuntimeException(
                    "Exception caught when trying to retrieve the resource.", ex);
        } finally {
            client.getConnectionManager().shutdown();
        }
    }

    /**
     * This method processes the SOAP response from the IdP, and converts it
     * for presenting it back to the WSP that requested a delegated SAML
     * assertion.
     * 
     * @param samlSession SAML session
     * @param authnState 
     * @return true, if successful
     */
    private boolean processSOAPResponse(SAMLSession samlSession, DelegatedSAMLAuthenticationState authnState) {
        this.logger.debug("Step 5 of 5: Processing SOAP response");

        try {
            String expression = "/soap:Envelope/soap:Header/ecp:Response";
            InputStream is = new ByteArrayInputStream(authnState.getSoapResponse().getBytes());
            InputSource source = new InputSource(is);
            DOMParser parser = new DOMParser();
            parser.setFeature("http://xml.org/sax/features/namespaces", true);
            parser.parse(source);
            Document doc = parser.getDocument();
            Node node = EXPRESSION_POOL.evaluate(expression, doc, XPathConstants.NODE);

            if (node != null) {
                String responseConsumerURL = node.getAttributes().getNamedItem("AssertionConsumerServiceURL")
                        .getTextContent();

                logger.debug("Found {} node found in SOAP response.", expression);

                if (responseConsumerURL != null
                        && responseConsumerURL.equals(authnState.getResponseConsumerURL())) {
                    logger.debug("responseConsumerURL {} matches {}", responseConsumerURL,
                            authnState.getResponseConsumerURL());

                    // Retrieve and save the SOAP prefix used
                    String soapPrefix = node.getParentNode().getPrefix();
                    Element ecpResponse = (Element) node;
                    Element soapHeader = (Element) ecpResponse.getParentNode();
                    removeAllChildren(soapHeader);

                    // Now on to the PAOS Response
                    Element paosResponse = doc.createElementNS("urn:liberty:paos:2003-08", "paos:Response");
                    paosResponse.setAttribute(soapPrefix + ":mustUnderstand", "1");
                    paosResponse.setAttribute(soapPrefix + ":actor", "http://schemas.xmlsoap.org/soap/actor/next");

                    // messageID is optional
                    if (authnState.getPaosMessageID() != null)
                        paosResponse.setAttribute("refToMessageID", authnState.getPaosMessageID());

                    soapHeader.appendChild(paosResponse);

                    if (authnState.getRelayStateElement() != null) {
                        Node relayState = doc.importNode(authnState.getRelayStateElement(), true);
                        soapHeader.appendChild(relayState);
                    }

                    // Store the modified SOAP Request in the SAML Session
                    String modifiedSOAPResponse = writeDomToString(doc);
                    authnState.setModifiedSOAPResponse(modifiedSOAPResponse);
                    return true;
                }

                logger.debug("responseConsumerURL {} does not match {}", responseConsumerURL,
                        authnState.getResponseConsumerURL());
                Document soapFaultMessage = createSOAPFaultDocument(
                        "AssertionConsumerServiceURL attribute missing or not matching the expected value.");
                Element soapHeader = (Element) soapFaultMessage.getFirstChild().getFirstChild();
                // Now on to the PAOS Response
                Element paosResponse = soapFaultMessage.createElementNS("urn:liberty:paos:2003-08",
                        "paos:Response");
                paosResponse.setAttribute(SOAP_PREFIX + ":mustUnderstand", "1");
                paosResponse.setAttribute(SOAP_PREFIX + ":actor", "http://schemas.xmlsoap.org/soap/actor/next");

                // messageID is optional
                if (authnState.getPaosMessageID() != null) {
                    paosResponse.setAttribute("refToMessageID", authnState.getPaosMessageID());
                }

                soapHeader.appendChild(paosResponse);

                if (authnState.getRelayStateElement() != null) {
                    Node relayState = soapFaultMessage.importNode(authnState.getRelayStateElement(), true);
                    soapHeader.appendChild(relayState);
                }
                // Store the SOAP Fault in the SAML Session
                String modifiedSOAPResponse = writeDomToString(soapFaultMessage);
                authnState.setModifiedSOAPResponse(modifiedSOAPResponse);
                sendSOAPFault(samlSession, authnState);
                return false;

            }

            // There was no response for the ECP.  Look for and propagate an error.
            String errorMessage = getSOAPFaultAsString(is);

            logger.warn("No {} node found in SOAP response. Error: {}", expression, errorMessage);

            if (errorMessage != null) {
                throw new DelegatedAuthenticationRuntimeException(errorMessage);
            }

            return false;
        } catch (XPathExpressionException ex) {
            logger.error("XPath programming error.", ex);
            throw new DelegatedAuthenticationRuntimeException("XPath programming error.", ex);
        } catch (SAXNotRecognizedException ex) {
            logger.error("Exception caught when trying to process the SOAP esponse from the IdP.", ex);
            throw new DelegatedAuthenticationRuntimeException("XPath programming error.", ex);
        } catch (SAXNotSupportedException ex) {
            logger.error("Exception caught when trying to process the SOAP esponse from the IdP.", ex);
            throw new DelegatedAuthenticationRuntimeException(
                    "Exception caught when trying to process the SOAP esponse from the IdP.", ex);
        } catch (SAXException ex) {
            logger.error("Exception caught when trying to process the SOAP esponse from the IdP.", ex);
            throw new DelegatedAuthenticationRuntimeException(
                    "Exception caught when trying to process the SOAP esponse from the IdP.", ex);
        } catch (DOMException ex) {
            logger.error("Exception caught when trying to process the SOAP esponse from the IdP.", ex);
            throw new DelegatedAuthenticationRuntimeException(
                    "Exception caught when trying to process the SOAP esponse from the IdP.", ex);
        } catch (IOException ex) {
            logger.error(
                    "This exception should not ever really occur, as the only I/O this method performs is on a ByteArrayInputStream.",
                    ex);
            throw new DelegatedAuthenticationRuntimeException(
                    "This exception should not ever really occur, as the only I/O this method performs is on a ByteArrayInputStream.",
                    ex);
        } catch (SOAPException ex) {
            logger.error("Error processing a SOAP message.", ex);
            throw new DelegatedAuthenticationRuntimeException("Error processing a SOAP message.", ex);
        }
    }

    /**
     * Utility method for serializing DOM to a String
     * @param doc Document to serialize
     * @return XML document as a String
     */
    private String writeDomToString(Document doc) {
        LSSerializer writer = domLoadSaveImpl.createLSSerializer();
        DOMConfiguration domConfig = writer.getDomConfig();
        domConfig.setParameter("xml-declaration", false);
        String xmlString = writer.writeToString(doc);
        return xmlString;
    }

    /**
     * Despite its name, this method performs two tasks:
     *    1)sending the SOAP response to the WSP is the final step of the delegated
     *      SAML authentication
     *    2)when this succeeds, the WSP returns the resource originally requested,
     *      so this also means that upon return from this method, the DelegatedSAMLAuthenticationState object
     *      will contain a String representation of the requested resource.
     *       
     * @param samlSession   SAML session representing this user and a SAML assertion
     *                      for the WSP on behalf of this user.
     * @param authnState      DelegatedSAMLAuthenticationState object that tracks the state of the authentication
     * @return HttpResponse from the WSP after authentication.  Depending on the HTTP method used, this will
     *                      either include an HTTP 302 redirect to the originally requested resource or a result
     *                      of submitting form data in case if the initial request was from HTTP POST.
     */
    private HttpResponse sendSOAPResponse(SAMLSession samlSession, DelegatedSAMLAuthenticationState authnState) {
        HttpPost method = new HttpPost(authnState.getResponseConsumerURL());
        method.setHeader("Content-Type", SAMLConstants.HTTP_HEADER_PAOS_CONTENT_TYPE);

        try {
            StringEntity postData = new StringEntity(authnState.getModifiedSOAPResponse(), HTTP.UTF_8);
            method.setEntity(postData);

            // Disable redirection
            HttpParams params = method.getParams();
            boolean redirecting = HttpClientParams.isRedirecting(params);
            if (redirecting) {
                HttpClientParams.setRedirecting(params, false);
                method.setParams(params);
            }
            HttpResponse response = samlSession.getHttpClient().execute(method);

            // Not sure whether this is necessary.  Just restoring HttpParams to
            // their original state.
            if (redirecting) {
                HttpClientParams.setRedirecting(params, true);
                method.setParams(params);
            }
            return response;
        } catch (Exception ex) {
            // There is nothing that can be done about this exception other than to log it
            // Exception must be caught and not rethrown to allow normal processing to continue
            logger.error("Exception caught when trying to retrieve the resource.", ex);
            throw new DelegatedAuthenticationRuntimeException(
                    "Exception caught while sending the delegated authentication assertion to the service provider.",
                    ex);
        }
    }

    /**
     * This method sends the SOAP response to the WSP
     * without retrieving the result.  This method assumes that it is merely
     * communicating a failure to the WSP, and the SAMLSession contains the
     * failure message, a SOAP Fault
     * @param samlSession
     * @return true, if successful
     */
    private boolean sendSOAPFault(SAMLSession samlSession, DelegatedSAMLAuthenticationState authnState) {
        HttpPost method = new HttpPost(authnState.getResponseConsumerURL());
        method.setHeader("Content-Type", SAMLConstants.HTTP_HEADER_PAOS_CONTENT_TYPE);
        //method.setHeader("PAOS", SAMLConstants.HTTP_HEADER_PAOS);

        try {
            StringEntity postData = new StringEntity(authnState.getModifiedSOAPResponse(), HTTP.UTF_8);
            method.setEntity(postData);
            HttpResponse response = samlSession.getHttpClient().execute(method);
            response.getStatusLine().getStatusCode();
            return true;
        } catch (Exception ex) {
            // There is nothing that can be done about this exception other than to log it
            // Exception must be caught and not rethrown to allow normal processing to continue
            logger.error("Exception caught when trying to retrieve the resource.", ex);
            throw new DelegatedAuthenticationRuntimeException(
                    "Exception caught while sending the delegated authentication assertion to the service provider.",
                    ex);
        }
    }

    /*
     * Empties the contents of an element
     */
    private void removeAllChildren(Element element) {
        Node child = element.getFirstChild();

        while (child != null) {
            Node next = child.getNextSibling();
            element.removeChild(child);
            child = next;
        }
    }

    /**
     * Assume that the InputStream has a SOAP fault message and return a String
     * suitable to present as an exception message
     *  
     * @param is InputStream that contains a SOAP message
     * @return String containing a formated error message
     * 
     * @throws IOException
     * @throws SOAPException
     */
    private String getSOAPFaultAsString(InputStream is) throws IOException, SOAPException {
        is.reset();
        MessageFactory factory = MessageFactory.newInstance();
        SOAPMessage message = factory.createMessage(null, is);
        SOAPBody body = message.getSOAPBody();

        if (body.hasFault()) {
            SOAPFault fault = body.getFault();
            String code, string, actor;
            code = fault.getFaultCode();
            string = fault.getFaultString();
            actor = fault.getFaultActor();
            String formatedMessage = "SOAP transaction resulted in a SOAP fault.";

            if (code != null)
                formatedMessage += "  Code=\"" + code + ".\"";

            if (string != null)
                formatedMessage += "  String=\"" + string + ".\"";

            if (actor != null)
                formatedMessage += "  Actor=\"" + actor + ".\"";

            return formatedMessage;
        }
        return null;
    }

    private Document createSOAPFaultDocument(String faultString) throws SOAPException {
        MessageFactory factory = MessageFactory.newInstance();
        SOAPMessage message = factory.createMessage();
        SOAPPart sp = message.getSOAPPart();
        SOAPEnvelope se = sp.getEnvelope();
        se.setPrefix(SOAP_PREFIX);
        se.getHeader().detachNode();
        se.addHeader();
        se.getBody().detachNode();
        SOAPBody body = se.addBody();
        SOAPFault fault = body.addFault();
        Name faultCode = se.createName("Client", null, SOAPConstants.URI_NS_SOAP_ENVELOPE);
        fault.setFaultCode(faultCode);
        fault.setFaultString(faultString);
        return se.getOwnerDocument();
    }

    /**
     * Sets up the SSL parameters of a connection to the IdP, including the
     * client certificate and server certificate trust.  The program that set up
     * the SAMLSession object is responsible for providing these optional SSL
     * parameters.
     *  
     * @param client
     * @param samlSession
     * @param authnState 
     * @throws MalformedURLException 
     */
    private void setupIdPClientConnection(HttpClient client, SAMLSession samlSession,
            DelegatedSAMLAuthenticationState authnState) throws MalformedURLException {
        URL url = new URL(authnState.getIdpEndpoint());
        String protocol = url.getProtocol();
        int port = url.getPort();

        // Unless we are using SSL/TLS, there is no need to do the socket factory

        if (protocol.equalsIgnoreCase("https")) {
            SSLSocketFactory socketFactory = samlSession.getIdPSocketFactory();

            if (port == -1)
                port = 443;

            Scheme sch = new Scheme(protocol, socketFactory, port);
            client.getConnectionManager().getSchemeRegistry().unregister(protocol);
            client.getConnectionManager().getSchemeRegistry().register(sch);
        }
    }

}