Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 6 commits into from
Jun 13, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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