Skip to content

Commit

Permalink
remove stale token from tokencache for sp auth as adal does not remov…
Browse files Browse the repository at this point in the history
…e it. (#1145)

* remove stale token from tokencache for sp auth as adal does not remove it.

* more improvements

* jshint fixes

* application token creds update

* update version
  • Loading branch information
amarzavery authored Jun 13, 2016
1 parent f8683d0 commit 958aedd
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
}

Expand All @@ -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);
}
Expand Down
162 changes: 142 additions & 20 deletions ClientRuntimes/NodeJS/ms-rest-azure/lib/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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.
*
Expand All @@ -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') {
Expand Down Expand Up @@ -100,29 +185,45 @@ 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);
console.log(userCodeResponse.message);
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.
Expand All @@ -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') {
Expand All @@ -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}
Expand All @@ -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') {
Expand All @@ -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;
10 changes: 4 additions & 6 deletions ClientRuntimes/NodeJS/ms-rest-azure/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion ClientRuntimes/NodeJS/ms-rest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down

0 comments on commit 958aedd

Please sign in to comment.