All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.onelogin.saml2.logout.LogoutResponse Maven / Gradle / Ivy

There is a newer version: 2.9.0
Show newest version
package com.onelogin.saml2.logout;

import java.io.IOException;
import java.net.URL;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import javax.xml.xpath.XPathExpressionException;

import org.apache.commons.lang3.text.StrSubstitutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import com.onelogin.saml2.exception.SettingsException;
import com.onelogin.saml2.exception.ValidationError;
import com.onelogin.saml2.http.HttpRequest;
import com.onelogin.saml2.model.SamlResponseStatus;
import com.onelogin.saml2.settings.Saml2Settings;
import com.onelogin.saml2.util.Constants;
import com.onelogin.saml2.util.SchemaFactory;
import com.onelogin.saml2.util.Util;

/**
 * LogoutResponse class of OneLogin's Java Toolkit.
 *
 * A class that implements SAML 2 Logout Response builder/parser/validator
 */ 
public class LogoutResponse {
	/**
     * Private property to construct a logger for this class.
     */
	private static final Logger LOGGER = LoggerFactory.getLogger(LogoutResponse.class);

	/**
	 * SAML LogoutResponse string
	 */
	private String logoutResponseString;	

	/**
	 * A DOMDocument object loaded from the SAML Response.
	 */
	private Document logoutResponseDocument;

	/**
	 * SAML LogoutResponse ID.
	 */
	private String id;

	/**
     * Settings data.
     */
	private final Saml2Settings settings;

	/**
     * HttpRequest object to be processed (Contains GET and POST parameters, request URL, ...).
     */
	private final HttpRequest request;

	/**
	 * URL of the current host + current view
	 */
	private String currentUrl;

	/**
	 * The inResponseTo attribute of the Logout Request
	 */
	private String inResponseTo;

	/**
	 * Time when the Logout Request was created
	 */
	private Calendar issueInstant;

	/**
	 * After validation, if it fails this property has the cause of the problem
	 */ 
	private String error;

	/**
	 * Constructs the LogoutResponse object.
	 *
	 * @param settings
	 *              OneLogin_Saml2_Settings
	 * @param request
     *              the HttpRequest object to be processed (Contains GET and POST parameters, request URL, ...).
     *
	 */
	public LogoutResponse(Saml2Settings settings, HttpRequest request) {
		this.settings = settings;
		this.request = request;
		
		String samlLogoutResponse = null;
		if (request != null) {
			currentUrl = request.getRequestURL();
			samlLogoutResponse = request.getParameter("SAMLResponse");
		}

		if (samlLogoutResponse != null && !samlLogoutResponse.isEmpty()) {	
			logoutResponseString = Util.base64decodedInflated(samlLogoutResponse);
			logoutResponseDocument = Util.loadXML(logoutResponseString);
		}
	}

	/**
	 * @return the base64 encoded unsigned Logout Response (deflated or not)
	 *
	 * @param deflated 
     *				If deflated or not the encoded Logout Response
     *
	 * @throws IOException 
	 */
	public String getEncodedLogoutResponse(Boolean deflated) throws IOException {
		String encodedLogoutResponse;
		if (deflated == null) {
			deflated = settings.isCompressResponseEnabled();
		}
		if (deflated) {
			encodedLogoutResponse = Util.deflatedBase64encoded(getLogoutResponseXml());
		} else {
			encodedLogoutResponse = Util.base64encoder(getLogoutResponseXml());
		}
		return encodedLogoutResponse;
	}
	
	/**
	 * @return the base64 encoded, unsigned Logout Response (deflated or not)
	 *
	 * @throws IOException 
	 */
	public String getEncodedLogoutResponse() throws IOException {
		return getEncodedLogoutResponse(null);
	}

	/**
	 * @return the plain XML Logout Response
	 */
	public String getLogoutResponseXml() {
		return logoutResponseString;
	}

	/**
	 * @return the ID of the Response
	 */
	public String getId() {
		String idvalue = null;
		if (id != null) {
			idvalue = id;
		} else if (logoutResponseDocument != null) {
			idvalue = logoutResponseDocument.getDocumentElement().getAttributes().getNamedItem("ID").getNodeValue();
		}
		return idvalue;
	}

