/* * @copyright * Copyright © Microsoft Open Technologies, Inc. * * All Rights Reserved * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http: *www.apache.org/licenses/LICENSE-2.0 * * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. * * See the Apache License, Version 2.0 for the specific language * governing permissions and limitations under the License. */ 'use strict'; var request = require('request'); var url = require('url'); var _ = require('underscore'); var AADConstants = require('./constants').AADConstants; var Logger = require('./log').Logger; var util = require('./util'); /** * Constructs an Authority object with a specific authority URL. * @private * @constructor * @param {string} authorityUrl A URL that identifies a token authority. * @param {bool} validateAuthority Indicates whether the Authority url should be validated as an actual AAD * authority. The default is true. */ function Authority(authorityUrl, validateAuthority) { this._log = null; this._url = url.parse(authorityUrl); this._validateAuthorityUrl(); this._validated = !validateAuthority; this._host = null; this._tenant = null; this._parseAuthority(); this._authorizationEndpoint = null; this._tokenEndpoint = null; this._deviceCodeEndpoint = null; this._isAdfsAuthority = (this._tenant.toLowerCase() === "adfs"); } /** * The URL of the authority * @instance * @type {string} * @memberOf Authority * @name url */ Object.defineProperty(Authority.prototype, 'url', { get: function() { return url.format(this._url); } }); /** * The token endpoint that the authority uses as discovered by instance discovery. * @instance * @type {string} * @memberOf Authority * @name tokenEndpoint */ Object.defineProperty(Authority.prototype, 'tokenEndpoint', { get: function() { return this._tokenEndpoint; } }); Object.defineProperty(Authority.prototype, 'deviceCodeEndpoint', { get: function() { return this._deviceCodeEndpoint; } }); /** * Checks the authority url to ensure that it meets basic requirements such as being over SSL. If it does not then * this method will throw if any of the checks fail. * @private * @throws {Error} If the authority url fails to pass any validation checks. */ Authority.prototype._validateAuthorityUrl = function() { if (this._url.protocol !== 'https:') { throw new Error('The authority url must be an https endpoint.'); } if (this._url.query) { throw new Error('The authority url must not have a query string.'); } }; /** * Parse the authority to get the tenant name. The rest of the * URL is thrown away in favor of one of the endpoints from the validation doc. * @private */ Authority.prototype._parseAuthority = function() { this._host = this._url.host; var pathParts = this._url.pathname.split('/'); this._tenant = pathParts[1]; if (!this._tenant) { throw new Error('Could not determine tenant.'); } }; /** * Performs instance discovery based on a simple match against well known authorities. * @private * @return {bool} Returns true if the authority is recognized. */ Authority.prototype._performStaticInstanceDiscovery = function() { this._log.verbose('Performing static instance discovery'); var hostIndex = _.indexOf(AADConstants.WELL_KNOWN_AUTHORITY_HOSTS, this._url.hostname); var found = hostIndex > -1; if (found) { this._log.verbose('Authority validated via static instance discovery.'); } return found; }; Authority.prototype._createAuthorityUrl = function() { return 'https://' + this._url.host + '/' + encodeURIComponent(this._tenant) + AADConstants.AUTHORIZE_ENDPOINT_PATH; }; /** * Creates an instance discovery endpoint url for the specific authority that this object represents. * @private * @param {string} authorityHost The host name of a well known authority. * @return {URL} The constructed endpoint url. */ Authority.prototype._createInstanceDiscoveryEndpointFromTemplate = function(authorityHost) { var discoveryEndpoint = AADConstants.INSTANCE_DISCOVERY_ENDPOINT_TEMPLATE; discoveryEndpoint = discoveryEndpoint.replace('{authorize_host}', authorityHost); discoveryEndpoint = discoveryEndpoint.replace('{authorize_endpoint}', encodeURIComponent(this._createAuthorityUrl())); return url.parse(discoveryEndpoint); }; /** * Performs instance discovery via a network call to well known authorities. * @private * @param {Authority.InstanceDiscoveryCallback} callback The callback function. If succesful, * this function calls the callback with the * tenantDiscoveryEndpoint returned by the * server. */ Authority.prototype._performDynamicInstanceDiscovery = function(callback) { try { var self = this; var discoveryEndpoint = this._createInstanceDiscoveryEndpointFromTemplate(AADConstants.WORLD_WIDE_AUTHORITY); var getOptions = util.createRequestOptions(self); this._log.verbose('Attempting instance discover'); this._log.verbose('Attempting instance discover at: ' + url.format(discoveryEndpoint), true); request.get(discoveryEndpoint, getOptions, util.createRequestHandler('Instance Discovery', this._log, callback, function(response, body) { var discoveryResponse = JSON.parse(body); if (discoveryResponse['tenant_discovery_endpoint']) { callback(null, discoveryResponse['tenant_discovery_endpoint']); } else { callback(self._log.createError('Failed to parse instance discovery response')); } }) ); } catch(e) { callback(e); } }; /** * @callback InstanceDiscoveryCallback * @private * @memberOf Authority * @param {Error} err If an error occurs during instance discovery then it will be returned here. * @param {string} tenantDiscoveryEndpoint If instance discovery is successful then this will contain the * tenantDiscoveryEndpoint associated with the authority. */ /** * Determines whether the authority is recognized as a trusted AAD authority. * @private * @param {Authority.InstanceDiscoveryCallback} callback The callback function. */ Authority.prototype._validateViaInstanceDiscovery = function(callback) { if (this._performStaticInstanceDiscovery()) { callback(); } else { this._performDynamicInstanceDiscovery(callback); } }; /** * @callback GetOauthEndpointsCallback * @private * @memberOf Authority * @param {Error} error An error if one occurred. */ /** * Given a tenant discovery endpoint this method will attempt to discover the token endpoint. If the * tenant discovery endpoint is unreachable for some reason then it will fall back to a algorithmic generation of the * token endpoint url. * @private * @param {string} tenantDiscoveryEndpoint The url of the tenant discovery endpoint for this authority. * @param {Authority.GetOauthEndpointsCallback} callback The callback function. */ Authority.prototype._getOAuthEndpoints = function(tenantDiscoveryEndpoint, callback) { if (this._tokenEndpoint && this._deviceCodeEndpoint) { callback(); return; } else { // fallback to the well known token endpoint path. if (!this._tokenEndpoint){ this._tokenEndpoint = url.format('https://' + this._url.host + '/' + encodeURIComponent(this._tenant)) + AADConstants.TOKEN_ENDPOINT_PATH; } if (!this._deviceCodeEndpoint){ this._deviceCodeEndpoint = url.format('https://' + this._url.host + '/' + encodeURIComponent(this._tenant)) + AADConstants.DEVICE_ENDPOINT_PATH; } callback(); return; } }; /** * @callback ValidateCallback * @memberOf Authority */ /** * Perform validation on the authority represented by this object. In addition to simple validation * the oauth token endpoint will be retrieved. * @param {Authority.ValidateCallback} callback The callback function. */ Authority.prototype.validate = function(callContext, callback) { this._log = new Logger('Authority', callContext._logContext); this._callContext = callContext; var self = this; if (!this._validated) { this._log.verbose('Performing instance discovery'); this._log.verbose('Performing instance discovery: ' + url.format(this._url), true); this._validateViaInstanceDiscovery(function(err, tenantDiscoveryEndpoint) { if (err) { callback(err); } else { self._validated = true; self._getOAuthEndpoints(tenantDiscoveryEndpoint, callback); return; } }); } else { this._log.verbose('Instance discovery/validation has either already been completed or is turned off'); this._log.verbose('Instance discovery/validation has either already been completed or is turned off: ' + url.format(this._url), true); this._getOAuthEndpoints(null, callback); return; } }; module.exports.Authority = Authority;