From f9a1606c5853e6fda45d87c56f4b047dac60f18d Mon Sep 17 00:00:00 2001 From: Amar Zavery Date: Sun, 22 May 2016 09:40:19 -0700 Subject: [PATCH] updates to node.js clientruntime (#1070) * inital commit * minor update * some more updates * UserTokenCredentials use the token cache to retrieve tokens * updates to node.js clientruntime * update readme --- .../Azure.NodeJS/AzureNodeJSCodeGenerator.cs | 2 +- .../NodeJS/NodeJS/NodeJSCodeGenerator.cs | 2 +- ClientRuntimes/NodeJS/ms-rest-azure/README.md | 12 +- .../applicationTokenCredentials.js | 45 +++++- .../lib/credentials/userTokenCredentials.js | 37 ++++- .../NodeJS/ms-rest-azure/lib/index.d.ts | 102 ++++++++++++- .../NodeJS/ms-rest-azure/lib/login.js | 142 ++++++++++-------- .../NodeJS/ms-rest-azure/lib/msRestAzure.js | 1 + .../NodeJS/ms-rest-azure/package.json | 4 +- ClientRuntimes/NodeJS/ms-rest/package.json | 5 +- 10 files changed, 260 insertions(+), 92 deletions(-) diff --git a/AutoRest/Generators/NodeJS/Azure.NodeJS/AzureNodeJSCodeGenerator.cs b/AutoRest/Generators/NodeJS/Azure.NodeJS/AzureNodeJSCodeGenerator.cs index b69f65ab8247a..23080cb3a6797 100644 --- a/AutoRest/Generators/NodeJS/Azure.NodeJS/AzureNodeJSCodeGenerator.cs +++ b/AutoRest/Generators/NodeJS/Azure.NodeJS/AzureNodeJSCodeGenerator.cs @@ -18,7 +18,7 @@ namespace Microsoft.Rest.Generator.Azure.NodeJS { public class AzureNodeJSCodeGenerator : NodeJSCodeGenerator { - private const string ClientRuntimePackage = "ms-rest-azure version 1.14.0"; + private const string ClientRuntimePackage = "ms-rest-azure version 1.14.2"; // List of models with paging extensions. private IList pageModels; diff --git a/AutoRest/Generators/NodeJS/NodeJS/NodeJSCodeGenerator.cs b/AutoRest/Generators/NodeJS/NodeJS/NodeJSCodeGenerator.cs index 98d40be85a514..c25a4aff6ec00 100644 --- a/AutoRest/Generators/NodeJS/NodeJS/NodeJSCodeGenerator.cs +++ b/AutoRest/Generators/NodeJS/NodeJS/NodeJSCodeGenerator.cs @@ -16,7 +16,7 @@ namespace Microsoft.Rest.Generator.NodeJS { public class NodeJSCodeGenerator : CodeGenerator { - private const string ClientRuntimePackage = "ms-rest version 1.14.0"; + private const string ClientRuntimePackage = "ms-rest version 1.14.2"; public NodeJsCodeNamer Namer { get; private set; } diff --git a/ClientRuntimes/NodeJS/ms-rest-azure/README.md b/ClientRuntimes/NodeJS/ms-rest-azure/README.md index fcb28f9478d83..0dfc9eb16c497 100644 --- a/ClientRuntimes/NodeJS/ms-rest-azure/README.md +++ b/ClientRuntimes/NodeJS/ms-rest-azure/README.md @@ -45,10 +45,18 @@ Otherwise it is better to use the above mechanism (interactive login). }); ``` -### ServicePrincipal authentication +#### Login with service principal name and secret ```javascript - var credentials = new msRestAzure.ApplicationTokenCredentials('your-client-id', 'your-domain', 'your-secret'); + var someAzureServiceClient = require('azure-arm-someService'); + msRestAzure.loginWithServicePrincipalSecret(clientId, secret, domain, function(err, credentials) { + var client = new someAzureServiceClient(credentials, 'your-subscriptionId'); + client.someOperationGroup.method(param1, param2, function(err, result) { + if (err) console.log(err); + console.log(result); + }); + }); ``` + ### Non-Interactive Authentication If you need to create an automation account for non interactive or scripting scenarios then please take a look at the documentation over [here](https://github.com/Azure/azure-sdk-for-node/blob/master/Documentation/Authentication.md). diff --git a/ClientRuntimes/NodeJS/ms-rest-azure/lib/credentials/applicationTokenCredentials.js b/ClientRuntimes/NodeJS/ms-rest-azure/lib/credentials/applicationTokenCredentials.js index 9a7885a932c9b..6c1df592b28ae 100644 --- a/ClientRuntimes/NodeJS/ms-rest-azure/lib/credentials/applicationTokenCredentials.js +++ b/ClientRuntimes/NodeJS/ms-rest-azure/lib/credentials/applicationTokenCredentials.js @@ -60,6 +60,39 @@ function ApplicationTokenCredentials(clientId, domain, secret, options) { this.context = new adal.AuthenticationContext(authorityUrl, this.environment.validateAuthority, this.tokenCache); } +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); + }); +} + +/** + * Tries to get the token from cache initially. If that is unsuccessfull then it tries to get the token from ADAL. + * @param {function} callback The callback in the form (err, result) + * @return {function} callback + * {Error} [err] The error if any + * {object} [tokenResponse] The tokenResponse (tokenType and accessToken are the two important properties). + */ +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); + }); + } else { + return callback(null, result); + } + }); +}; + /** * Signs a request with the Authentication header. * @@ -68,15 +101,11 @@ function ApplicationTokenCredentials(clientId, domain, secret, options) { * @return {undefined} */ ApplicationTokenCredentials.prototype.signRequest = function (webResource, callback) { - var self = this; - self.context.acquireTokenWithClientCredentials(self.environment.activeDirectoryResourceId, self.clientId, self.secret, function (err, result) { - if (err) { - return callback(new Error('Failed to acquire token for application. \n' + err)); - } - + this.getToken(function (err, result) { + if (err) return callback(err); webResource.headers[Constants.HeaderConstants.AUTHORIZATION] = - util.format('%s %s', self.authorizationScheme, result.accessToken); - callback(null); + util.format('%s %s', result.tokenType, result.accessToken); + return callback(null); }); }; diff --git a/ClientRuntimes/NodeJS/ms-rest-azure/lib/credentials/userTokenCredentials.js b/ClientRuntimes/NodeJS/ms-rest-azure/lib/credentials/userTokenCredentials.js index 19eed7559a8df..12908b0edbbff 100644 --- a/ClientRuntimes/NodeJS/ms-rest-azure/lib/credentials/userTokenCredentials.js +++ b/ClientRuntimes/NodeJS/ms-rest-azure/lib/credentials/userTokenCredentials.js @@ -67,11 +67,35 @@ function UserTokenCredentials(clientId, domain, username, password, options) { this.context = new adal.AuthenticationContext(authorityUrl, this.environment.validateAuthority, this.tokenCache); } -UserTokenCredentials.prototype.retrieveTokenFromCache = function (callback) { - var self = this; - self.context.acquireToken(self.environment.activeDirectoryResourceId, self.username, self.clientId, function (err, result) { +function _retrieveTokenFromCache(callback) { + this.context.acquireToken(this.environment.activeDirectoryResourceId, this.username, this.clientId, function (err, result) { if (err) return callback(err); - return callback(null, result.tokenType, result.accessToken); + return callback(null, result); + }); +} + +/** + * Tries to get the token from cache initially. If that is unsuccessfull then it tries to get the token from ADAL. + * @param {function} callback The callback in the form (err, result) + * @return {function} callback + * {Error} [err] The error if any + * {object} [tokenResponse] The tokenResponse (tokenType and accessToken are the two important properties). + */ +UserTokenCredentials.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. Let's try again. + self.context.acquireTokenWithUsernamePassword(self.environment.activeDirectoryResourceId, self.username, + self.password, self.clientId, function (err, tokenResponse) { + if (err) { + return callback(new Error('Failed to acquire token for the user. \n' + err)); + } + return callback(null, tokenResponse); + }); + } else { + return callback(null, result); + } }); }; @@ -83,9 +107,10 @@ UserTokenCredentials.prototype.retrieveTokenFromCache = function (callback) { * @return {undefined} */ UserTokenCredentials.prototype.signRequest = function (webResource, callback) { - return this.retrieveTokenFromCache(function(err, scheme, token) { + this.getToken(function (err, result) { if (err) return callback(err); - webResource.headers[Constants.HeaderConstants.AUTHORIZATION] = util.format('%s %s', scheme, token); + webResource.headers[Constants.HeaderConstants.AUTHORIZATION] = + util.format('%s %s', result.tokenType, result.accessToken); return callback(null); }); }; diff --git a/ClientRuntimes/NodeJS/ms-rest-azure/lib/index.d.ts b/ClientRuntimes/NodeJS/ms-rest-azure/lib/index.d.ts index ef4378cff4e40..4d113e8c9ed54 100644 --- a/ClientRuntimes/NodeJS/ms-rest-azure/lib/index.d.ts +++ b/ClientRuntimes/NodeJS/ms-rest-azure/lib/index.d.ts @@ -150,13 +150,40 @@ export interface AzureTokenCredentialsOptions { */ authorizationScheme?: string; - // TODO: What type should this really have? How is it used? /** - * The token cache. Default value is null. + * The token cache. Default value is MemoryCache from adal. */ tokenCache?: any; } +export interface LoginWithUsernamePasswordOptions extends AzureTokenCredentialsOptions { + /** + * The domain or tenant id containing this application. Default value is 'common'. + */ + domain?: string; + + /** + * The active directory application client id. + * See {@link https://azure.microsoft.com/en-us/documentation/articles/active-directory-devquickstarts-dotnet/ Active Directory Quickstart for .Net} + * for an example. + */ + clientId?: string +} + +export interface DeviceTokenCredentialsOptions extends LoginWithUsernamePasswordOptions { + /** + * The user name for account in the form: 'user@example.com'. Default value is 'user@example.com'. + */ + username?: string; +} + +export interface InteractiveLoginOptions extends DeviceTokenCredentialsOptions { + /** + * The language code specifying how the message should be localized to. Default value 'en-us'. + */ + language?: string; +} + export class ApplicationTokenCredentials extends msRest.ServiceClientCredentials { /** * Creates a new ApplicationTokenCredentials object. @@ -179,12 +206,79 @@ export class UserTokenCredentials extends msRest.ServiceClientCredentials { * @param {string} domain The domain or tenant id containing this application. * @param {string} username The user name for the Organization Id account. * @param {string} password The password for the Organization Id account. - * @param {string} clientRedirectUri The Uri where the user will be redirected after authenticating with AD. * @param {AzureTokenCredentialsOptions} options Object representing optional parameters. */ - constructor(clientId: string, domain: string, username: string, password: string, clientRedirectUri: string, options?: AzureTokenCredentialsOptions); + constructor(clientId: string, domain: string, username: string, password: string, options?: AzureTokenCredentialsOptions); +} + +export class DeviceTokenCredentials extends msRest.ServiceClientCredentials { + /** + * Creates a new DeviceTokenCredentials object. + * @param {DeviceTokenCredentialsOptions} options Object representing optional parameters. + */ + constructor(options?: DeviceTokenCredentialsOptions); } // TODO: WHAT SHOULD WE EXPOSE HERE? export class BaseResource { } + +/** + * 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 + * + * @param {InteractiveLoginOptions} [options] The parameter options. + * + * @param {function} callback + * + * @returns {function} callback(err, credentials) + * + * {Error} [err] - The Error object if an error occurred, null otherwise. + * + * {DeviceTokenCredentials} [credentials] - The DeviceTokenCredentials object + */ +export function interactiveLogin (options?: InteractiveLoginOptions, callback); + +/** + * Provides a UserTokenCredentials object. 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. + * + * @param {LoginWithUsernamePasswordOptions} [options] The parameter options. + * + * @param {function} callback + * + * @returns {function} callback(err, credentials) + * + * {Error} [err] - The Error object if an error occurred, null otherwise. + * + * {UserTokenCredentials} [credentials] - The UserTokenCredentials object + */ +export function loginWithUsernamePassword (username: string, password: string, options?: LoginWithUsernamePasswordOptions, callback); + + +/** + * Provides an ApplicationTokenCredentials object. + * + * @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} + * for an example. + * + * @param {string} secret The application secret for the service principal. + * + * @param {string} domain The domain or tenant id containing this application. + * + * @param {AzureTokenCredentialsOptions} [options] The parameter options. + * + * @param {function} callback + * + * @returns {function} callback(err, credentials) + * + * {Error} [err] - The Error object if an error occurred, null otherwise. + * + * {ApplicationTokenCredentials} [credentials] - The ApplicationTokenCredentials object + */ +export function loginWithServicePrincipalSecret (clientId: string, secret: string, domain: string, options?: AzureTokenCredentialsOptions, callback); \ No newline at end of file diff --git a/ClientRuntimes/NodeJS/ms-rest-azure/lib/login.js b/ClientRuntimes/NodeJS/ms-rest-azure/lib/login.js index 1bcd458631679..7aff354c5d5d6 100644 --- a/ClientRuntimes/NodeJS/ms-rest-azure/lib/login.js +++ b/ClientRuntimes/NodeJS/ms-rest-azure/lib/login.js @@ -3,12 +3,11 @@ var adal= require('adal-node'); var async = require('async'); -var msrest = require('ms-rest'); -var Constants = msrest.Constants; var azureConstants = require('./constants'); -var UserTokenCredentials = require('./credentials/userTokenCredentials'); +var ApplicationTokenCredentials = require('./credentials/applicationTokenCredentials'); var DeviceTokenCredentials = require('./credentials/deviceTokenCredentials'); +var UserTokenCredentials = require('./credentials/userTokenCredentials'); var AzureEnvironment = require('./azureEnvironment'); function _createDeviceCredentials(tokenResponse) { @@ -23,13 +22,22 @@ function _createDeviceCredentials(tokenResponse) { return credentials; } -function _createUserCredentials(tokenResponse) { - var options = {}; - options.environment = this.environment; - options.tokenCache = this.tokenCache; - options.authorizationScheme = tokenResponse.tokenType; - var credentials = new UserTokenCredentials(this.clientId, this.domain, tokenResponse.userId, this.password, options); - return credentials; +function _turnOnLogging() { + var log = adal.Logging; + log.setLoggingOptions( + { + level : log.LOGGING_LEVEL.VERBOSE, + log : function (level, message, error) { + console.info(message); + if (error) { + console.error(error); + } + } + }); +} + +if (process.env['AZURE_ADAL_LOGGING_ENABLED']) { + _turnOnLogging(); } /** @@ -50,7 +58,7 @@ function _createUserCredentials(tokenResponse) { * * @param {object} [options.language] The language code specifying how the message should be localized to. Default value 'en-us'. * - * @param {function} callback The language code specifying how the message should be localized to. Default value 'en-us'. + * @param {function} callback * * @returns {function} callback(err, credentials) * @@ -113,40 +121,33 @@ exports.interactive = function interactive(options, callback) { }; /** -* Provides a UserTokenCredentials object if successful. -* -* @param {string} username The user name for the Organization Id account. -* @param {string} password The password for the Organization Id account. -* @param {object} [options] Object representing optional parameters. -* @param {string} [options.clientId] The active directory application client id. -* See {@link https://azure.microsoft.com/en-us/documentation/articles/active-directory-devquickstarts-dotnet/ Active Directory Quickstart for .Net} -* for an example. -* @param {string} [options.domain] The domain or tenant id containing this application. Default value 'common'. -* @param {AzureEnvironment} [options.environment] The azure environment to authenticate with. -* @param {string} [options.authorizationScheme] The authorization scheme. Default value is 'bearer'. -* @param {object} [options.tokenCache] The token cache. Default value is the MemoryCache object from adal. -* @param {function} callback The language code specifying how the message should be localized to. Default value 'en-us'. -* -* @returns {function} callback(err, credentials) -* -* {Error} [err] - The Error object if an error occurred, null otherwise. -* -* {UserTokenCredentials} [credentials] - The UserTokenCredentials object -*/ + * Provides a UserTokenCredentials object. 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. + * @param {object} [options] Object representing optional parameters. + * @param {string} [options.clientId] The active directory application client id. + * See {@link https://azure.microsoft.com/en-us/documentation/articles/active-directory-devquickstarts-dotnet/ Active Directory Quickstart for .Net} + * for an example. + * @param {string} [options.domain] The domain or tenant id containing this application. Default value 'common'. + * @param {AzureEnvironment} [options.environment] The azure environment to authenticate with. + * @param {string} [options.authorizationScheme] The authorization scheme. Default value is 'bearer'. + * @param {object} [options.tokenCache] The token cache. Default value is the MemoryCache object from adal. + * @param {function} callback + * + * @returns {function} callback(err, credentials) + * + * {Error} [err] - The Error object if an error occurred, null otherwise. + * + * {UserTokenCredentials} [credentials] - The UserTokenCredentials object + */ exports.withUsernamePassword = function withUsernamePassword(username, password, options, callback) { if(!callback && typeof options === 'function') { callback = options; options = {}; } - if (!Boolean(username) || typeof username.valueOf() !== 'string') { - throw new Error('username must be a non empty string.'); - } - - if (!Boolean(password) || typeof password.valueOf() !== 'string') { - throw new Error('password must be a non empty string.'); - } - if (!options.domain) { options.domain = azureConstants.AAD_COMMON_TENANT; } @@ -154,34 +155,47 @@ exports.withUsernamePassword = function withUsernamePassword(username, password, if (!options.clientId) { options.clientId = azureConstants.DEFAULT_ADAL_CLIENT_ID; } - - if (!options.environment) { - options.environment = AzureEnvironment.Azure; - } - - if (!options.authorizationScheme) { - options.authorizationScheme = Constants.HeaderConstants.AUTHORIZATION_SCHEME; + var creds; + try { + creds = new UserTokenCredentials(options.clientId, options.domain, username, password, options); + } catch (err) { + return callback(err); } + return callback(null, creds); +}; - if (!options.tokenCache) { - options.tokenCache = new adal.MemoryCache(); +/** + * Provides an ApplicationTokenCredentials object. + * + * @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} + * for an example. + * @param {string} secret The application secret for the service principal. + * @param {string} domain The domain or tenant id containing this application. + * @param {object} [options] Object representing optional parameters. + * @param {AzureEnvironment} [options.environment] The azure environment to authenticate with. + * @param {string} [options.authorizationScheme] The authorization scheme. Default value is 'bearer'. + * @param {object} [options.tokenCache] The token cache. Default value is the MemoryCache object from adal. + * @param {function} callback + * + * @returns {function} callback(err, credentials) + * + * {Error} [err] - The Error object if an error occurred, null otherwise. + * + * {UserTokenCredentials} [credentials] - The UserTokenCredentials object + */ +exports.withServicePrincipalSecret = function withServicePrincipalSecret(clientId, secret, domain, options, callback) { + if(!callback && typeof options === 'function') { + callback = options; + options = {}; } - - this.environment = options.environment; - this.authorizationScheme = options.authorizationScheme; - this.tokenCache = options.tokenCache; - this.domain = options.domain; - this.clientId = options.clientId; - this.username = username; - this.password = password; - var authorityUrl = this.environment.activeDirectoryEndpointUrl + this.domain; - this.context = new adal.AuthenticationContext(authorityUrl, this.environment.validateAuthority, this.tokenCache); - var self = this; - self.context.acquireTokenWithUsernamePassword(self.environment.activeDirectoryResourceId, self.username, - self.password, self.clientId, function (err, tokenResponse) { - if (err) return callback(new Error('Failed to acquire token. \n' + err)); - return callback(null, _createUserCredentials.call(self, tokenResponse)); - }); + var creds; + try { + creds = new ApplicationTokenCredentials(clientId, domain, secret, options); + } catch (err) { + return callback(err); + } + return callback(null, creds); }; exports = module.exports; \ No newline at end of file diff --git a/ClientRuntimes/NodeJS/ms-rest-azure/lib/msRestAzure.js b/ClientRuntimes/NodeJS/ms-rest-azure/lib/msRestAzure.js index aec8292d060b5..62d4b7944c636 100644 --- a/ClientRuntimes/NodeJS/ms-rest-azure/lib/msRestAzure.js +++ b/ClientRuntimes/NodeJS/ms-rest-azure/lib/msRestAzure.js @@ -14,4 +14,5 @@ exports.TokenCredentials = require('ms-rest').TokenCredentials; exports.generateUuid = require('./utils').generateUuid; exports.interactiveLogin = require('./login').interactive; exports.loginWithUsernamePassword = require('./login').withUsernamePassword; +exports.loginWithServicePrincipalSecret = require('./login').withServicePrincipalSecret; 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 3e196bf039e69..d6f7444e9f900 100644 --- a/ClientRuntimes/NodeJS/ms-rest-azure/package.json +++ b/ClientRuntimes/NodeJS/ms-rest-azure/package.json @@ -5,7 +5,7 @@ "email": "azsdkteam@microsoft.com", "url": "https://github.com/Azure/AutoRest" }, - "version": "1.14.0", + "version": "1.14.2", "description": "Client Runtime for Node.js Azure client libraries generated using AutoRest", "tags": [ "node", "microsoft", "autorest", "azure", "clientruntime" ], "keywords": [ "node", "microsoft", "autorest", "azure", "clientruntime" ], @@ -18,7 +18,7 @@ "async": "0.2.7", "uuid": "2.0.1", "adal-node": "^0.1.17", - "ms-rest": "^1.14.0", + "ms-rest": "^1.14.2", "moment": "^2.6.0" }, "devDependencies": { diff --git a/ClientRuntimes/NodeJS/ms-rest/package.json b/ClientRuntimes/NodeJS/ms-rest/package.json index 8dabfaf0e6a6a..f65673c6a39d6 100644 --- a/ClientRuntimes/NodeJS/ms-rest/package.json +++ b/ClientRuntimes/NodeJS/ms-rest/package.json @@ -5,15 +5,12 @@ "email": "azsdkteam@microsoft.com", "url": "https://github.com/Azure/AutoRest" }, - "version": "1.14.0", + "version": "1.14.2", "description": "Client Runtime for Node.js client libraries generated using AutoRest", "tags": ["node", "microsoft", "autorest", "clientruntime"], "keywords": ["node", "microsoft", "autorest", "clientruntime"], "main": "./lib/msRest.js", "typings": "./lib/index.d.ts", - "engines": { - "node": ">= 0.10.0" - }, "license": "MIT", "dependencies": { "underscore": "^1.4.0",