diff --git a/ClientRuntimes/NodeJS/ms-rest-azure/lib/credentials/applicationTokenCredentials.js b/ClientRuntimes/NodeJS/ms-rest-azure/lib/credentials/applicationTokenCredentials.js index 6c1df592b2..8afc2f3f3a 100644 --- a/ClientRuntimes/NodeJS/ms-rest-azure/lib/credentials/applicationTokenCredentials.js +++ b/ClientRuntimes/NodeJS/ms-rest-azure/lib/credentials/applicationTokenCredentials.js @@ -60,12 +60,37 @@ function ApplicationTokenCredentials(clientId, domain, secret, options) { this.context = new adal.AuthenticationContext(authorityUrl, this.environment.validateAuthority, this.tokenCache); } -function _retrieveTokenFromCache (callback) { +function _removeInvalidEntry(query, callback) { + var self = this; + self.tokenCache.find(query, function (err, entries) { + if (err) return callback(err); + if (entries && entries.length > 0) { + return self.tokenCache.remove(entries, callback); + } else { + return callback(); + } + }); +} + +function _retrieveTokenFromCache(callback) { //For service principal userId and clientId are the same thing. Since the token has _clientId property we shall //retrieve token using it. - this.context.acquireToken(this.environment.activeDirectoryResourceId, null, this.clientId, function (err, result) { - if (err) return callback(err); - return callback(null, result); + var self = this; + self.context.acquireToken(self.environment.activeDirectoryResourceId, null, self.clientId, function (err, result) { + if (err) { + //make sure to remove the stale token from the tokencache. ADAL gives the same error message "Entry not found in cache." + //for entry not being present in the cache and for accessToken being expired in the cache. We do not want the token cache + //to contain the expired token hence we will search for it and delete it explicitly over here. + _removeInvalidEntry.call(self, { _clientId: self.clientId}, function (erronRemove) { + if (erronRemove) { + return callback(new Error('Error occurred while removing the expired token for service principal from token cache.\n' + erronRemove.message)); + } else { + return callback(err); + } + }); + } else { + return callback(null, result); + } }); } @@ -80,13 +105,17 @@ ApplicationTokenCredentials.prototype.getToken = function (callback) { var self = this; _retrieveTokenFromCache.call(this, function (err, result) { if (err) { - //Some error occured in retrieving the token from cache. May be the cache was empty or the access token expired. Let's try again. - self.context.acquireTokenWithClientCredentials(self.environment.activeDirectoryResourceId, self.clientId, self.secret, function (err, tokenResponse) { - if (err) { - return callback(new Error('Failed to acquire token for application with the provided secret. \n' + err)); - } - return callback(null, tokenResponse); - }); + if (err.message.match(/.*while removing the expired token for service principal.*/i) !== null) { + return callback(err); + } else { + //Some error occured in retrieving the token from cache. May be the cache was empty or the access token expired. Let's try again. + self.context.acquireTokenWithClientCredentials(self.environment.activeDirectoryResourceId, self.clientId, self.secret, function (err, tokenResponse) { + if (err) { + return callback(new Error('Failed to acquire token for application with the provided secret. \n' + err)); + } + return callback(null, tokenResponse); + }); + } } else { return callback(null, result); } diff --git a/ClientRuntimes/NodeJS/ms-rest-azure/lib/login.js b/ClientRuntimes/NodeJS/ms-rest-azure/lib/login.js index 7aff354c5d..f1dc5ee5ca 100644 --- a/ClientRuntimes/NodeJS/ms-rest-azure/lib/login.js +++ b/ClientRuntimes/NodeJS/ms-rest-azure/lib/login.js @@ -3,25 +3,99 @@ var adal= require('adal-node'); var async = require('async'); - +var util = require('util'); var azureConstants = require('./constants'); var ApplicationTokenCredentials = require('./credentials/applicationTokenCredentials'); var DeviceTokenCredentials = require('./credentials/deviceTokenCredentials'); var UserTokenCredentials = require('./credentials/userTokenCredentials'); var AzureEnvironment = require('./azureEnvironment'); +var SubscriptionClient = require('azure-arm-resource').SubscriptionClient; -function _createDeviceCredentials(tokenResponse) { +// It will create a DeviceTokenCredentials object by default +function _createCredentials(parameters) { var options = {}; options.environment = this.environment; options.domain = this.domain; options.clientId = this.clientId; options.tokenCache = this.tokenCache; - options.username = tokenResponse.userId; - options.authorizationScheme = tokenResponse.tokenType; - var credentials = new DeviceTokenCredentials(options); + options.username = this.username; + options.authorizationScheme = this.authorizationScheme; + if (parameters) { + if (parameters.domain) { + options.domain = parameters.domain; + } + if (parameters.environment) { + options.environment = parameters.environment; + } + if (parameters.userId) { + options.username = parameters.userId; + } + if (parameters.tokenCache) { + options.tokenCache = parameters.tokenCache; + } + } + var credentials; + if (UserTokenCredentials.prototype.isPrototypeOf(this)) { + credentials = new UserTokenCredentials(options.clientId, options.domain, options.username, this.password, options); + } else if (ApplicationTokenCredentials.prototype.isPrototypeOf(this)) { + credentials = new ApplicationTokenCredentials(options.clientId, options.domain, this.secret, options); + } else { + credentials = new DeviceTokenCredentials(options); + } return credentials; } +function buildTenantList(credentials, callback) { + var tenants = []; + if (credentials.domain && credentials.domain !== azureConstants.AAD_COMMON_TENANT) { + return callback(null, [credentials.domain]); + } + var client = new SubscriptionClient(credentials, credentials.environment.resourceManagerEndpointUrl); + client.tenants.list(function (err, result) { + async.eachSeries(result, function (tenantInfo, cb) { + tenants.push(tenantInfo.tenantId); + cb(err); + }, function (err) { + callback(err, tenants); + }); + }); +} + +function getSubscriptionsFromTenants(tenantList, callback) { + var self = this; + var subscriptions = []; + var userType = 'user'; + var username = self.username; + if (ApplicationTokenCredentials.prototype.isPrototypeOf(self)) { + userType = 'servicePrincipal'; + username = self.clientId; + } + async.eachSeries(tenantList, function (tenant, cb) { + var creds = _createCredentials.call(self, { domain: tenant }); + var client = new SubscriptionClient(creds, creds.environment.resourceManagerEndpointUrl); + client.subscriptions.list(function (err, result) { + if (!err) { + if (result && result.length > 0) { + subscriptions = subscriptions.concat(result.map(function (s) { + s.tenantId = tenant; + s.user = { name: username, type: userType }; + s.environmentName = creds.environment.name; + s.name = s.displayName; + s.id = s.subscriptionId; + delete s.displayName; + delete s.subscriptionId; + delete s.subscriptionPolicies; + return s; + })); + } + } + return cb(err); + }); + }, function (err) { + callback(err, subscriptions); + }); +} + function _turnOnLogging() { var log = adal.Logging; log.setLoggingOptions( @@ -40,9 +114,20 @@ if (process.env['AZURE_ADAL_LOGGING_ENABLED']) { _turnOnLogging(); } +function _crossCheckUserNameWithToken(usernameFromMethodCall, userIdFromToken) { + //to maintain the casing consistency between 'azureprofile.json' and token cache. (RD 1996587) + //use the 'userId' here, which should be the same with "username" except the casing. + if (usernameFromMethodCall.toLowerCase() === userIdFromToken.toLowerCase()) { + return userIdFromToken; + } else { + throw new Error(util.format('The userId of \'%s\' in access token doesn\'t match the username from method call \'%s\'', + userIdFromToken, usernameFromMethodCall)); + } +} + /** * Provides a url and code that needs to be copy and pasted in a browser and authenticated over there. If successful, the user will get a - * DeviceTokenCredentials object + * DeviceTokenCredentials object and the list of subscriptions associated with that userId across all the applicable tenants. * * @param {object} [options] Object representing optional parameters. * @@ -62,9 +147,9 @@ if (process.env['AZURE_ADAL_LOGGING_ENABLED']) { * * @returns {function} callback(err, credentials) * - * {Error} [err] - The Error object if an error occurred, null otherwise. - * + * {Error} [err] - The Error object if an error occurred, null otherwise. * {DeviceTokenCredentials} [credentials] - The DeviceTokenCredentials object + * {Array} [subscriptions] - List of associated subscriptions across all the applicable tenants. */ exports.interactive = function interactive(options, callback) { if(!callback && typeof options === 'function') { @@ -100,7 +185,9 @@ exports.interactive = function interactive(options, callback) { var authorityUrl = this.environment.activeDirectoryEndpointUrl + this.domain; this.context = new adal.AuthenticationContext(authorityUrl, this.environment.validateAuthority, this.tokenCache); var self = this; + var tenantList = []; async.waterfall([ + //acquire usercode function (callback) { self.context.acquireUserCode(self.environment.activeDirectoryResourceId, self.clientId, self.language, function (err, userCodeResponse) { if (err) return callback(err); @@ -108,21 +195,35 @@ exports.interactive = function interactive(options, callback) { return callback(null, userCodeResponse); }); }, + //acquire token with device code and set the username to userId received from tokenResponse. function (userCodeResponse, callback) { self.context.acquireTokenWithDeviceCode(self.environment.activeDirectoryResourceId, self.clientId, userCodeResponse, function (err, tokenResponse) { if (err) return callback(err); - return callback(null, tokenResponse); + self.username = tokenResponse.userId; + self.authorizationScheme = tokenResponse.tokenType; + return callback(null); }); + }, + //get the list of tenants + function (callback) { + var credentials = _createCredentials.call(self); + buildTenantList(credentials, callback); + }, + //build the token cache by getting tokens for all the tenants. We will acquire token from adal only when a request is sent. This is good as we also need + //to build the list of subscriptions across all tenants. So let's build both at the same time :). + function (tenants, callback) { + tenantList = tenants; + getSubscriptionsFromTenants.call(self, tenants, callback); } - ], function(err, tokenResponse) { + ], function(err, subscriptions) { if (err) return callback(err); - return callback(null, _createDeviceCredentials.call(self, tokenResponse)); + return callback(null, _createCredentials.call(self), subscriptions); }); }; /** - * Provides a UserTokenCredentials object. This method is applicable only for organizational ids that are not 2FA enabled. - * Otherwise please use interactive login. + * Provides a UserTokenCredentials object and the list of subscriptions associated with that userId across all the applicable tenants. + * This method is applicable only for organizational ids that are not 2FA enabled otherwise please use interactive login. * * @param {string} username The user name for the Organization Id account. * @param {string} password The password for the Organization Id account. @@ -138,9 +239,9 @@ exports.interactive = function interactive(options, callback) { * * @returns {function} callback(err, credentials) * - * {Error} [err] - The Error object if an error occurred, null otherwise. - * + * {Error} [err] - The Error object if an error occurred, null otherwise. * {UserTokenCredentials} [credentials] - The UserTokenCredentials object + * {Array} [subscriptions] - List of associated subscriptions across all the applicable tenants. */ exports.withUsernamePassword = function withUsernamePassword(username, password, options, callback) { if(!callback && typeof options === 'function') { @@ -156,16 +257,31 @@ exports.withUsernamePassword = function withUsernamePassword(username, password, options.clientId = azureConstants.DEFAULT_ADAL_CLIENT_ID; } var creds; + var tenantList = []; try { creds = new UserTokenCredentials(options.clientId, options.domain, username, password, options); } catch (err) { return callback(err); } - return callback(null, creds); + creds.getToken(function (err, result) { + if (err) return callback(err); + creds.username = _crossCheckUserNameWithToken(username, result.userId); + async.waterfall([ + function (callback) { + buildTenantList(creds, callback); + }, + function (tenants, callback) { + tenantList = tenants; + getSubscriptionsFromTenants.call(creds, tenants, callback); + }, + ], function (err, subscriptions) { + return callback(null, creds, subscriptions); + }); + }); }; /** - * Provides an ApplicationTokenCredentials object. + * Provides an ApplicationTokenCredentials object and the list of subscriptions associated with that servicePrinicpalId/clientId across all the applicable tenants. * * @param {string} clientId The active directory application client id also known as the SPN (ServicePrincipal Name). * See {@link https://azure.microsoft.com/en-us/documentation/articles/active-directory-devquickstarts-dotnet/ Active Directory Quickstart for .Net} @@ -180,9 +296,9 @@ exports.withUsernamePassword = function withUsernamePassword(username, password, * * @returns {function} callback(err, credentials) * - * {Error} [err] - The Error object if an error occurred, null otherwise. - * + * {Error} [err] - The Error object if an error occurred, null otherwise. * {UserTokenCredentials} [credentials] - The UserTokenCredentials object + * {Array} [subscriptions] - List of associated subscriptions across all the applicable tenants. */ exports.withServicePrincipalSecret = function withServicePrincipalSecret(clientId, secret, domain, options, callback) { if(!callback && typeof options === 'function') { @@ -195,7 +311,13 @@ exports.withServicePrincipalSecret = function withServicePrincipalSecret(clientI } catch (err) { return callback(err); } - return callback(null, creds); + creds.getToken(function (err) { + if (err) return callback(err); + getSubscriptionsFromTenants.call(creds, [domain], function (err, subscriptions) { + if (err) return callback(err); + return callback(null, creds, subscriptions); + }); + }); }; exports = module.exports; \ No newline at end of file diff --git a/ClientRuntimes/NodeJS/ms-rest-azure/package.json b/ClientRuntimes/NodeJS/ms-rest-azure/package.json index d6f7444e9f..8357ebf50e 100644 --- a/ClientRuntimes/NodeJS/ms-rest-azure/package.json +++ b/ClientRuntimes/NodeJS/ms-rest-azure/package.json @@ -5,21 +5,19 @@ "email": "azsdkteam@microsoft.com", "url": "https://github.com/Azure/AutoRest" }, - "version": "1.14.2", + "version": "1.14.4", "description": "Client Runtime for Node.js Azure client libraries generated using AutoRest", "tags": [ "node", "microsoft", "autorest", "azure", "clientruntime" ], "keywords": [ "node", "microsoft", "autorest", "azure", "clientruntime" ], "main": "./lib/msRestAzure.js", - "engines": { - "node": ">= 0.10.0" - }, "license": "MIT", "dependencies": { "async": "0.2.7", "uuid": "2.0.1", "adal-node": "^0.1.17", - "ms-rest": "^1.14.2", - "moment": "^2.6.0" + "ms-rest": "^1.14.3", + "moment": "^2.6.0", + "azure-arm-resource": "^1.4.4-preview" }, "devDependencies": { "jshint": "2.6.3", diff --git a/ClientRuntimes/NodeJS/ms-rest/package.json b/ClientRuntimes/NodeJS/ms-rest/package.json index f65673c6a3..0ba36267e6 100644 --- a/ClientRuntimes/NodeJS/ms-rest/package.json +++ b/ClientRuntimes/NodeJS/ms-rest/package.json @@ -5,7 +5,7 @@ "email": "azsdkteam@microsoft.com", "url": "https://github.com/Azure/AutoRest" }, - "version": "1.14.2", + "version": "1.14.3", "description": "Client Runtime for Node.js client libraries generated using AutoRest", "tags": ["node", "microsoft", "autorest", "clientruntime"], "keywords": ["node", "microsoft", "autorest", "clientruntime"],