	 /**
     * Determines if the SAML LogoutResponse is valid
     *
     * @param requestId
     *              The ID of the LogoutRequest sent by this SP to the IdP
     *
     * @return if the SAML LogoutResponse is or not valid
     */
	public Boolean isValid(String requestId) {
		error = null;

		try {
			if (this.logoutResponseDocument == null) {
				throw new ValidationError("SAML Logout Response is not loaded", ValidationError.INVALID_XML_FORMAT);
			}

			if (this.currentUrl == null || this.currentUrl.isEmpty()) {
				throw new Exception("The URL of the current host was not established");
			}

			String signature = request.getParameter("Signature");

			if (settings.isStrict()) {
				Element rootElement = logoutResponseDocument.getDocumentElement();
				rootElement.normalize();				

				if (settings.getWantXMLValidation()) {
					if (!Util.validateXML(this.logoutResponseDocument, SchemaFactory.SAML_SCHEMA_PROTOCOL_2_0)) {
						throw new ValidationError("Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd", ValidationError.INVALID_XML_FORMAT);
					}
				}

				String responseInResponseTo = rootElement.hasAttribute("InResponseTo") ? rootElement.getAttribute("InResponseTo") : null;
				if (requestId == null && responseInResponseTo != null && settings.isRejectUnsolicitedResponsesWithInResponseTo()) {
					throw new ValidationError("The Response has an InResponseTo attribute: " + responseInResponseTo +
							" while no InResponseTo was expected", ValidationError.WRONG_INRESPONSETO);
				}

				// Check if the InResponseTo of the Response matches the ID of the AuthNRequest (requestId) if provided
				if (requestId != null && !Objects.equals(responseInResponseTo, requestId)) {
						throw new ValidationError("The InResponseTo of the Logout Response: " + responseInResponseTo
								+ ", does not match the ID of the Logout request sent by the SP: " + requestId, ValidationError.WRONG_INRESPONSETO);
				}

				// Check issuer
                String issuer = getIssuer();
                if (issuer != null && !issuer.isEmpty() && !issuer.equals(settings.getIdpEntityId())) {
					throw new ValidationError(
							String.format("Invalid issuer in the Logout Response. Was '%s', but expected '%s'" , issuer, settings.getIdpEntityId()),
							ValidationError.WRONG_ISSUER
					);
                }

				// Check destination
				if (rootElement.hasAttribute("Destination")) {
					String destinationUrl = rootElement.getAttribute("Destination");
					if (destinationUrl != null) {
						if (!destinationUrl.isEmpty() && !destinationUrl.equals(currentUrl)) {
							throw new ValidationError("The LogoutResponse was received at " + currentUrl + " instead of "
									+ destinationUrl, ValidationError.WRONG_DESTINATION);
						}
					}
				}

                if (settings.getWantMessagesSigned() && (signature == null || signature.isEmpty())) {
                    throw new ValidationError("The Message of the Logout Response is not signed and the SP requires it", ValidationError.NO_SIGNED_MESSAGE);
                }
			}

			if (signature != null && !signature.isEmpty()) {
				X509Certificate cert = settings.getIdpx509cert();
				if (cert == null) {
					throw new SettingsException("In order to validate the sign on the Logout Response, the x509cert of the IdP is required", SettingsException.CERT_NOT_FOUND);
				}

				List certList = new ArrayList();
				List multipleCertList = settings.getIdpx509certMulti();

				if (multipleCertList != null && multipleCertList.size() != 0) {
					certList.addAll(multipleCertList);
				}

				if (certList.isEmpty() || !certList.contains(cert)) {
					certList.add(0, cert);
				}

				String signAlg = request.getParameter("SigAlg");
				if (signAlg == null || signAlg.isEmpty()) {
					signAlg = Constants.RSA_SHA1;
				}

				String signedQuery = "SAMLResponse=" + request.getEncodedParameter("SAMLResponse");

				String relayState = request.getEncodedParameter("RelayState");
				if (relayState != null && !relayState.isEmpty()) {
					signedQuery += "&RelayState=" + relayState;
				}

				signedQuery += "&SigAlg=" + request.getEncodedParameter("SigAlg", signAlg);

				if (!Util.validateBinarySignature(signedQuery, Util.base64decoder(signature), certList, signAlg)) {
					throw new ValidationError("Signature validation failed. Logout Response rejected", ValidationError.INVALID_SIGNATURE);
				}
			}

			LOGGER.debug("LogoutRequest validated --> " + logoutResponseString);
			return true;
		} catch (Exception e) {
			error = e.getMessage();
			LOGGER.debug("LogoutResponse invalid --> " + logoutResponseString);
			LOGGER.error(error);
			return false;
		}
	}

