package com.onelogin.saml2.logout; import java.io.IOException; import java.net.URL; import java.security.cert.X509Certificate; import java.util.Calendar; import java.util.HashMap; 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.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); } 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), cert, 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; } /** * 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(); 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<String, String> valueMap = new HashMap<String, String>(); 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("<samlp:LogoutResponse xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" "); template.append("ID=\"${id}\" "); template.append("Version=\"2.0\" "); template.append("IssueInstant=\"${issueInstant}\"${destinationStr}${inResponseStr} >"); template.append("<saml:Issuer>${issuer}</saml:Issuer>"); template.append("<samlp:Status>"); template.append("<samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\" />"); template.append("</samlp:Status>"); template.append("</samlp:LogoutResponse>"); 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; } }