// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. package com.microsoft.aad.msal4j; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import java.io.ByteArrayInputStream; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.Map.Entry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; class MexParser { private final static Logger log = LoggerFactory.getLogger(MexParser.class); private static final String TRANSPORT_BINDING_XPATH = "wsp:ExactlyOne/wsp:All/sp:TransportBinding"; private static final String TRANSPORT_BINDING_2005_XPATH = "wsp:ExactlyOne/wsp:All/sp2005:TransportBinding"; private static final String PORT_XPATH = "//wsdl:definitions/wsdl:service/wsdl:port"; private static final String ADDRESS_XPATH = "wsa10:EndpointReference/wsa10:Address"; private static final String SOAP_ACTION_XPATH = "wsdl:operation/soap12:operation/@soapAction"; private static final String RST_SOAP_ACTION = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue"; private static final String RST_SOAP_ACTION_2005 = "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue"; private static final String SOAP_TRANSPORT_XPATH = "soap12:binding/@transport"; private static final String SOAP_HTTP_TRANSPORT_VALUE = "http://schemas.xmlsoap.org/soap/http"; private interface PolicySelector { Map selectPolicies(Document document, XPath xPath, boolean logPii) throws XPathExpressionException; } private static class NegotiateAuthenticationPolicySelector implements PolicySelector { public Map selectPolicies(Document xmlDocument, XPath xPath, boolean logPii) throws XPathExpressionException { String xpathExpression = "//wsdl:definitions/wsp:Policy/wsp:ExactlyOne/wsp:All/http:NegotiateAuthentication"; return selectIntegratedPoliciesWithExpression(xmlDocument, xPath, xpathExpression); } } private static class WsTrustEndpointPolicySelector implements PolicySelector { public Map selectPolicies(Document xmlDocument, XPath xPath, boolean logPii) throws XPathExpressionException { String xpathExpression = "//wsdl:definitions/wsp:Policy/wsp:ExactlyOne/wsp:All/" + "sp:SignedEncryptedSupportingTokens/wsp:Policy/sp:UsernameToken/" + "wsp:Policy/sp:WssUsernameToken10"; Map policies = selectUsernamePasswordPoliciesWithExpression( xmlDocument, xPath, xpathExpression, logPii); ((NamespaceContextImpl) xPath.getNamespaceContext()).modifyNameSpace("sp", "http://schemas.xmlsoap.org/ws/2005/07/securitypolicy"); xpathExpression = "//wsdl:definitions/wsp:Policy/wsp:ExactlyOne/wsp:All/" + "sp:SignedSupportingTokens/wsp:Policy/sp:UsernameToken/" + "wsp:Policy/sp:WssUsernameToken10"; policies.putAll(selectUsernamePasswordPoliciesWithExpression( xmlDocument, xPath, xpathExpression, logPii)); return policies; } } static BindingPolicy getPolicy(String mexResponse, PolicySelector policySelector, boolean logPii) throws Exception { DocumentBuilderFactory builderFactory = SafeDocumentBuilderFactory.createInstance(); builderFactory.setNamespaceAware(true); DocumentBuilder builder = builderFactory.newDocumentBuilder(); Document xmlDocument = builder.parse(new ByteArrayInputStream( mexResponse.getBytes(StandardCharsets.UTF_8))); XPath xPath = XPathFactory.newInstance().newXPath(); NamespaceContextImpl nameSpace = new NamespaceContextImpl(); xPath.setNamespaceContext(nameSpace); Map policies = policySelector.selectPolicies(xmlDocument, xPath, logPii); if (policies.isEmpty()) { log.debug("No matching policies"); return null; } else { Map bindings = getMatchingBindings( xmlDocument, xPath, policies, logPii); if (bindings.isEmpty()) { log.debug("No matching bindings"); return null; } else { getPortsForPolicyBindings(xmlDocument, xPath, bindings, policies, logPii); return selectSingleMatchingPolicy(policies); } } } static BindingPolicy getPolicyFromMexResponseForIntegrated(String mexResponse, boolean logPii) throws Exception { return getPolicy(mexResponse, new NegotiateAuthenticationPolicySelector(), logPii); } static BindingPolicy getWsTrustEndpointFromMexResponse(String mexResponse, boolean logPii) throws Exception { return getPolicy(mexResponse, new WsTrustEndpointPolicySelector(), logPii); } private static BindingPolicy selectSingleMatchingPolicy( Map policies) { BindingPolicy wstrust13 = null; BindingPolicy wstrust2005 = null; // Select wstrust13 first if wstrust13 available Iterator> it = policies.entrySet() .iterator(); while (it.hasNext()) { Map.Entry pair = it.next(); if (pair.getValue().getUrl() != null) { if (pair.getValue().getVersion() == WSTrustVersion.WSTRUST13) { wstrust13 = pair.getValue(); } else if (pair.getValue().getVersion() == WSTrustVersion.WSTRUST2005) { wstrust2005 = pair.getValue(); } } } if (wstrust13 == null && wstrust2005 == null) { log.warn("No policies found with the url"); return null; } return wstrust13 != null ? wstrust13 : wstrust2005; } private static void getPortsForPolicyBindings(Document xmlDocument, XPath xPath, Map bindings, Map policies, boolean logPii) throws Exception { NodeList portNodes = (NodeList) xPath.compile(PORT_XPATH).evaluate( xmlDocument, XPathConstants.NODESET); if (portNodes.getLength() == 0) { log.warn("No ports found"); } else { for (int i = 0; i < portNodes.getLength(); i++) { Node portNode = portNodes.item(i); String bindingId = portNode.getAttributes() .getNamedItem("binding").getNodeValue(); String[] bindingIdParts = bindingId.split(":"); bindingId = bindingIdParts[bindingIdParts.length - 1]; BindingPolicy trustPolicy = bindings.get(bindingId); if (trustPolicy != null) { BindingPolicy bindingPolicy = policies.get(trustPolicy .getUrl()); if (bindingPolicy != null && StringHelper.isBlank(bindingPolicy.getUrl())) { bindingPolicy.setVersion(trustPolicy.getVersion()); NodeList addressNodes = (NodeList) xPath.compile( ADDRESS_XPATH).evaluate(portNode, XPathConstants.NODESET); if (addressNodes.getLength() > 0) { String address = addressNodes.item(0) .getTextContent(); if (address != null && address.toLowerCase().startsWith( "https://")) { bindingPolicy.setUrl(address.trim()); } else { if (logPii) { log.warn("Skipping insecure endpoint" + ": " + address); } else { log.warn("Skipping insecure endpoint"); } } } else { throw new MsalClientException( "Error parsing WSTrustResponse: No address nodes on port", AuthenticationErrorCode.WSTRUST_INVALID_RESPONSE); } } } } } } private static Map getMatchingBindings( Document xmlDocument, XPath xPath, Map policies, boolean logPii) throws XPathExpressionException { Map bindings = new HashMap<>(); NodeList nodeList = (NodeList) xPath.compile( "//wsdl:definitions/wsdl:binding/wsp:PolicyReference") .evaluate(xmlDocument, XPathConstants.NODESET); for (int i = 0; i < nodeList.getLength(); i++) { Node node = nodeList.item(i); String uri = node.getAttributes().getNamedItem("URI") .getNodeValue(); if (policies.containsKey(uri)) { Node bindingNode = node.getParentNode(); String bindingName = bindingNode.getAttributes() .getNamedItem("name").getNodeValue(); WSTrustVersion version = checkSoapActionAndTransport(xPath, bindingNode, logPii); if (version != WSTrustVersion.UNDEFINED) { BindingPolicy policy = new BindingPolicy(""); policy.setUrl(uri); policy.setVersion(version); bindings.put(bindingName, policy); } } } return bindings; } private static WSTrustVersion checkSoapActionAndTransport(XPath xPath, Node bindingNode, boolean logPii) throws XPathExpressionException { NodeList soapTransportAttributes = null; String soapAction = null; String bindingName = bindingNode.getAttributes().getNamedItem("name") .getNodeValue(); NodeList soapActionAttributes = (NodeList) xPath.compile( SOAP_ACTION_XPATH) .evaluate(bindingNode, XPathConstants.NODESET); if (soapActionAttributes.getLength() > 0) { soapAction = soapActionAttributes.item(0).getNodeValue(); soapTransportAttributes = (NodeList) xPath.compile( SOAP_TRANSPORT_XPATH).evaluate(bindingNode, XPathConstants.NODESET); if (soapTransportAttributes != null && soapTransportAttributes.getLength() > 0 && soapTransportAttributes.item(0).getNodeValue() .equalsIgnoreCase(SOAP_HTTP_TRANSPORT_VALUE)) { if (soapAction.equalsIgnoreCase(RST_SOAP_ACTION)) { if (logPii) { log.debug("Found binding matching Action and Transport: " + bindingName); } else { log.debug("Found binding matching Action and Transport"); } return WSTrustVersion.WSTRUST13; } else if (soapAction.equalsIgnoreCase(RST_SOAP_ACTION_2005)) { if (logPii) { log.debug("Binding node did not match soap Action or Transport: " + bindingName); } else { log.debug("Binding node did not match soap Action or Transport"); } return WSTrustVersion.WSTRUST2005; } } } return WSTrustVersion.UNDEFINED; } private static Map selectUsernamePasswordPoliciesWithExpression( Document xmlDocument, XPath xPath, String xpathExpression, boolean logPii) throws XPathExpressionException { Map policies = new HashMap<>(); NodeList nodeList = (NodeList) xPath.compile(xpathExpression).evaluate( xmlDocument, XPathConstants.NODESET); for (int i = 0; i < nodeList.getLength(); i++) { // Nodes // sp:WssUsernameToken10 // wsp:Policy // sp:UsernameToken // wsp:Policy // sp:SignedEncryptedSupportingTokens // wsp:All // wsp:ExactlyOne // wsp:Policy String policy = checkPolicy(xPath, nodeList.item(i).getParentNode() .getParentNode().getParentNode().getParentNode() .getParentNode().getParentNode().getParentNode(), logPii); policies.put("#" + policy, new BindingPolicy("#" + policy)); } return policies; } private static Map selectIntegratedPoliciesWithExpression(Document xmlDocument, XPath xPath, String xpathExpression) throws XPathExpressionException { Map policies = new HashMap<>(); NodeList nodeList = (NodeList) xPath.compile(xpathExpression).evaluate(xmlDocument, XPathConstants.NODESET); for (int i = 0; i < nodeList.getLength(); i++) { // get back to //wsdl:definitions/wsp:Policy String policy = checkPolicyIntegrated(xPath, nodeList.item(i).getParentNode().getParentNode().getParentNode()); policies.put("#" + policy, new BindingPolicy("#" + policy)); } return policies; } private static String checkPolicy(XPath xPath, Node node, boolean logPii) throws XPathExpressionException { String policyId = null; Node id = node.getAttributes().getNamedItem("wsu:Id"); NodeList transportBindingNodes = (NodeList) xPath.compile( TRANSPORT_BINDING_XPATH).evaluate(node, XPathConstants.NODESET); if (transportBindingNodes.getLength() == 0) { transportBindingNodes = (NodeList) xPath.compile( TRANSPORT_BINDING_2005_XPATH).evaluate(node, XPathConstants.NODESET); } if (transportBindingNodes.getLength() > 0 && id != null) { policyId = id.getNodeValue(); if (logPii) { log.debug("found matching policy id: " + policyId); } else { log.debug("found matching policy"); } } else { String nodeValue = "none"; if (id != null) { nodeValue = id.getNodeValue(); } if (logPii) { log.debug("potential policy did not match required transport binding: " + nodeValue); } else { log.debug("potential policy did not match required transport binding"); } } return policyId; } private static String checkPolicyIntegrated(XPath xPath, Node node) throws XPathExpressionException { Node id = node.getAttributes().getNamedItem("wsu:Id"); String policyId = id.getNodeValue(); return policyId; } }