	public Boolean isValid() {		
		return isValid(null);
	}

	/**
	 * Gets the Issuer from Logout Response.
	 * 
	 * @return the issuer of the logout response
	 *
	 * @throws XPathExpressionException
	 */
    public String getIssuer() throws XPathExpressionException {
    	String issuer = null;
		NodeList issuers = this.query("/samlp:LogoutResponse/saml:Issuer");
		if (issuers.getLength() == 1) {
			issuer = issuers.item(0).getTextContent();
		}    	
        return issuer;
    }

    /**
     * Gets the Status of the Logout Response.
     *
     * @return the Status
     *
     * @throws XPathExpressionException
     */
    public String getStatus() throws XPathExpressionException
    {
    	String statusCode = null;
		NodeList entries = this.query("/samlp:LogoutResponse/samlp:Status/samlp:StatusCode");
		if (entries.getLength() == 1) {
			statusCode = entries.item(0).getAttributes().getNamedItem("Value").getNodeValue();
		}
        return statusCode;
    }

    /**
     * Gets the Status of the Logout Response.
     *
     * @return SamlResponseStatus
     *
     * @throws ValidationError
     */
    public SamlResponseStatus getSamlResponseStatus() throws ValidationError
    {
		String statusXpath = "/samlp:Response/samlp:Status";
		return Util.getStatus(statusXpath, this.logoutResponseDocument);
    }

	/**
     * Extracts nodes that match the query from the DOMDocument (Logout Response Menssage)
     *
     * @param query
     *				Xpath Expression
     *
     * @return DOMNodeList The queried nodes
     */
	private NodeList query (String query) throws XPathExpressionException {
		return Util.query(this.logoutResponseDocument, query, null);
	}

    /**
     * Generates a Logout Response XML string.
     *
     * @param inResponseTo
     *				InResponseTo attribute value to bet set at the Logout Response. 
     */
	public void build(String inResponseTo) {
		id = Util.generateUniqueID(settings.getUniqueIDPrefix());
		issueInstant = Calendar.getInstance();
		this.inResponseTo = inResponseTo;

		StrSubstitutor substitutor = generateSubstitutor(settings);
		this.logoutResponseString = substitutor.replace(getLogoutResponseTemplate());
	}

    /**
     * Generates a Logout Response XML string.
     *
     */
	public void build() {
		build(null);
	}	

	/**
	 * Substitutes LogoutResponse variables within a string by values.
	 *
	 * @param settings
	 * 				Saml2Settings object. Setting data
	 * 
	 * @return the StrSubstitutor object of the LogoutResponse 
	 */
	private StrSubstitutor generateSubstitutor(Saml2Settings settings) {
		Map valueMap = new HashMap();

		valueMap.put("id", id);		

		String issueInstantString = Util.formatDateTime(issueInstant.getTimeInMillis());
		valueMap.put("issueInstant", issueInstantString);

		String destinationStr = "";
		URL slo =  settings.getIdpSingleLogoutServiceResponseUrl();
		if (slo != null) {
			destinationStr = " Destination=\"" + slo.toString() + "\"";
		}
		valueMap.put("destinationStr", destinationStr);

		String inResponseStr = "";
		if (inResponseTo != null) {
			inResponseStr = " InResponseTo=\"" + inResponseTo + "\"";
		}
		valueMap.put("inResponseStr", inResponseStr);		

		valueMap.put("issuer", settings.getSpEntityId());

		return new StrSubstitutor(valueMap);
	}

	/**
	 * @return the LogoutResponse's template
	 */
	private static StringBuilder getLogoutResponseTemplate() {
		StringBuilder template = new StringBuilder();
		template.append("");
		template.append("${issuer}");
		template.append("");
		template.append("");
		template.append("");
		template.append("");
		return template;
	}

	/**
     * After execute a validation process, if fails this method returns the cause
     *
     * @return the cause of the validation error 
     */
	public String getError() {
		return error;
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy