Repository: auth0/node-auth0
Pull request #774 (2023-01-12 13:32:24)

[SDK-3868] Add support for managing client credentials
Adding new client credentials endpoints (replaces #734)


Patch:
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 <caption>
  *   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.


Test patch:
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();
+      });
+    });
+  });
+});