From ad047dcc91069c94b2e003875bc2c3d5fec66dff Mon Sep 17 00:00:00 2001 From: Toon De Coninck Date: Fri, 2 Jun 2017 01:12:57 +0200 Subject: [PATCH] Added tokenProvider functionality --- README.md | 21 +- package.json | 2 + src/Auth0RestClient.js | 71 +++++ src/index.js | 3 +- src/management/BlacklistedTokensManager.js | 5 +- src/management/ClientGrantsManager.js | 5 +- src/management/ClientsManager.js | 5 +- src/management/ConnectionsManager.js | 5 +- src/management/DeviceCredentialsManager.js | 7 +- src/management/EmailProviderManager.js | 5 +- src/management/JobsManager.js | 5 +- src/management/LogsManager.js | 5 +- src/management/ManagementTokenProvider.js | 121 ++++++++ src/management/ResourceServersManager.js | 4 +- src/management/RulesManager.js | 5 +- src/management/StatsManager.js | 5 +- src/management/TenantManager.js | 4 +- src/management/TicketsManager.js | 4 +- src/management/UsersManager.js | 15 +- src/management/index.js | 48 ++- test/auth0-rest-client.tests.js | 119 ++++++++ test/auth0.tests.js | 6 + test/management/management-client.tests.js | 38 ++- .../management-token-provider.tests.js | 288 ++++++++++++++++++ 24 files changed, 734 insertions(+), 62 deletions(-) create mode 100644 src/Auth0RestClient.js create mode 100644 src/management/ManagementTokenProvider.js create mode 100644 test/auth0-rest-client.tests.js create mode 100644 test/management/management-token-provider.tests.js diff --git a/README.md b/README.md index 81d732d37..7a404fea6 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,6 @@ var auth0 = new AuthenticationClient({ ## Management API Client The Auth0 Management API is meant to be used by back-end servers or trusted parties performing administrative tasks. Generally speaking, anything that can be done through the Auth0 dashboard (and more) can also be done through this API. - Initialize your client class with an API v2 token and a domain. ```js @@ -43,7 +42,7 @@ var management = new ManagementClient({ }); ``` -> When using at browser you should use `telemetry: false`. +> Note: When using at browser you should use `telemetry: false`. To obtain a Management API token from your node backend, you can use Client Credentials Grant using your registered Auth0 Non Interactive Clients @@ -71,6 +70,24 @@ auth0.clientCredentialsGrant({ Also you can request a token when the user authenticates using any of our client side SDKs, e.g. [auth0.js](https://github.com/auth0/auth0.js). +Or initialize your client class with the ManagementTokenProvider. +~~~js +var ManagementClient = require('auth0').ManagementClient; +var ManagementTokenProvider = require('auth0').ManagementTokenProvider; +var auth0 = new ManagementClient({ + domain: '{YOUR_ACCOUNT}.auth0.com', + tokenProvider: new ManagementTokenProvider({ + clientId: '{YOUR_NON_INTERACTIVE_CLIENT_ID}', + clientSecret: '{YOUR_NON_INTERACTIVE_CLIENT_SECRET}', + domain: '{YOUR_ACCOUNT}.auth0.com', + scope: '{MANAGEMENT_API_SCOPES}', + useCache: true //default + }) + }); +~~~ + +> Note: When using at browser you should use `telemetry: false`. + ## Promises and callbacks Be aware that all methods can be used with promises or callbacks. However, when a callback is provided no promise will be returned. diff --git a/package.json b/package.json index 82523b0ef..9d81232c5 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "homepage": "https://github.com/auth0/node-auth0", "dependencies": { "bluebird": "^2.10.2", + "lru-memoizer": "^1.11.1", + "object.assign": "^4.0.4", "request": "^2.81.0", "rest-facade": "^1.5.0" }, diff --git a/src/Auth0RestClient.js b/src/Auth0RestClient.js new file mode 100644 index 000000000..0b29e8e06 --- /dev/null +++ b/src/Auth0RestClient.js @@ -0,0 +1,71 @@ +var RestClient = require('rest-facade').Client; +var Promise = require('bluebird'); +var ArgumentError = require('rest-facade').ArgumentError; + +var Auth0RestClient = function (resourceUrl, options, provider) { + if (resourceUrl === null || resourceUrl === undefined) { + throw new ArgumentError('Must provide a Resource Url'); + } + + if ('string' !== typeof resourceUrl || resourceUrl.length === 0) { + throw new ArgumentError('The provided Resource Url is invalid'); + } + + if (options === null || typeof options !== 'object') { + throw new ArgumentError('Must provide options'); + } + + this.options = options; + this.provider = provider; + this.restClient = new RestClient(resourceUrl, options); + + this.wrappedProvider = function (method, args) { + if (!this.provider) { + return this.restClient[method].apply(this.restClient, args); + } + + var callback; + if(args && args[args.length -1] instanceof Function){ + callback = args[args.length -1]; + } + + var self = this; + return this.provider.getAccessToken() + .then(function (access_token) { + self.options.headers['Authorization'] = 'Bearer ' + access_token; + return self.restClient[method].apply(self.restClient, args); + }).catch(function(err){ + if(callback){ + return callback(err); + } + return Promise.reject(err); + }); + } +}; + +Auth0RestClient.prototype.getAll = function ( /* [params], [callback] */ ) { + return this.wrappedProvider('getAll', arguments); +}; + + +Auth0RestClient.prototype.get = function ( /* [params], [callback] */ ) { + return this.wrappedProvider('get', arguments); +} + +Auth0RestClient.prototype.create = function ( /* [params], [callback] */ ) { + return this.wrappedProvider('create', arguments); +} + +Auth0RestClient.prototype.patch = function ( /* [params], [callback] */ ) { + return this.wrappedProvider('patch', arguments); +} + +Auth0RestClient.prototype.update = function ( /* [params], [callback] */ ) { + return this.wrappedProvider('update', arguments); +} + +Auth0RestClient.prototype.delete = function ( /* [params], [callback] */ ) { + return this.wrappedProvider('delete', arguments); +} + +module.exports = Auth0RestClient; diff --git a/src/index.js b/src/index.js index b36054859..c5032cd50 100644 --- a/src/index.js +++ b/src/index.js @@ -6,5 +6,6 @@ module.exports = { ManagementClient: require('./management'), - AuthenticationClient: require('./auth') + AuthenticationClient: require('./auth'), + ManagementTokenProvider: require('./management/ManagementTokenProvider') }; diff --git a/src/management/BlacklistedTokensManager.js b/src/management/BlacklistedTokensManager.js index 83d8d4a68..f6e03f147 100644 --- a/src/management/BlacklistedTokensManager.js +++ b/src/management/BlacklistedTokensManager.js @@ -1,7 +1,6 @@ -var RestClient = require('rest-facade').Client; var ArgumentError = require('rest-facade').ArgumentError; var utils = require('../utils'); - +var Auth0RestClient = require('../Auth0RestClient'); /** * @class BlacklistedTokensManager @@ -44,7 +43,7 @@ var BlacklistedTokensManager = function (options) { * * @type {external:RestClient} */ - this.resource = new RestClient(options.baseUrl + '/blacklists/tokens', clientOptions); + this.resource = new Auth0RestClient(options.baseUrl + '/blacklists/tokens', clientOptions, options.tokenProvider); }; diff --git a/src/management/ClientGrantsManager.js b/src/management/ClientGrantsManager.js index 9f74e1882..87db12bc7 100644 --- a/src/management/ClientGrantsManager.js +++ b/src/management/ClientGrantsManager.js @@ -1,7 +1,6 @@ -var RestClient = require('rest-facade').Client; var ArgumentError = require('rest-facade').ArgumentError; var utils = require('../utils'); - +var Auth0RestClient = require('../Auth0RestClient'); /** * @class ClientGrantsManager @@ -46,7 +45,7 @@ var ClientGrantsManager = function (options) { * * @type {external:RestClient} */ - this.resource = new RestClient(options.baseUrl + '/client-grants/:id', clientOptions); + this.resource = new Auth0RestClient(options.baseUrl + '/client-grants/:id', clientOptions, options.tokenProvider); }; diff --git a/src/management/ClientsManager.js b/src/management/ClientsManager.js index f8c9642f3..8444d3fe1 100644 --- a/src/management/ClientsManager.js +++ b/src/management/ClientsManager.js @@ -1,7 +1,6 @@ -var RestClient = require('rest-facade').Client; var ArgumentError = require('rest-facade').ArgumentError; var utils = require('../utils'); - +var Auth0RestClient = require('../Auth0RestClient'); /** * @class ClientsManager @@ -49,7 +48,7 @@ var ClientsManager = function (options) { * * @type {external:RestClient} */ - this.resource = new RestClient(options.baseUrl + '/clients/:client_id', clientOptions); + this.resource = new Auth0RestClient(options.baseUrl + '/clients/:client_id', clientOptions, options.tokenProvider); }; diff --git a/src/management/ConnectionsManager.js b/src/management/ConnectionsManager.js index fc69f04f2..8f4ac579c 100644 --- a/src/management/ConnectionsManager.js +++ b/src/management/ConnectionsManager.js @@ -1,7 +1,6 @@ -var RestClient = require('rest-facade').Client; var ArgumentError = require('rest-facade').ArgumentError; var utils = require('../utils'); - +var Auth0RestClient = require('../Auth0RestClient'); /** * @class ConnectionsManager @@ -43,7 +42,7 @@ var ConnectionsManager = function (options) { * * @type {external:RestClient} */ - this.resource = new RestClient(options.baseUrl + '/connections/:id ', apiOptions); + this.resource = new Auth0RestClient(options.baseUrl + '/connections/:id ', apiOptions, options.tokenProvider); }; diff --git a/src/management/DeviceCredentialsManager.js b/src/management/DeviceCredentialsManager.js index aae9025d2..6d46491a4 100644 --- a/src/management/DeviceCredentialsManager.js +++ b/src/management/DeviceCredentialsManager.js @@ -1,7 +1,6 @@ -var RestClient = require('rest-facade').Client; var ArgumentError = require('rest-facade').ArgumentError; var utils = require('../utils'); - +var Auth0RestClient = require('../Auth0RestClient'); /** * Simple facade for consuming a REST API endpoint. @@ -49,9 +48,9 @@ var DeviceCredentialsManager = function (options) { * {@link https://auth0.com/docs/api/v2#!/Device_Credentials * Auth0 DeviceCredentialsManagers endpoint}. * - * @type {external:RestDeviceCredentialsManager} + * @type {external:RestClient} */ - this.resource = new RestClient(options.baseUrl + '/device-credentials/:id', clientOptions); + this.resource = new Auth0RestClient(options.baseUrl + '/device-credentials/:id', clientOptions, options.tokenProvider); }; diff --git a/src/management/EmailProviderManager.js b/src/management/EmailProviderManager.js index 33ade9a0c..2e672a7c9 100644 --- a/src/management/EmailProviderManager.js +++ b/src/management/EmailProviderManager.js @@ -1,7 +1,6 @@ -var RestClient = require('rest-facade').Client; var ArgumentError = require('rest-facade').ArgumentError; var utils = require('../utils'); - +var Auth0RestClient = require('../Auth0RestClient'); /** * Simple facade for consuming a REST API endpoint. @@ -50,7 +49,7 @@ var EmailProviderManager = function (options) { * * @type {external:RestClient} */ - this.resource = new RestClient(options.baseUrl + '/emails/provider', clientOptions); + this.resource = new Auth0RestClient(options.baseUrl + '/emails/provider', clientOptions, options.tokenProvider); }; diff --git a/src/management/JobsManager.js b/src/management/JobsManager.js index 53490fe2a..ea79adc79 100644 --- a/src/management/JobsManager.js +++ b/src/management/JobsManager.js @@ -3,9 +3,8 @@ var extend = require('util')._extend; var Promise = require('bluebird'); var fs = require('fs'); -var RestClient = require('rest-facade').Client; var ArgumentError = require('rest-facade').ArgumentError; - +var Auth0RestClient = require('../Auth0RestClient'); /** * Simple facade for consuming a REST API endpoint. @@ -51,7 +50,7 @@ var JobsManager = function (options){ * * @type {external:RestClient} */ - this.jobs = new RestClient(options.baseUrl + '/jobs/:id', clientOptions); + this.jobs = new Auth0RestClient(options.baseUrl + '/jobs/:id', clientOptions, options.tokenProvider); }; diff --git a/src/management/LogsManager.js b/src/management/LogsManager.js index 0070f4b3e..332dbbdf0 100644 --- a/src/management/LogsManager.js +++ b/src/management/LogsManager.js @@ -1,7 +1,6 @@ -var RestClient = require('rest-facade').Client; var ArgumentError = require('rest-facade').ArgumentError; var utils = require('../utils'); - +var Auth0RestClient = require('../Auth0RestClient'); /** * @class LogsManager @@ -43,7 +42,7 @@ var LogsManager = function (options) { * * @type {external:RestClient} */ - this.resource = new RestClient(options.baseUrl + '/logs/:id ', apiOptions); + this.resource = new Auth0RestClient(options.baseUrl + '/logs/:id ', apiOptions, options.tokenProvider); }; /** diff --git a/src/management/ManagementTokenProvider.js b/src/management/ManagementTokenProvider.js new file mode 100644 index 000000000..c0638c6fb --- /dev/null +++ b/src/management/ManagementTokenProvider.js @@ -0,0 +1,121 @@ +var ArgumentError = require('rest-facade').ArgumentError; +var assign = Object.assign || require('object.assign'); +var AuthenticationClient = require('../auth'); +var memoizer = require('lru-memoizer'); +var Promise = require('bluebird'); + +var BASE_URL_FORMAT = 'https://%s'; +var DEFAULT_OPTIONS = { useCache : true }; + +/** + * @class ManagementTokenProvider + * Auth0 Management API Token Provider. + * @constructor + * @memberOf module:management + * + * @param {Object} options Options for the ManagementTokenProvider. + * @param {String} options.domain ManagementClient server domain. + * @param {String} options.clientId Non Interactive Client Id. + * @param {String} options.clientSecret Non Interactive Client Secret. + * @param {String} [options.useCache] Enable caching (default true) + * @param {String} [options.scope] Scope + * @example + * Initialize a Management Token Provider class. + * + * + * var ManagementTokenProvider = require('auth0').ManagementTokenProvider; + * var provider = new ManagementTokenProvider({ + * clientId: '{YOUR_NON_INTERACTIVE_CLIENT_ID}', + * clientSecret: '{YOUR_NON_INTERACTIVE_CLIENT_SECRET}', + * domain: '{YOUR_ACCOUNT}.auth0.com' + * }); + */ +var ManagementTokenProvider = function (options) { + if (!options || typeof options !== 'object') { + throw new ArgumentError('Options must be an object'); + } + + var params = assign({}, DEFAULT_OPTIONS, options); + + if (!params.clientId || params.clientId.length === 0) { + throw new ArgumentError('Must provide a Client Id'); + } + + if (!params.clientSecret || params.clientSecret.length === 0) { + throw new ArgumentError('Must provide a Client Secret'); + } + + if(typeof params.useCache !== 'boolean'){ + throw new ArgumentError('The useCache must be a boolean'); + } + + this.options = params; + + this.authenticationClient = new AuthenticationClient({ + domain: params.domain, + clientId: params.clientId, + clientSecret: params.clientSecret, + telemetry: params.telemetry + }); +} + +/** + * Returns the access_token. + * + * @method getAccessToken + * @memberOf module:management.ManagementTokenProvider.prototype + * + * @return {Promise} Promise returning an access_token. + */ +ManagementTokenProvider.prototype.getAccessToken = function () { + + if(this.options.useCache){ + return this.getCachedAccessToken(this.options.domain, this.options.clientId, this.options.scope) + .then(function (data) { + return data.access_token + }); + }else{ + return this.clientCredentialsGrant(this.options.domain, this.options.scope) + .then(function (data) { + return data.access_token + }); + } +} + +ManagementTokenProvider.prototype.getCachedAccessToken = Promise.promisify( + memoizer({ + load: function (domain, clientId, scope, callback) { + this.clientCredentialsGrant(domain, scope) + .then(function (data) { + callback(null, data); + }) + .catch(function (err) { + callback(err); + }); + }, + hash: function (domain, clientId, scope) { + return domain + '-' + clientId + '-' + scope; + }, + itemMaxAge: function (domain, clientid, scope, data) { + // if the expires_in is lower than 10 seconds, do not subtract 10 additional seconds. + if (data.expires_in && data.expires_in < 10 /* seconds */){ + return data.expires_in * 1000; + }else if(data.expires_in){ + // Subtract 10 seconds from expires_in to fetch a new one, before it expires. + return data.expires_in * 1000 - 10000/* milliseconds */; + } + return 3600 * 1000 // 1h; + }, + max: 100, + maxAge: 1000 * 60 + }) +); + +ManagementTokenProvider.prototype.clientCredentialsGrant = function (domain, scope) { + return this.authenticationClient.clientCredentialsGrant({ + audience: 'https://' + domain + '/api/v2/', + scope: scope + }); +}; + +module.exports = ManagementTokenProvider; diff --git a/src/management/ResourceServersManager.js b/src/management/ResourceServersManager.js index 178bbb632..44de35ddc 100644 --- a/src/management/ResourceServersManager.js +++ b/src/management/ResourceServersManager.js @@ -1,6 +1,6 @@ -var RestClient = require('rest-facade').Client; var ArgumentError = require('rest-facade').ArgumentError; var utils = require('../utils'); +var Auth0RestClient = require('../Auth0RestClient'); /** * @class ResourceServersManager @@ -48,7 +48,7 @@ var ResourceServersManager = function (options) { * * @type {external:RestClient} */ - this.resource = new RestClient(options.baseUrl + '/resource-servers/:id', apiOptions); + this.resource = new Auth0RestClient(options.baseUrl + '/resource-servers/:id', apiOptions, options.tokenProvider); }; /** diff --git a/src/management/RulesManager.js b/src/management/RulesManager.js index 45a2d7052..7073db00e 100644 --- a/src/management/RulesManager.js +++ b/src/management/RulesManager.js @@ -1,7 +1,6 @@ -var RestClient = require('rest-facade').Client; var ArgumentError = require('rest-facade').ArgumentError; var utils = require('../utils'); - +var Auth0RestClient = require('../Auth0RestClient'); /** * Simple facade for consuming a REST API endpoint. @@ -50,7 +49,7 @@ var RulesManager = function (options) { * * @type {external:RestClient} */ - this.resource = new RestClient(options.baseUrl + '/rules/:id ', apiOptions); + this.resource = new Auth0RestClient(options.baseUrl + '/rules/:id ', apiOptions, options.tokenProvider); }; diff --git a/src/management/StatsManager.js b/src/management/StatsManager.js index a7aaae0de..067a0972a 100644 --- a/src/management/StatsManager.js +++ b/src/management/StatsManager.js @@ -1,6 +1,5 @@ -var RestClient = require('rest-facade').Client; var ArgumentError = require('rest-facade').ArgumentError; - +var Auth0RestClient = require('../Auth0RestClient'); /** * Simple facade for consuming a REST API endpoint. @@ -44,7 +43,7 @@ var StatsManager = function (options){ * * @type {external:RestClient} */ - this.stats = new RestClient(options.baseUrl + '/stats/:type', clientOptions); + this.stats = new Auth0RestClient(options.baseUrl + '/stats/:type', clientOptions, options.tokenProvider); }; diff --git a/src/management/TenantManager.js b/src/management/TenantManager.js index 6a78c4edf..41429bd05 100644 --- a/src/management/TenantManager.js +++ b/src/management/TenantManager.js @@ -1,5 +1,5 @@ -var RestClient = require('rest-facade').Client; var ArgumentError = require('rest-facade').ArgumentError; +var Auth0RestClient = require('../Auth0RestClient'); /** @@ -44,7 +44,7 @@ var TenantManager = function (options){ * * @type {external:RestClient} */ - this.tenant = new RestClient(options.baseUrl + '/tenants/settings', clientOptions); + this.tenant = new Auth0RestClient(options.baseUrl + '/tenants/settings', clientOptions, options.tokenProvider); }; /** diff --git a/src/management/TicketsManager.js b/src/management/TicketsManager.js index 12bb42b4b..5061e73ff 100644 --- a/src/management/TicketsManager.js +++ b/src/management/TicketsManager.js @@ -1,5 +1,5 @@ -var RestClient = require('rest-facade').Client; var ArgumentError = require('rest-facade').ArgumentError; +var Auth0RestClient = require('../Auth0RestClient'); /** @@ -33,7 +33,7 @@ var TicketsManager = function (options){ * * @type {external:RestClient} */ - this.ticket = new RestClient(options.baseUrl + '/tickets/:type', clientOptions); + this.ticket = new Auth0RestClient(options.baseUrl + '/tickets/:type', clientOptions, options.tokenProvider); }; diff --git a/src/management/UsersManager.js b/src/management/UsersManager.js index a6d1c3d23..4f1ed8bf0 100644 --- a/src/management/UsersManager.js +++ b/src/management/UsersManager.js @@ -1,6 +1,5 @@ -var RestClient = require('rest-facade').Client; var ArgumentError = require('rest-facade').ArgumentError; - +var Auth0RestClient = require('../Auth0RestClient'); /** * Simple facade for consuming a REST API endpoint. @@ -37,8 +36,8 @@ var UsersManager = function (options){ headers: options.headers, query: { repeatParams: false } }; - - this.users = new RestClient(options.baseUrl + '/users/:id', clientOptions); + + this.users = new Auth0RestClient(options.baseUrl + '/users/:id', clientOptions, options.tokenProvider); /** * Provides an abstraction layer for consuming the @@ -47,28 +46,28 @@ var UsersManager = function (options){ * * @type {external:RestClient} */ - this.multifactor = new RestClient(options.baseUrl + '/users/:id/multifactor/:provider', clientOptions); + this.multifactor = new Auth0RestClient(options.baseUrl + '/users/:id/multifactor/:provider', clientOptions, options.tokenProvider); /** * Provides a simple abstraction layer for linking user accounts. * * @type {external:RestClient} */ - this.identities = new RestClient(options.baseUrl + '/users/:id/identities/:provider/:user_id', clientOptions); + this.identities = new Auth0RestClient(options.baseUrl + '/users/:id/identities/:provider/:user_id', clientOptions, options.tokenProvider); /** * Provides a simple abstraction layer for user logs * * @type {external:RestClient} */ - this.userLogs = new RestClient(options.baseUrl + '/users/:id/logs', clientOptions); + this.userLogs = new Auth0RestClient(options.baseUrl + '/users/:id/logs', clientOptions, options.tokenProvider); /** * Provides an abstraction layer for retrieving Guardian enrollments. * * @type {external:RestClient} */ - this.enrollments = new RestClient(options.baseUrl + '/users/:id/enrollments', clientOptions); + this.enrollments = new Auth0RestClient(options.baseUrl + '/users/:id/enrollments', clientOptions, options.tokenProvider); }; diff --git a/src/management/index.js b/src/management/index.js index 16eabf460..9b67c5aed 100644 --- a/src/management/index.js +++ b/src/management/index.js @@ -22,6 +22,7 @@ var JobsManager = require('./JobsManager'); var TicketsManager = require('./TicketsManager'); var LogsManager = require('./LogsManager'); var ResourceServersManager = require('./ResourceServersManager'); +var ManagementTokenProvider = require('./ManagementTokenProvider'); var BASE_URL_FORMAT = 'https://%s/api/v2'; @@ -47,33 +48,62 @@ var BASE_URL_FORMAT = 'https://%s/api/v2'; * token: '{YOUR_API_V2_TOKEN}', * domain: '{YOUR_ACCOUNT}.auth0.com' * }); + * + * + * @example + * Initialize your client class with the Management Token Provider. + * + * + * var ManagementClient = require('auth0').ManagementClient; + * var ManagementTokenProvider = require('auth0').ManagementTokenProvider; + * var auth0 = new ManagementClient({ + * tokenProvider: new ManagementTokenProvider({ + * clientId: '{YOUR_NON_INTERACTIVE_CLIENT_ID}', + * clientSecret: '{YOUR_NON_INTERACTIVE_CLIENT_SECRET}', + * domain: '{YOUR_ACCOUNT}.auth0.com' + * }) + * }); * - * @param {Object} options Options for the ManagementClient SDK. - * @param {String} options.token API access token. - * @param {String} [options.domain] ManagementClient server domain. + * @param {Object} options Options for the ManagementClient SDK. + * Required properties depend on the way initialization is performed as you can see in the examples. + * @param {String} [options.token] API access token. + * @param {String} [options.domain] ManagementClient server domain. + * @param {String} [options.tokenProvider] Token Provider. + * */ var ManagementClient = function (options) { if (!options || typeof options !== 'object') { throw new ArgumentError('Management API SDK options must be an object'); } - if (!options.token || options.token.length === 0) { - throw new ArgumentError('An access token must be provided'); + if (!options.domain || options.domain.length === 0) { + throw new ArgumentError('Must provide a Domain'); } - if (!options.domain || options.domain.length === 0) { - throw new ArgumentError('Must provide a domain'); + if(!options.tokenProvider){ + if (!options.token || options.token.length === 0) { + throw new ArgumentError('Must provide a Token'); + } + }else{ + if(!options.tokenProvider.getAccessToken || typeof options.tokenProvider.getAccessToken !== 'function'){ + throw new ArgumentError('The tokenProvider does not have a function getAccessToken'); + } + this.tokenProvider = options.tokenProvider; } var managerOptions = { headers: { - 'Authorization': 'Bearer ' + options.token, 'User-agent': 'node.js/' + process.version.replace('v', ''), 'Content-Type': 'application/json' }, - baseUrl: util.format(BASE_URL_FORMAT, options.domain) + baseUrl: util.format(BASE_URL_FORMAT, options.domain), + tokenProvider: this.tokenProvider }; + if (options.token && options.token.length !== 0) { + managerOptions.headers['Authorization'] = 'Bearer ' + options.token; + } + if (options.telemetry !== false) { var telemetry = jsonToBase64(options.clientInfo || this.getClientInfo()); diff --git a/test/auth0-rest-client.tests.js b/test/auth0-rest-client.tests.js new file mode 100644 index 000000000..f65d62532 --- /dev/null +++ b/test/auth0-rest-client.tests.js @@ -0,0 +1,119 @@ +var expect = require('chai').expect; +var nock = require('nock'); + +var ArgumentError = require('rest-facade').ArgumentError; +var ManagementTokenProvider = require('../src/management/ManagementTokenProvider') +var Auth0RestClient = require('../src/Auth0RestClient'); + +var API_URL = 'https://tenant.auth0.com'; + +describe('Auth0RestClient', function () { + before(function () { + this.providerMock = { + getAccessToken: function () { + return Promise.resolve('access_token'); + } + } + }); + + it('should raise an error when no resource Url is provided', function () { + expect(Auth0RestClient) + .to.throw(ArgumentError, 'Must provide a Resource Url'); + }); + + it('should raise an error when resource Url is invalid', function () { + var client = Auth0RestClient.bind(null, ''); + expect(client) + .to.throw(ArgumentError, 'The provided Resource Url is invalid'); + }); + + it('should raise an error when no options is provided', function () { + var client = Auth0RestClient.bind(null, '/some-resource'); + expect(client) + .to.throw(ArgumentError, 'Must provide options'); + }); + + it('should accept a callback', function (done) { + nock(API_URL).get('/some-resource') + .reply(200, { data: 'value' }); + + var options = { + headers: {} + } + var client = new Auth0RestClient(API_URL + '/some-resource', options, this.providerMock); + client.getAll(function (err, data) { + expect(data).to.deep.equal({ data: 'value' }); + done(); + nock.cleanAll() + }); + }); + + it('should return a promise if no callback is given', function (done) { + nock(API_URL).get('/some-resource') + .reply(200, { data: 'value' }); + + var options = { + headers: {} + } + + var client = new Auth0RestClient(API_URL + '/some-resource', options, this.providerMock); + client.getAll().then(function(data){ + expect(data).to.deep.equal({ data: 'value' }); + done(); + nock.cleanAll() + }); + }); + + it('should accept a callback and handle errors', function (done) { + var providerMock = { + getAccessToken: function () { + return Promise.reject(new Error('Some Error')); + } + } + + nock(API_URL).get('/some-resource') + .reply(500); + + var options = { + headers: {} + } + var client = new Auth0RestClient(API_URL + '/some-resource', options, providerMock); + client.getAll(function (err, data) { + expect(err).to.not.null; + expect(err.message).to.be.equal('Some Error'); + done(); + nock.cleanAll(); + }); + }); + + it('should set access token as Authorization header in options object', function (done) { + nock(API_URL).get('/some-resource') + .reply(200); + + var options = { + headers: {} + } + + var client = new Auth0RestClient(API_URL + '/some-resource', options, this.providerMock); + client.getAll().then(function(data){ + expect(client.options.headers['Authorization']).to.be.equal('Bearer access_token'); + done(); + nock.cleanAll(); + }); + }); + + it('should catch error when provider.getAccessToken throws an error', function (done) { + var providerMock = { + getAccessToken: function () { + return Promise.reject(new Error('Some Error')); + } + } + + var client = new Auth0RestClient('/some-resource', {}, providerMock); + client.getAll().catch(function (err) { + expect(err).to.not.null; + expect(err.message).to.be.equal('Some Error'); + done(); + }) + }); +}); diff --git a/test/auth0.tests.js b/test/auth0.tests.js index bfd8fbb30..a07f03cf1 100644 --- a/test/auth0.tests.js +++ b/test/auth0.tests.js @@ -3,6 +3,7 @@ var expect = require('chai').expect; var auth0 = require('../src'); var AuthenticationClient = require('../src/auth'); var ManagementClient = require('../src/management'); +var ManagementTokenProvider = require('../src/management/ManagementTokenProvider') describe('Auth0 module', function () { @@ -18,4 +19,9 @@ describe('Auth0 module', function () { .to.equal(ManagementClient); }); + + it('should expose the ManagementTokenProvider', function () { + expect(auth0.ManagementTokenProvider) + .to.equal(ManagementTokenProvider); + }); }); diff --git a/test/management/management-client.tests.js b/test/management/management-client.tests.js index 168eb6bab..3c6686ed7 100644 --- a/test/management/management-client.tests.js +++ b/test/management/management-client.tests.js @@ -23,23 +23,51 @@ describe('ManagementClient', function () { .to.throw(ArgumentError, 'Management API SDK options must be an object'); }); - it('should raise an error when the token is not valid', function () { var options = { token: '', domain: 'tenant.auth.com' }; var client = ManagementClient.bind(null, options); expect(client) - .to.throw(ArgumentError, 'An access token must be provided'); + .to.throw(ArgumentError, 'Must provide a Token'); }); - it('should raise an error when the domain is not valid', function () { - var client = ManagementClient.bind(null, { token: 'token', domain: '' }); + var client = ManagementClient.bind(null, { token: 'token' }); + + expect(client) + .to.throw(ArgumentError, 'Must provide a Domain'); + }); + + it('should raise an error when the token provider does not have a function getAccessToken', function () { + var client = ManagementClient.bind(null, { tokenProvider : {} }); + + expect(client) + .to.throw(ArgumentError, 'Must provide a Domain'); + }); + + it('should raise an error when the domain is not valid and a tokenProvider is specified', function () { + var client = ManagementClient.bind(null, { domain: 'domain', tokenProvider: {} }); + + expect(client) + .to.throw(ArgumentError, 'The tokenProvider does not have a function getAccessToken'); + }); + + it('should raise an error when the token provider does have a property getAccessToken that is not a function', function () { + var client = ManagementClient.bind(null, { domain: 'domain', tokenProvider : { getAccessToken: [] } }); expect(client) - .to.throw(ArgumentError, 'Must provide a domain'); + .to.throw(ArgumentError, 'The tokenProvider does not have a function getAccessToken'); }); + it('should set the tokenProvider instance property if provider is passed', function () { + var fakeTokenProvider = { getAccessToken: function(){} }; + var options = { domain: 'domain', tokenProvider : fakeTokenProvider }; + var client = new ManagementClient(options); + + expect(client.tokenProvider) + .to.exist + .to.be.equal(fakeTokenProvider); + }); describe('instance properties', function () { var manager; diff --git a/test/management/management-token-provider.tests.js b/test/management/management-token-provider.tests.js new file mode 100644 index 000000000..a12c4f7a4 --- /dev/null +++ b/test/management/management-token-provider.tests.js @@ -0,0 +1,288 @@ +var expect = require('chai').expect; +var nock = require('nock'); +var assign = Object.assign || require('object.assign'); +var ArgumentError = require('rest-facade').ArgumentError; +var APIError = require('rest-facade').APIError; + +var ManagementTokenProvider = require('../../src/management/ManagementTokenProvider'); + +describe('ManagementTokenProvider', function () { + var defaultConfig = { clientId: 'clientId', clientSecret: 'clientSecret', 'domain': 'auth0-node-sdk.auth0.com' }; + + it('should expose an instance of ManagementTokenProvider', function () { + expect(new ManagementTokenProvider(defaultConfig)) + .to.exist + .to.be.an.instanceOf(ManagementTokenProvider); + }); + + it('should raise an error when no options object is provided', function () { + expect(ManagementTokenProvider) + .to.throw(ArgumentError, 'Options must be an object'); + }); + + it('should raise an error when the domain is not set', function () { + var provider = ManagementTokenProvider.bind(null, { clientId: 'clientId', clientSecret: 'clientSecret' }); + + expect(provider) + .to.throw(ArgumentError, 'Must provide a domain'); + }); + + it('should raise an error when the domain is not valid', function () { + var provider = ManagementTokenProvider.bind(null, { clientId: 'clientId', clientSecret: 'clientSecret', 'domain': '' }); + + expect(provider) + .to.throw(ArgumentError, 'Must provide a domain'); + }); + + it('should raise an error when the clientId is not set', function () { + var provider = ManagementTokenProvider.bind(null, { clientSecret: 'clientSecret', domain: 'domain' }); + + expect(provider) + .to.throw(ArgumentError, 'Must provide a Client Id'); + }); + + it('should raise an error when the clientId is not valid', function () { + var provider = ManagementTokenProvider.bind(null, { clientId: '', clientSecret: 'clientSecret', 'domain': 'domain' }); + + expect(provider) + .to.throw(ArgumentError, 'Must provide a Client Id'); + }); + + it('should raise an error when the clientSecret is not set', function () { + var provider = ManagementTokenProvider.bind(null, { clientId: 'clientId' , domain: 'domain' }); + + expect(provider) + .to.throw(ArgumentError, 'Must provide a Client Secret'); + }); + + it('should raise an error when the clientSecret is not valid', function () { + var provider = ManagementTokenProvider.bind(null, { clientId: 'clientId', clientSecret: '', 'domain': 'domain' }); + + expect(provider) + .to.throw(ArgumentError, 'Must provide a Client Secret'); + }); + + it('should raise an error when the useCache is not of type boolean', function () { + var provider = ManagementTokenProvider.bind(null, { clientId: 'clientId', clientSecret: 'clientSecret', 'domain': 'domain', 'useCache': 'false' }); + + expect(provider) + .to.throw(ArgumentError, 'The useCache must be a boolean'); + }); + + it('should set useCache to true when not specified', function () { + var provider = new ManagementTokenProvider({ clientId: 'clientId', clientSecret: 'clientSecret', 'domain': 'domain' }); + expect(provider.options.useCache).to.be.true; + }); + + it('should set useCache to true when passed as true', function () { + var provider = new ManagementTokenProvider({ clientId: 'clientId', clientSecret: 'clientSecret', 'domain': 'domain', 'useCache': true }); + expect(provider.options.useCache).to.be.true; + }); + + it('should set useCache to false when passed as false', function () { + var provider = new ManagementTokenProvider({ clientId: 'clientId', clientSecret: 'clientSecret', 'domain': 'domain', 'useCache': false }); + expect(provider.options.useCache).to.be.false; + }); + + it('should handle network errors correctly', function (done) { + var config = assign({}, defaultConfig); + config.domain = 'domain'; + var client = new ManagementTokenProvider(config); + + nock('https://' + config.domain) + .post('/oauth/token') + .reply(401); + + client.getAccessToken() + .catch(function(err){ + expect(err).to.exist + done(); + }); + }); + + it('should handle unauthorized errors correctly', function (done) { + var client = new ManagementTokenProvider(defaultConfig); + + nock('https://' + defaultConfig.domain) + .post('/oauth/token') + .reply(401); + + client.getAccessToken() + .catch(function(err){ + expect(err) + .to.exist + .to.be.an.instanceOf(APIError); + expect(err.statusCode).to.be.equal(401); + done(); + nock.cleanAll(); + }); + }); + + it('should return access token', function (done) { + var config = assign({}, defaultConfig); + config.domain = 'auth0-node-sdk-1.auth0.com' + var client = new ManagementTokenProvider(config); + + nock('https://' + config.domain) + .post('/oauth/token') + .reply(200, { + access_token: 'token', + expires_in: 3600 + }) + + client.getAccessToken() + .then(function(access_token){ + expect(access_token).to.exist; + expect(access_token).to.be.equal('token'); + done(); + nock.cleanAll(); + }); + }); + + it('should contain correct body payload', function (done) { + var config = assign({}, defaultConfig); + config.domain = 'auth0-node-sdk-2.auth0.com' + var client = new ManagementTokenProvider(config); + + nock('https://' + config.domain) + .post('/oauth/token',function(body) { + + expect(body.client_id).to.equal('clientId'); + expect(body.client_secret).to.equal('clientSecret'); + expect(body.grant_type).to.equal('client_credentials'); + expect(body.audience).to.equal('https://auth0-node-sdk-2.auth0.com/api/v2/'); + return true; + }) + .reply(function(uri, requestBody, cb) { + return cb(null, [200, { access_token: 'token', expires_in: 3600 }]); + }); + + client.getAccessToken() + .then(function(data){ + + done(); + nock.cleanAll(); + }); + }); + + it('should return access token from the cache the second call', function (done) { + var config = assign({}, defaultConfig); + config.domain = 'auth0-node-sdk-3.auth0.com' + var client = new ManagementTokenProvider(config); + + nock('https://' + config.domain) + .post('/oauth/token') + .once() + .reply(200, { + access_token: 'access_token', + expires_in: 3600 + }); + + client.getAccessToken() + .then(function(access_token){ + expect(access_token).to.exist; + expect(access_token).to.be.equal('access_token'); + + client.getAccessToken() + .then(function(access_token){ + expect(access_token).to.exist; + expect(access_token).to.be.equal('access_token'); + done(); + nock.cleanAll(); + }); + }); + }); + + it('should request new access token when cache is expired', function (done) { + var config = assign({}, defaultConfig); + config.domain = 'auth0-node-sdk-4.auth0.com' + var client = new ManagementTokenProvider(config); + + nock('https://' + config.domain) + .post('/oauth/token') + .reply(200, { + access_token: 'access_token', + expires_in: 1 / 40 // 1sec / 40 = 25ms + }) + .post('/oauth/token') + .reply(200, { + access_token: 'new_access_token', + expires_in: 3600 + }) + + client.getAccessToken() + .then(function(access_token){ + expect(access_token).to.exist; + expect(access_token).to.be.equal('access_token'); + + setTimeout(function() { + client.getAccessToken() + .then(function(access_token){ + expect(access_token).to.exist; + expect(access_token).to.be.equal('new_access_token'); + done(); + nock.cleanAll(); + }); + }, 40); // 40ms + }); + }); + + it('should return new access token on the second call when cache is disabled', function (done) { + var config = assign({ useCache: false }, defaultConfig); + config.domain = 'auth0-node-sdk-3.auth0.com' + var client = new ManagementTokenProvider(config); + + nock('https://' + config.domain) + .post('/oauth/token') + .reply(200, { + access_token: 'access_token', + expires_in: 3600 + }) + .post('/oauth/token') + .reply(200, { + access_token: 'new_access_token', + expires_in: 3600 + }) + + client.getAccessToken() + .then(function(access_token){ + expect(access_token).to.exist; + expect(access_token).to.be.equal('access_token'); + + client.getAccessToken() + .then(function(access_token){ + expect(access_token).to.exist; + expect(access_token).to.be.equal('new_access_token'); + done(); + nock.cleanAll(); + }).catch(function(err){ + expect.fail(); + done(); + nock.cleanAll(); + }); + }); + }); + + it('should pass the correct payload in the body of the oauth/token request', function (done) { + var config = assign({ scope: 'read:foo read:bar' }, defaultConfig); + var client = new ManagementTokenProvider(config); + + nock('https://' + config.domain) + .post('/oauth/token', function(payload){ + expect(payload).to.exist; + expect(payload.scope).to.be.equal('read:foo read:bar'); + expect(payload.client_id).to.be.equal('clientId'); + expect(payload.client_secret).to.be.equal('clientSecret'); + expect(payload.grant_type).to.be.equal('client_credentials'); + expect(payload.audience).to.be.equal('https://auth0-node-sdk.auth0.com/api/v2/'); + return true; + }) + .reply(200); + + client.getAccessToken() + .then(function(access_token){ + done(); + nock.cleanAll(); + }); + }); +});