authority.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  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 _ = require('underscore');
  25. var AADConstants = require('./constants').AADConstants;
  26. var Logger = require('./log').Logger;
  27. var util = require('./util');
  28. /**
  29. * Constructs an Authority object with a specific authority URL.
  30. * @private
  31. * @constructor
  32. * @param {string} authorityUrl A URL that identifies a token authority.
  33. * @param {bool} validateAuthority Indicates whether the Authority url should be validated as an actual AAD
  34. * authority. The default is true.
  35. */
  36. function Authority(authorityUrl, validateAuthority) {
  37. this._log = null;
  38. this._url = url.parse(authorityUrl);
  39. this._validateAuthorityUrl();
  40. this._validated = !validateAuthority;
  41. this._host = null;
  42. this._tenant = null;
  43. this._parseAuthority();
  44. this._authorizationEndpoint = null;
  45. this._tokenEndpoint = null;
  46. this._deviceCodeEndpoint = null;
  47. this._isAdfsAuthority = (this._tenant.toLowerCase() === "adfs");
  48. }
  49. /**
  50. * The URL of the authority
  51. * @instance
  52. * @type {string}
  53. * @memberOf Authority
  54. * @name url
  55. */
  56. Object.defineProperty(Authority.prototype, 'url', {
  57. get: function() {
  58. return url.format(this._url);
  59. }
  60. });
  61. /**
  62. * The token endpoint that the authority uses as discovered by instance discovery.
  63. * @instance
  64. * @type {string}
  65. * @memberOf Authority
  66. * @name tokenEndpoint
  67. */
  68. Object.defineProperty(Authority.prototype, 'tokenEndpoint', {
  69. get: function() {
  70. return this._tokenEndpoint;
  71. }
  72. });
  73. Object.defineProperty(Authority.prototype, 'deviceCodeEndpoint', {
  74. get: function() {
  75. return this._deviceCodeEndpoint;
  76. }
  77. });
  78. /**
  79. * Checks the authority url to ensure that it meets basic requirements such as being over SSL. If it does not then
  80. * this method will throw if any of the checks fail.
  81. * @private
  82. * @throws {Error} If the authority url fails to pass any validation checks.
  83. */
  84. Authority.prototype._validateAuthorityUrl = function() {
  85. if (this._url.protocol !== 'https:') {
  86. throw new Error('The authority url must be an https endpoint.');
  87. }
  88. if (this._url.query) {
  89. throw new Error('The authority url must not have a query string.');
  90. }
  91. };
  92. /**
  93. * Parse the authority to get the tenant name. The rest of the
  94. * URL is thrown away in favor of one of the endpoints from the validation doc.
  95. * @private
  96. */
  97. Authority.prototype._parseAuthority = function() {
  98. this._host = this._url.host;
  99. var pathParts = this._url.pathname.split('/');
  100. this._tenant = pathParts[1];
  101. if (!this._tenant) {
  102. throw new Error('Could not determine tenant.');
  103. }
  104. };
  105. /**
  106. * Performs instance discovery based on a simple match against well known authorities.
  107. * @private
  108. * @return {bool} Returns true if the authority is recognized.
  109. */
  110. Authority.prototype._performStaticInstanceDiscovery = function() {
  111. this._log.verbose('Performing static instance discovery');
  112. var hostIndex = _.indexOf(AADConstants.WELL_KNOWN_AUTHORITY_HOSTS, this._url.hostname);
  113. var found = hostIndex > -1;
  114. if (found) {
  115. this._log.verbose('Authority validated via static instance discovery.');
  116. }
  117. return found;
  118. };
  119. Authority.prototype._createAuthorityUrl = function() {
  120. return 'https://' + this._url.host + '/' + encodeURIComponent(this._tenant) + AADConstants.AUTHORIZE_ENDPOINT_PATH;
  121. };
  122. /**
  123. * Creates an instance discovery endpoint url for the specific authority that this object represents.
  124. * @private
  125. * @param {string} authorityHost The host name of a well known authority.
  126. * @return {URL} The constructed endpoint url.
  127. */
  128. Authority.prototype._createInstanceDiscoveryEndpointFromTemplate = function(authorityHost) {
  129. var discoveryEndpoint = AADConstants.INSTANCE_DISCOVERY_ENDPOINT_TEMPLATE;
  130. discoveryEndpoint = discoveryEndpoint.replace('{authorize_host}', authorityHost);
  131. discoveryEndpoint = discoveryEndpoint.replace('{authorize_endpoint}', encodeURIComponent(this._createAuthorityUrl()));
  132. return url.parse(discoveryEndpoint);
  133. };
  134. /**
  135. * Performs instance discovery via a network call to well known authorities.
  136. * @private
  137. * @param {Authority.InstanceDiscoveryCallback} callback The callback function. If succesful,
  138. * this function calls the callback with the
  139. * tenantDiscoveryEndpoint returned by the
  140. * server.
  141. */
  142. Authority.prototype._performDynamicInstanceDiscovery = function(callback) {
  143. try {
  144. var self = this;
  145. var discoveryEndpoint = this._createInstanceDiscoveryEndpointFromTemplate(AADConstants.WORLD_WIDE_AUTHORITY);
  146. var getOptions = util.createRequestOptions(self);
  147. this._log.verbose('Attempting instance discover');
  148. this._log.verbose('Attempting instance discover at: ' + url.format(discoveryEndpoint), true);
  149. request.get(discoveryEndpoint, getOptions, util.createRequestHandler('Instance Discovery', this._log, callback,
  150. function(response, body) {
  151. var discoveryResponse = JSON.parse(body);
  152. if (discoveryResponse['tenant_discovery_endpoint']) {
  153. callback(null, discoveryResponse['tenant_discovery_endpoint']);
  154. } else {
  155. callback(self._log.createError('Failed to parse instance discovery response'));
  156. }
  157. })
  158. );
  159. } catch(e) {
  160. callback(e);
  161. }
  162. };
  163. /**
  164. * @callback InstanceDiscoveryCallback
  165. * @private
  166. * @memberOf Authority
  167. * @param {Error} err If an error occurs during instance discovery then it will be returned here.
  168. * @param {string} tenantDiscoveryEndpoint If instance discovery is successful then this will contain the
  169. * tenantDiscoveryEndpoint associated with the authority.
  170. */
  171. /**
  172. * Determines whether the authority is recognized as a trusted AAD authority.
  173. * @private
  174. * @param {Authority.InstanceDiscoveryCallback} callback The callback function.
  175. */
  176. Authority.prototype._validateViaInstanceDiscovery = function(callback) {
  177. if (this._performStaticInstanceDiscovery()) {
  178. callback();
  179. } else {
  180. this._performDynamicInstanceDiscovery(callback);
  181. }
  182. };
  183. /**
  184. * @callback GetOauthEndpointsCallback
  185. * @private
  186. * @memberOf Authority
  187. * @param {Error} error An error if one occurred.
  188. */
  189. /**
  190. * Given a tenant discovery endpoint this method will attempt to discover the token endpoint. If the
  191. * tenant discovery endpoint is unreachable for some reason then it will fall back to a algorithmic generation of the
  192. * token endpoint url.
  193. * @private
  194. * @param {string} tenantDiscoveryEndpoint The url of the tenant discovery endpoint for this authority.
  195. * @param {Authority.GetOauthEndpointsCallback} callback The callback function.
  196. */
  197. Authority.prototype._getOAuthEndpoints = function(tenantDiscoveryEndpoint, callback) {
  198. if (this._tokenEndpoint && this._deviceCodeEndpoint) {
  199. callback();
  200. return;
  201. } else {
  202. // fallback to the well known token endpoint path.
  203. if (!this._tokenEndpoint){
  204. this._tokenEndpoint = url.format('https://' + this._url.host + '/' + encodeURIComponent(this._tenant)) + AADConstants.TOKEN_ENDPOINT_PATH;
  205. }
  206. if (!this._deviceCodeEndpoint){
  207. this._deviceCodeEndpoint = url.format('https://' + this._url.host + '/' + encodeURIComponent(this._tenant)) + AADConstants.DEVICE_ENDPOINT_PATH;
  208. }
  209. callback();
  210. return;
  211. }
  212. };
  213. /**
  214. * @callback ValidateCallback
  215. * @memberOf Authority
  216. */
  217. /**
  218. * Perform validation on the authority represented by this object. In addition to simple validation
  219. * the oauth token endpoint will be retrieved.
  220. * @param {Authority.ValidateCallback} callback The callback function.
  221. */
  222. Authority.prototype.validate = function(callContext, callback) {
  223. this._log = new Logger('Authority', callContext._logContext);
  224. this._callContext = callContext;
  225. var self = this;
  226. if (!this._validated) {
  227. this._log.verbose('Performing instance discovery');
  228. this._log.verbose('Performing instance discovery: ' + url.format(this._url), true);
  229. this._validateViaInstanceDiscovery(function(err, tenantDiscoveryEndpoint) {
  230. if (err)
  231. {
  232. callback(err);
  233. } else {
  234. self._validated = true;
  235. self._getOAuthEndpoints(tenantDiscoveryEndpoint, callback);
  236. return;
  237. }
  238. });
  239. } else {
  240. this._log.verbose('Instance discovery/validation has either already been completed or is turned off');
  241. this._log.verbose('Instance discovery/validation has either already been completed or is turned off: ' + url.format(this._url), true);
  242. this._getOAuthEndpoints(null, callback);
  243. return;
  244. }
  245. };
  246. module.exports.Authority = Authority;