From b89b86439e74fe04a29c47c12bf1c7274f591002 Mon Sep 17 00:00:00 2001 From: Adam Mcgrath Date: Thu, 12 Jan 2023 15:43:27 +0000 Subject: [PATCH] [SDK-3868] Add support for managing client credentials (#774) Co-authored-by: Rita Zerrizuela Co-authored-by: Jake Lacey --- src/management/ClientCredentialsManager.js | 112 +++++++ src/management/index.js | 10 +- test/management/client-credentials.tests.js | 348 ++++++++++++++++++++ 3 files changed, 469 insertions(+), 1 deletion(-) create mode 100644 src/management/ClientCredentialsManager.js create mode 100644 test/management/client-credentials.tests.js diff --git a/src/management/ClientCredentialsManager.js b/src/management/ClientCredentialsManager.js new file mode 100644 index 000000000..a343f8066 --- /dev/null +++ b/src/management/ClientCredentialsManager.js @@ -0,0 +1,112 @@ +const BaseManager = require('./BaseManager'); + +/** + * The client credentials class provides a simple abstraction for performing CRUD operations + * on Auth0 ClientCredentials. + */ +class ClientCredentialsManager extends BaseManager { + /** + * @param {object} options The client options. + * @param {string} options.baseUrl The URL of the API. + * @param {object} [options.headers] Headers to be included in all requests. + * @param {object} [options.retry] Retry Policy Config + */ + constructor(options) { + super(options); + + /** + * @type {external:RestClient} + */ + this.resource = this._getRestClient('/clients/:client_id/credentials/:credential_id'); + } + + /** + * Create a new client credential. + * + * @example + * var params = { client_id: CLIENT_ID }; + * + * management.clientCredentials.create(params, data, function (err, credential) { + * if (err) { + * // Handle error. + * } + * + * // The credential created. + * console.log({credential}); + * }); + * @param {string} params.client_id The client id. + * @param {object} data Client Credential data object. + * @param {Function} [cb] Callback function. + * @returns {Promise|undefined} + */ + create(...args) { + return this.resource.create(...args); + } + + /** + * Return a list of credentials for a given client. + * + * @example + * var params = { client_id: CLIENT_ID }; + * + * management.clientCredentials.getAll(params, function (err, credentials) { + * if (err) { + * // Handle error. + * } + * + * // A list of credentials associated with that client. + * console.log({credentials}); + * }); + * @param {string} params.client_id The client id. + * @param {Function} [cb] Callback function. + * @returns {Promise|undefined} + */ + getAll(...args) { + return this.resource.getAll(...args); + } + + /** + * Return a credential for a given client. + * + * @example + * var params = { client_id: CLIENT_ID, credential_id: CREDENTIAL_ID }; + * + * management.clientCredentials.getById(data, function (err, credential) { + * if (err) { + * // Handle error. + * } + * + * // A specific credential associated with that credential. + * console.log({credential}); + * }); + * @param {string} params.client_id The client id. + * @param {string} params.credential_id The credential id. + * @param {Function} [cb] Callback function. + * @returns {Promise|undefined} + */ + get(...args) { + return this.resource.get(...args); + } + + /** + * Delete a credential for a given client. + * + * @example + * var params = { client_id: CLIENT_ID, credential_id: CREDENTIAL_ID }; + * + * management.clientCredentials.delete(params, data, function (err) { + * if (err) { + * // Handle error. + * } + * }); + * @param {string} params.client_id The client id. + * @param {string} params.credential_id The credential id. + * @param {Function} [cb] Callback function. + * @returns {Promise|undefined} + */ + delete(...args) { + return this.resource.delete(...args); + } +} + +module.exports = ClientCredentialsManager; diff --git a/src/management/index.js b/src/management/index.js index 0754a0f7a..1a58ce354 100644 --- a/src/management/index.js +++ b/src/management/index.js @@ -4,6 +4,7 @@ const { jsonToBase64, generateClientInfo } = utils; const { ArgumentError } = require('rest-facade'); // Managers. +const ClientCredentialsManager = require('./ClientCredentialsManager'); const ClientsManager = require('./ClientsManager'); const ClientGrantsManager = require('./ClientGrantsManager'); const GrantsManager = require('./GrantsManager'); @@ -73,7 +74,6 @@ const MANAGEMENT_API_AUD_FORMAT = 'https://%s/api/v2/'; * cacheTTLInSeconds: 10 * } * }); - * * @example * Initialize your client class, by using a Non Interactive Client to fetch an access_token * via the Client Credentials Grant, providing a Private Key using the private_key_jwt token @@ -177,6 +177,14 @@ class ManagementClient { */ this.clients = new ClientsManager(managerOptions); + /** + * Simple abstraction for performing CRUD operations on the + * client credentials endpoint. + * + * @type {module:management.ClientCredentialsManager} + */ + this.clientCredentials = new ClientCredentialsManager(managerOptions); + /** * Simple abstraction for performing CRUD operations on the client grants * endpoint. diff --git a/test/management/client-credentials.tests.js b/test/management/client-credentials.tests.js new file mode 100644 index 000000000..f7bf32f19 --- /dev/null +++ b/test/management/client-credentials.tests.js @@ -0,0 +1,348 @@ +const { expect } = require('chai'); +const nock = require('nock'); + +const SRC_DIR = '../../src'; +const API_URL = 'https://tenant.auth0.com'; + +const ClientCredentialsManager = require(`${SRC_DIR}/management/ClientCredentialsManager`); +const { ArgumentError } = require('rest-facade'); + +describe('ClientsCredentialsManager', () => { + before(function () { + this.token = 'TOKEN'; + this.clientCredentials = new ClientCredentialsManager({ + headers: { authorization: `Bearer ${this.token}` }, + baseUrl: API_URL, + }); + }); + + describe('instance', () => { + const methods = ['getAll', 'get', 'create', 'delete']; + + methods.forEach((method) => { + it(`should have a ${method} method`, function () { + expect(this.clientCredentials[method]).to.exist.to.be.an.instanceOf(Function); + }); + }); + }); + + describe('#constructor', () => { + it('should error when no options are provided', () => { + expect(() => new ClientCredentialsManager()).to.throw( + ArgumentError, + 'Must provide manager options' + ); + }); + + it('should throw an error when no base URL is provided', () => { + const client = () => new ClientCredentialsManager({}); + + expect(client).to.throw(ArgumentError, 'Must provide a base URL for the API'); + }); + + it('should throw an error when the base URL is invalid', () => { + const client = () => new ClientCredentialsManager({ baseUrl: '' }); + + expect(client).to.throw(ArgumentError, 'The provided base URL is invalid'); + }); + }); + + describe('#create', () => { + const FAKE_CLIENT_ID = 'cli_1234'; + const PATH = `/clients/${FAKE_CLIENT_ID}/credentials`; + const options = { client_id: FAKE_CLIENT_ID }; + const data = { + pem: 'some-public-key', + name: 'some-super-name-given-to-my-public-key', + }; + + beforeEach(function () { + this.request = nock(API_URL).post(PATH).reply(200); + }); + + it('should accept a callback', function (done) { + this.clientCredentials.create(options, data, done); + }); + + it('should return a promise when no callback is given', function (done) { + this.clientCredentials + .create(options, data) + .then(done.bind(null, null)) + .catch(done.bind(null, null)); + }); + + it('should pass any errors to the "catch" handler', function (done) { + nock.cleanAll(); + + nock(API_URL).post(PATH).reply(500); + + this.clientCredentials.create(options, data).catch((err) => { + expect(err).to.exist; + done(); + }); + }); + + it(`should perform POST request to ${PATH}`, function (done) { + const { request } = this; + + this.clientCredentials.create(options, data).then(() => { + expect(request.isDone()).to.be.true; + done(); + }); + }); + + it('should include the token in the Authorization header', function (done) { + nock.cleanAll(); + + const request = nock(API_URL) + .post(PATH) + .matchHeader('Authorization', `Bearer ${this.token}`) + .reply(200); + + this.clientCredentials.create(options, data).then(() => { + expect(request.isDone()).to.be.true; + done(); + }); + }); + + it('should pass the data in the body of the request', function (done) { + nock.cleanAll(); + + const request = nock(API_URL).post(PATH, data).reply(200); + + this.clientCredentials.create(options, data).then(() => { + expect(request.isDone()).to.be.true; + + done(); + }); + }); + }); + describe('#getAll', () => { + const FAKE_CLIENT_ID = 'cli_1234'; + const PATH = `/clients/${FAKE_CLIENT_ID}/credentials`; + const options = { client_id: FAKE_CLIENT_ID }; + + beforeEach(function () { + this.request = nock(API_URL).get(PATH).reply(200); + }); + + it('should accept a callback', function (done) { + this.clientCredentials.getAll(options, done); + }); + + it('should return a promise when no callback is given', function (done) { + this.clientCredentials + .getAll(options) + .then(done.bind(null, null)) + .catch(done.bind(null, null)); + }); + + it('should pass any errors to the "catch" handler', function (done) { + nock.cleanAll(); + + nock(API_URL).get(PATH).reply(500); + + this.clientCredentials.getAll(options).catch((err) => { + expect(err).to.exist; + done(); + }); + }); + + it('should pass the body of the response to the "then" handler', function (done) { + nock.cleanAll(); + + const data = [{ test: true }]; + nock(API_URL).get(PATH).reply(200, data); + + this.clientCredentials.getAll(options).then((credentials) => { + expect(credentials).to.be.an.instanceOf(Array); + expect(credentials.length).to.equal(data.length); + expect(credentials[0].test).to.equal(data[0].test); + done(); + }); + }); + + it(`should perform GET request to ${PATH}`, function (done) { + const { request } = this; + + this.clientCredentials.getAll(options).then(() => { + expect(request.isDone()).to.be.true; + done(); + }); + }); + + it('should include the token in the Authorization header', function (done) { + nock.cleanAll(); + + const request = nock(API_URL) + .get(PATH) + .matchHeader('Authorization', `Bearer ${this.token}`) + .reply(200); + + this.clientCredentials.getAll(options).then(() => { + expect(request.isDone()).to.be.true; + done(); + }); + }); + + it('should pass the parameters in the query-string', function (done) { + nock.cleanAll(); + + const params = { + include_fields: true, + fields: 'test', + }; + + const request = nock(API_URL).get(PATH).query(params).reply(200); + + const combinedOptions = Object.assign({}, params, options); + + this.clientCredentials.getAll(combinedOptions).then(() => { + expect(request.isDone()).to.be.true; + done(); + }); + }); + }); + describe('#get', () => { + const FAKE_CLIENT_ID = 'cli_1234'; + const FAKE_CREDENTIAL_ID = 'cre_1234'; + const PATH = `/clients/${FAKE_CLIENT_ID}/credentials/${FAKE_CREDENTIAL_ID}`; + const options = { client_id: FAKE_CLIENT_ID, credential_id: FAKE_CREDENTIAL_ID }; + + beforeEach(function () { + this.request = nock(API_URL).get(PATH).reply(200); + }); + + it('should accept a callback', function (done) { + this.clientCredentials.get(options, done); + }); + + it('should return a promise when no callback is given', function (done) { + this.clientCredentials.get(options).then(done.bind(null, null)).catch(done.bind(null, null)); + }); + + it('should pass any errors to the "catch" handler', function (done) { + nock.cleanAll(); + + nock(API_URL).get(PATH).reply(500); + + this.clientCredentials.get(options).catch((err) => { + expect(err).to.exist; + done(); + }); + }); + + it('should pass the body of the response to the "then" handler', function (done) { + nock.cleanAll(); + + const data = [{ test: true }]; + nock(API_URL).get(PATH).reply(200, data); + + this.clientCredentials.get(options).then((credentials) => { + expect(credentials).to.be.an.instanceOf(Array); + expect(credentials.length).to.equal(data.length); + expect(credentials[0].test).to.equal(data[0].test); + done(); + }); + }); + + it(`should perform GET request to ${PATH}`, function (done) { + const { request } = this; + + this.clientCredentials.get(options).then(() => { + expect(request.isDone()).to.be.true; + done(); + }); + }); + + it('should include the token in the Authorization header', function (done) { + nock.cleanAll(); + + const request = nock(API_URL) + .get(PATH) + .matchHeader('Authorization', `Bearer ${this.token}`) + .reply(200); + + this.clientCredentials.get(options).then(() => { + expect(request.isDone()).to.be.true; + done(); + }); + }); + + it('should pass the parameters in the query-string', function (done) { + nock.cleanAll(); + + const params = { + include_fields: true, + fields: 'test', + }; + + const request = nock(API_URL).get(PATH).query(params).reply(200); + + const combinedOptions = Object.assign({}, params, options); + + this.clientCredentials.get(combinedOptions).then(() => { + expect(request.isDone()).to.be.true; + done(); + }); + }); + }); + + describe('#delete', () => { + const FAKE_CLIENT_ID = 'cli_1234'; + const FAKE_CREDENTIAL_ID = 'cre_1234'; + const PATH = `/clients/${FAKE_CLIENT_ID}/credentials/${FAKE_CREDENTIAL_ID}`; + const options = { client_id: FAKE_CLIENT_ID, credential_id: FAKE_CREDENTIAL_ID }; + + beforeEach(function () { + this.request = nock(API_URL).delete(PATH).reply(200); + }); + + it('should accept a callback', function (done) { + this.clientCredentials.delete(options, done); + }); + + it('should return a promise when no callback is given', function (done) { + this.clientCredentials + .delete(options) + .then(done.bind(null, null)) + .catch(done.bind(null, null)); + }); + + it(`should perform DELETE request to ${PATH}`, function (done) { + const { request } = this; + + this.clientCredentials.delete(options).then(() => { + expect(request.isDone()).to.be.true; + done(); + }); + }); + + it('should pass any errors to the promise catch handler', function (done) { + nock.cleanAll(); + + nock(API_URL).delete(PATH).reply(500); + + this.clientCredentials.delete(options).catch((err) => { + expect(err).to.exist; + + done(); + }); + }); + + it('should include the token in the authorization header', function (done) { + nock.cleanAll(); + + const request = nock(API_URL) + .delete(PATH) + .matchHeader('authorization', `Bearer ${this.token}`) + .reply(200); + + this.clientCredentials.delete(options).then(() => { + expect(request.isDone()).to.be.true; + + done(); + }); + }); + }); +});