adal-node
Version:
Windows Azure Active Directory Client Library for node
292 lines (260 loc) • 9.53 kB
JavaScript
/*
* @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 querystring = require('querystring');
var axios = require('axios');
var _ = require('underscore');
var url = require('url');
var constants = require('./constants');
var Logger = require('./log').Logger;
var util = require('./util');
var AccountType = constants.UserRealm.AccountType;
var FederationProtocolType = constants.UserRealm.FederationProtocolType;
var USER_REALM_PATH_TEMPLATE = 'common/UserRealm/<user>';
/**
* Create a new UserRealm object
* @private
* @constructor
* @param {object} callContext Contains any context information that applies to the request.
* @param {string} userPrinciple The username for which a realm needs to be discovered.
* @param {string} authority The string url of the authority that owns the userPrinciple.
*/
function UserRealm(callContext, userPrinciple, authority) {
this._log = new Logger('UserRealm', callContext._logContext);
this._callContext = callContext;
this._apiVersion = '1.0';
this._federationProtocol = null;
this._accountType = null;
this._federationMetadataUrl = null;
this._federationActiveAuthUrl = null;
this._userPrinciple = userPrinciple;
this._authority = authority;
}
/**
* The API version requested by UserRealm.
* @type {string}
* @instance
* @memberOf UserRealm
* @name apiVersion
*/
Object.defineProperty(UserRealm.prototype, 'apiVersion', {
get : function() {
return this._apiVersion;
}
});
/**
* The federation protocol used by the users realm.
* @type {string}
* @instance
* @memberOf UserRealm
* @name federationProtocol
*/
Object.defineProperty(UserRealm.prototype, 'federationProtocol', {
get : function() {
return this._federationProtocol;
}
});
/**
* The Type of account. Either managed or federated.
* @type {string}
* @instance
* @memberOf UserRealm
* @name accountType
*/
Object.defineProperty(UserRealm.prototype, 'accountType', {
get : function() {
return this._accountType;
}
});
/**
* If this is a federated account then this property will contain the mex url.
* @type {string}
* @instance
* @memberOf UserRealm
* @name federationsMetadataUrl
*/
Object.defineProperty(UserRealm.prototype, 'federationMetadataUrl', {
get : function() {
return this._federationMetadataUrl;
}
});
/**
* If the account is federated this will contain the authentication endpoint.
* @type {string}
* @instance
* @memberOf UserRealm
* @name federationActiveAuthUrl
*/
Object.defineProperty(UserRealm.prototype, 'federationActiveAuthUrl', {
get : function() {
return this._federationActiveAuthUrl;
}
});
/**
* Given the authority url this method constructs a full user realm discovery url.
* @private
* @returns A full user realm discovery url including path and query string.
*/
UserRealm.prototype._getUserRealmUrl = function() {
var userRealmUrl = util.copyUrl(this._authority);
var urlEncodedUser = encodeURIComponent(this._userPrinciple);
userRealmUrl.pathname = USER_REALM_PATH_TEMPLATE.replace('<user>', urlEncodedUser);
var userRealmQuery = {
'api-version' : this._apiVersion
};
userRealmUrl.search = querystring.stringify(userRealmQuery);
userRealmUrl = util.copyUrl(userRealmUrl);
return userRealmUrl;
};
/**
* Given a constants object and a value, validates that the value is a key in the constants object.
* @private
* @param {object} constants An object containing constant key value pairs.
* @param {string} value A value to check against the constants
* @param {bool} caseSensitive set to true if comparisons should be made as case sensitive. Defaults to false.
* @returns {bool|string} If value passed in matches one of the constants then the return value is the matched constant.
* If a non case sensitive match was done, then the value returned may be different than the value
* passed in. If there is no match then the method returns false.
*/
UserRealm.prototype._validateConstantValue = function(constants, value, caseSensitive) {
if (!value) {
return false;
}
if (!caseSensitive) {
value = value.toLowerCase();
}
return _.contains(_.values(constants), value) ? value : false;
};
/**
* Checks whether an account type string is valid.
* @private
* @param {string} type An account type string.
* @returns {bool}
*/
UserRealm.prototype._validateAccountType = function(type) {
return this._validateConstantValue(AccountType, type);
};
/**
* Checks whether a federation protocol string is valid.
* @private
* @param {string} protocol A federation protocol string.
* @returns {bool}
*/
UserRealm.prototype._validateFederationProtocol = function(protocol) {
return this._validateConstantValue(FederationProtocolType, protocol);
};
/**
* Logs the values parsed as part of user realm discovery.
* @private
*/
UserRealm.prototype._logParsedResponse = function() {
this._log.verbose('UserRealm response:');
this._log.verbose(' AccountType: ' + this.accountType);
this._log.verbose(' FederationProtocol: ' + this.federationProtocol);
this._log.verbose(' FederationMetatdataUrl: ' + this.federationMetadataUrl, true);
this._log.verbose(' FederationActiveAuthUrl: ' + this.federationActiveAuthUrl, true);
};
/**
* Parses the response from a user realm discovery request.
* @private
* @param {string} body The body returned as part of the http user realm discovery request.
* @param {UserRealm.DiscoverCallback} callback Called when parsing is complete.
*/
UserRealm.prototype._parseDiscoveryResponse = function(body, callback) {
this._log.verbose('Discovery response:\n' + body, true);
var response;
try {
response = body;
} catch (err) {
callback(this._log.createError('Parsing realm discovery respone JSON failed: ' + body, true));
return;
}
var accountType = this._validateAccountType(response['account_type']);
if (!accountType) {
callback(this._log.createError('Cannot parse account_type: ' + accountType));
return;
}
this._accountType = accountType;
if (this._accountType === AccountType.Federated) {
var protocol = this._validateFederationProtocol(response['federation_protocol']);
if (!protocol) {
callback(this._log.createError('Cannot parse federation protocol: ' + protocol));
return;
}
this._federationProtocol = protocol;
this._federationMetadataUrl = response['federation_metadata_url'];
this._federationActiveAuthUrl = response['federation_active_auth_url'];
}
this._logParsedResponse();
callback();
};
/**
* @callback DiscoverCallback
* @memberOf UserRealm
* @param {Error} error If an error occurs during discovery then this parameter will be used to return the error.
*/
/**
* Performs user realm discovery and fills in the properties on this object.
* @private
* @param {UserRealm.DiscoverCallback} callback Called when discovery is complete.
*/
UserRealm.prototype.discover = function(callback) {
var self = this;
var options = util.createRequestOptions(
this,
{
headers : {
Accept : 'application/json'
}
}
);
var userRealmUrl = this._getUserRealmUrl();
this._log.verbose('Performing user realm discovery at: ' + url.format(userRealmUrl), true);
axios.get(userRealmUrl, options).then((response) => {
util.logReturnCorrelationId(this._log, 'User Realm Discovery', response);
// status >= 300 && < 400
if (!util.isHttpSuccess(response.status)) {
var returnErrorString = 'User Realm Discovery' + ' request returned http error: ' + response.status + ' and server response: ' + response.status;
callback(this._log.createError(returnErrorString, true), response.data);
}
// Success case: status >= 200 && < 300
self._parseDiscoveryResponse(response.data, callback);
}).catch((error) => {
// status >= 400: error case
if (error.response) {
util.logReturnCorrelationId(this._log, 'User Realm Discovery', error.response);
this._log.error('User Realm Discovery' + ' request failed with', error.response.status, true);
var returnErrorString = 'User Realm Discovery' + ' request returned http error: ' + error.response.status + ' and server response: ' + JSON.stringify(error.response.data);
callback(this._log.createError(returnErrorString, true), error.response.data);
}
// if there is no response from the server
else if (error.request) {
this._log.error('User Realm Discovery' + ' request was made but no response was received', error.request, true);
callback(self._log.createError('No response from the server'));
}
// request was never made
else {
this._log.error('User Realm Discovery' + ' request was never made, please check', error.message, true);
callback(error.message);
}
});
};
module.exports = UserRealm;