mex.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. /*
  2. * @copyright
  3. * Copyright © Microsoft Open Technologies, Inc.
  4. *
  5. * All Rights Reserved
  6. *
  7. * Licensed under the Apache License, Version 2.0 (the "License");
  8. * you may not use this file except in compliance with the License.
  9. * You may obtain a copy of the License at
  10. *
  11. * http: *www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
  14. * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
  15. * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A
  16. * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT.
  17. *
  18. * See the Apache License, Version 2.0 for the specific language
  19. * governing permissions and limitations under the License.
  20. */
  21. 'use strict';
  22. var request = require('request');
  23. var url = require('url');
  24. var DOMParser = require('xmldom').DOMParser;
  25. var _ = require('underscore');
  26. var Logger = require('./log').Logger;
  27. var util = require('./util');
  28. var xmlutil = require('./xmlutil');
  29. var select = xmlutil.xpathSelect;
  30. var Namespaces = require('./constants').XmlNamespaces;
  31. var WSTrustVersion = require('./constants').WSTrustVersion;
  32. /**
  33. * Create a new Mex object.
  34. * @private
  35. * @constructor
  36. * @param {object} callContext Contains any context information that applies to the request.
  37. * @param {string} url The url of the mex endpoint.
  38. */
  39. function Mex(callContext, url) {
  40. this._log = new Logger('MEX', callContext._logContext);
  41. this._callContext = callContext;
  42. this._url = url;
  43. this._dom = null;
  44. this._mexDoc = null;
  45. this._usernamePasswordPolicy = {};
  46. this._log.verbose('Mex created');
  47. this._log.verbose('Mex created with url: ' + url, true);
  48. }
  49. /**
  50. * Returns the policy containing IDP url and wstrust version from which a username passwowrd can be exchanged for a token.
  51. * @instance
  52. * @memberOf Mex
  53. * @name usernamePasswordPolicy
  54. */
  55. Object.defineProperty(Mex.prototype, 'usernamePasswordPolicy', {
  56. get: function() {
  57. return this._usernamePasswordPolicy;
  58. }
  59. });
  60. /**
  61. * @callback DiscoverCallback
  62. * @memberOf Mex
  63. * @param {object} error
  64. */
  65. /**
  66. * Performs Mex discovery. This method will retrieve the mex document, parse it, and extract
  67. * the username password ws-trust endpoint.
  68. * @private
  69. * @param {Mex.DiscoverCallback} callback Called when discover is complete.
  70. */
  71. Mex.prototype.discover = function (callback) {
  72. this._log.verbose('Retrieving mex');
  73. this._log.verbose('Retrieving mex at: ' + this._url);
  74. var self = this;
  75. var options = util.createRequestOptions(self, { headers : { 'Content-Type' : 'application/soap+xml'} });
  76. request.get(this._url, options, util.createRequestHandler('Mex Get', this._log, callback,
  77. function(response, body) {
  78. try {
  79. self._mexDoc = body;
  80. var options = {
  81. errorHandler : self._log.error
  82. };
  83. self._dom = new DOMParser(options).parseFromString(self._mexDoc);
  84. self._parse(callback);
  85. return;
  86. } catch (err) {
  87. self._log.error('Failed to parse mex response in to DOM', err, true);
  88. callback(err);
  89. }
  90. })
  91. );
  92. };
  93. var TRANSPORT_BINDING_XPATH = 'wsp:ExactlyOne/wsp:All/sp:TransportBinding';
  94. var TRANSPORT_BINDING_2005_XPATH = 'wsp:ExactlyOne/wsp:All/sp2005:TransportBinding';
  95. /**
  96. * Checks a DOM policy node that is a potentialy appplicable username password policy
  97. * to ensure that it has the correct transport.
  98. * @private
  99. * @param {object} policyNode The policy node to check.
  100. * @returns {string} If the policy matches the desired transport then the id of the policy is returned.
  101. * If not then null is returned.
  102. */
  103. Mex.prototype._checkPolicy = function(policyNode) {
  104. var policyId = null;
  105. var id = policyNode.getAttributeNS(Namespaces.wsu, 'Id');
  106. var transportBindingNodes = select(policyNode, TRANSPORT_BINDING_XPATH);
  107. if (0 === transportBindingNodes.length) {
  108. transportBindingNodes = select(policyNode, TRANSPORT_BINDING_2005_XPATH);
  109. }
  110. if (0 !== transportBindingNodes.length) {
  111. if (id) {
  112. policyId = id;
  113. }
  114. }
  115. if (policyId) {
  116. this._log.verbose('found matching policy id');
  117. this._log.verbose('found matching policy id: ' + policyId, true);
  118. } else {
  119. if (!id) {
  120. id = '<no id>';
  121. }
  122. this._log.verbose('potential policy did not match required transport binding');
  123. this._log.verbose('potential policy did not match required transport binding: ' + id, true);
  124. }
  125. return policyId;
  126. };
  127. /**
  128. * Finds all username password policies within the mex document.
  129. * @private
  130. * @param xpath The xpath expression for selecting username token nodes.
  131. * @returns {object} A map object that contains objects containing the id of username password polices.
  132. */
  133. Mex.prototype._selectUsernamePasswordPolicies = function(xpath) {
  134. var policies = {};
  135. var usernameTokenNodes = select(this._dom, xpath);
  136. if (!usernameTokenNodes.length) {
  137. this._log.warn('no username token policy nodes found');
  138. return;
  139. }
  140. for (var i=0; i < usernameTokenNodes.length; i++) {
  141. var policyNode = usernameTokenNodes[i].parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode;
  142. var id = this._checkPolicy(policyNode);
  143. if (id) {
  144. var idRef = '#' + id;
  145. policies[idRef] = { id : idRef };
  146. }
  147. }
  148. return _.isEmpty(policies) ? null : policies;
  149. };
  150. var SOAP_ACTION_XPATH = 'wsdl:operation/soap12:operation/@soapAction';
  151. var RST_SOAP_ACTION_13 = 'http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue';
  152. var RST_SOAP_ACTION_2005 = 'http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue';
  153. var SOAP_TRANSPORT_XPATH = 'soap12:binding/@transport';
  154. var SOAP_HTTP_TRANSPORT_VALUE = 'http://schemas.xmlsoap.org/soap/http';
  155. /**
  156. * Given a DOM binding node determines whether it matches the correct soap action and transport.
  157. * @private
  158. * @param {object} bindingNode The DOM node to check.
  159. * @returns {bool}
  160. */
  161. Mex.prototype._checkSoapActionAndTransport = function(bindingNode) {
  162. var soapTransportAttributes;
  163. var soapAction;
  164. var soapTransport;
  165. var bindingName = bindingNode.getAttribute('name');
  166. var soapActionAttributes = select(bindingNode, SOAP_ACTION_XPATH);
  167. if (soapActionAttributes.length) {
  168. soapAction = soapActionAttributes[0].value;
  169. soapTransportAttributes = select(bindingNode, SOAP_TRANSPORT_XPATH);
  170. }
  171. if (soapTransportAttributes.length) {
  172. soapTransport = soapTransportAttributes[0].value;
  173. }
  174. if (soapTransport === SOAP_HTTP_TRANSPORT_VALUE) {
  175. if (soapAction === RST_SOAP_ACTION_13) {
  176. this._log.verbose('foud binding matching Action and Transport: ' + bindingName);
  177. return WSTrustVersion.WSTRUST13;
  178. }
  179. else if (soapAction === RST_SOAP_ACTION_2005) {
  180. this._log.verbose('found binding matching Action and Transport: ' + bindingName);
  181. return WSTrustVersion.WSTRUST2005;
  182. }
  183. }
  184. this._log.verbose('binding node did not match soap Action or Transport: ' + bindingName);
  185. return WSTrustVersion.UNDEFINED;
  186. };
  187. /**
  188. * Given a map with policy id keys, finds the bindings in the mex document that are linked to thos policies.
  189. * @private
  190. * @param {object} policies A map with policy id keys.
  191. * @returns {object} a map of bindings id's to policy id's.
  192. */
  193. Mex.prototype._getMatchingBindings = function(policies) {
  194. var bindings = {};
  195. var bindingPolicyRefNodes = select(this._dom, '//wsdl:definitions/wsdl:binding/wsp:PolicyReference');
  196. for (var i=0; i < bindingPolicyRefNodes.length; i++) {
  197. var node = bindingPolicyRefNodes[i];
  198. var uri = node.getAttribute('URI');
  199. var policy = policies[uri];
  200. if (policy) {
  201. var bindingNode = node.parentNode;
  202. var bindingName = bindingNode.getAttribute('name');
  203. var version = this._checkSoapActionAndTransport(bindingNode);
  204. if (version !== WSTrustVersion.UNDEFINED) {
  205. var bindingPolicy = {};
  206. bindingPolicy.url = uri;
  207. bindingPolicy.version = version;
  208. bindings[bindingName] = bindingPolicy;
  209. }
  210. }
  211. }
  212. return _.isEmpty(bindings) ? null : bindings;
  213. };
  214. /**
  215. * Ensures that a url points to an SSL endpoint.
  216. * @private
  217. * @param {string} endpointUrl The url to check.
  218. * @returns {bool}
  219. */
  220. Mex.prototype._urlIsSecure = function(endpointUrl) {
  221. var parsedUrl = url.parse(endpointUrl);
  222. return parsedUrl.protocol === 'https:';
  223. };
  224. var PORT_XPATH = '//wsdl:definitions/wsdl:service/wsdl:port';
  225. var ADDRESS_XPATH = 'wsa10:EndpointReference/wsa10:Address';
  226. /**
  227. * Finds all of the wsdl ports in the mex document that are associated with username password policies. Augments
  228. * the passed in bindings with the endpoint url of the correct port.
  229. * @private
  230. * @param {object} bindings A map of binding id's to policy id's.
  231. */
  232. Mex.prototype._getPortsForPolicyBindings = function(bindings, policies) {
  233. var portNodes = select(this._dom, PORT_XPATH);
  234. if (0 === portNodes.length) {
  235. this._log.warning('no ports found');
  236. }
  237. for (var i=0; i < portNodes.length; i++) {
  238. var portNode = portNodes[i];
  239. var bindingId = portNode.getAttribute('binding');
  240. // Clear any prefix
  241. var bindingIdParts = bindingId.split(':');
  242. bindingId = bindingIdParts[bindingIdParts.length - 1];
  243. var trustPolicy = bindings[bindingId];
  244. if (trustPolicy) {
  245. var bindingPolicy = policies[trustPolicy.url];
  246. if (bindingPolicy && !bindingPolicy.url) {
  247. bindingPolicy.version = trustPolicy.version;
  248. var addressNode = select(portNode, ADDRESS_XPATH);
  249. if (0 === addressNode) {
  250. throw this._log.createError('no address nodes on port.');
  251. }
  252. var address = xmlutil.findElementText(addressNode[0]);
  253. if (this._urlIsSecure(address)) {
  254. bindingPolicy.url = address;
  255. } else {
  256. this._log.warn('skipping insecure endpoint: ' + address);
  257. }
  258. }
  259. }
  260. }
  261. };
  262. /**
  263. * Given a list of username password policies chooses one of them at random as the policy chosen by this Mex instance.
  264. * @private
  265. * @param {object} policies A map of policy id's to an object containing username password ws-trust endpoint addresses.
  266. */
  267. Mex.prototype._selectSingleMatchingPolicy = function(policies) {
  268. // if both wstrust13 and wstrust2005 policy exists, then choose wstrust13, otherwise choose whatever exists.
  269. var matchingPolicies = _.filter(policies, function(policy) { return policy.url ? true : false; });
  270. if (!matchingPolicies) {
  271. this._log.warn('no policies found with an url');
  272. return;
  273. }
  274. var wstrust13Policy = null, wstrust2005Policy = null;
  275. for(var i = 0; i < matchingPolicies.length; ++i) {
  276. var matchingPolicy = matchingPolicies[i];
  277. if (WSTrustVersion.WSTRUST13 === matchingPolicy.version) {
  278. wstrust13Policy = matchingPolicy;
  279. }
  280. else if (WSTrustVersion.WSTRUST2005 === matchingPolicy.version) {
  281. wstrust2005Policy = matchingPolicy;
  282. }
  283. }
  284. if (!wstrust13Policy && !wstrust2005Policy) {
  285. this._log.warn('no policies found with an url');
  286. this._usernamePasswordPolicy = null;
  287. return;
  288. }
  289. this._usernamePasswordPolicy = wstrust13Policy ? wstrust13Policy : wstrust2005Policy;
  290. };
  291. /**
  292. * Parses the mex document previously retrieved.
  293. * @private
  294. * @param {Mex.DiscoverCallback} callback
  295. */
  296. Mex.prototype._parse = function(callback) {
  297. var self = this;
  298. var xpathExpression = '//wsdl:definitions/wsp:Policy/wsp:ExactlyOne/wsp:All/sp:SignedEncryptedSupportingTokens/wsp:Policy/sp:UsernameToken/wsp:Policy/sp:WssUsernameToken10';
  299. var policies = self._selectUsernamePasswordPolicies(xpathExpression);
  300. xpathExpression = '//wsdl:definitions/wsp:Policy/wsp:ExactlyOne/wsp:All/sp2005:SignedSupportingTokens/wsp:Policy/sp2005:UsernameToken/wsp:Policy/sp2005:WssUsernameToken10';
  301. if (policies) {
  302. _.extend(policies, self._selectUsernamePasswordPolicies(xpathExpression));
  303. }
  304. else {
  305. policies = self._selectUsernamePasswordPolicies(xpathExpression);
  306. }
  307. if (!policies) {
  308. callback(self._log.createError('No matching policies'));
  309. return;
  310. }
  311. var bindings = self._getMatchingBindings(policies);
  312. if (!bindings) {
  313. callback(self._log.createError('No matching bindings'));
  314. return;
  315. }
  316. self._getPortsForPolicyBindings(bindings, policies);
  317. self._selectSingleMatchingPolicy(policies);
  318. var err = this._url ? undefined : this._log.createError('No ws-trust endpoints match requirements.');
  319. callback(err);
  320. };
  321. module.exports = Mex;