From 7a629dfa590f4e9a757c12ba8c0b6df184c1b09a Mon Sep 17 00:00:00 2001
From: Frederik Prijck <frederik.prijck@auth0.com>
Date: Wed, 25 Jan 2023 11:20:23 +0100
Subject: [PATCH] Add support for proxy in AuthenticationClient (#779)

---
 src/auth/DatabaseAuthenticator.js         |   2 +
 src/auth/OAuthAuthenticator.js            |   2 +
 src/auth/PasswordlessAuthenticator.js     |   2 +
 src/auth/index.js                         |   2 +
 src/management/ManagementTokenProvider.js |   2 +
 test/auth/database-auth.tests.js          | 134 ++++++++++++++++
 test/auth/oauth.tests.js                  | 176 ++++++++++++++++++++++
 test/auth/passwordless.tests.js           | 131 ++++++++++++++++
 8 files changed, 451 insertions(+)

diff --git a/src/auth/DatabaseAuthenticator.js b/src/auth/DatabaseAuthenticator.js
index cebb038a8..72568de05 100644
--- a/src/auth/DatabaseAuthenticator.js
+++ b/src/auth/DatabaseAuthenticator.js
@@ -10,6 +10,7 @@ class DatabaseAuthenticator {
    * @param  {object}              options            Authenticator options.
    * @param  {string}              options.baseUrl    The auth0 account URL.
    * @param  {string}              [options.clientId] Default client ID.
+   * @param   {string}             [options.proxy]    Add the `superagent-proxy` dependency and specify a proxy url eg 'https://myproxy.com:1234'
    * @param  {OAuthAuthenticator}  oauth              OAuthAuthenticator instance.
    */
   constructor(options, oauth) {
@@ -29,6 +30,7 @@ class DatabaseAuthenticator {
     const clientOptions = {
       errorFormatter: { message: 'message', name: 'error' },
       headers: options.headers,
+      proxy: options.proxy,
     };
 
     this.oauth = oauth;
diff --git a/src/auth/OAuthAuthenticator.js b/src/auth/OAuthAuthenticator.js
index d7d7c31d1..709f561d3 100644
--- a/src/auth/OAuthAuthenticator.js
+++ b/src/auth/OAuthAuthenticator.js
@@ -35,6 +35,7 @@ class OAuthAuthenticator {
    * @param  {string}              [options.clientAssertionSigningKey] Private key used to sign the client assertion JWT.
    * @param  {string}              [options.clientAssertionSigningAlg] Default 'RS256'.
    * @param  {boolean}             [options.__bypassIdTokenValidation] Whether the id_token should be validated or not
+   * @param   {string}             [options.proxy]                     Add the `superagent-proxy` dependency and specify a proxy url eg 'https://myproxy.com:1234'
    */
   constructor(options) {
     if (!options) {
@@ -54,6 +55,7 @@ class OAuthAuthenticator {
       errorCustomizer: SanitizedError,
       errorFormatter: { message: 'message', name: 'error' },
       headers: options.headers,
+      proxy: options.proxy,
     };
 
     this.oauth = new RestClient(`${options.baseUrl}/oauth/:type`, clientOptions);
diff --git a/src/auth/PasswordlessAuthenticator.js b/src/auth/PasswordlessAuthenticator.js
index a6be10d6b..c933f4fe9 100644
--- a/src/auth/PasswordlessAuthenticator.js
+++ b/src/auth/PasswordlessAuthenticator.js
@@ -29,6 +29,7 @@ class PasswordlessAuthenticator {
    * @param  {string}              [options.clientSecret] Default client secret.
    * @param  {string}              [options.clientAssertionSigningKey] Private key used to sign the client assertion JWT.
    * @param  {string}              [options.clientAssertionSigningAlg] Default 'RS256'.
+   * @param   {string}             [options.proxy]    Add the `superagent-proxy` dependency and specify a proxy url eg 'https://myproxy.com:1234'
    * @param  {OAuthAuthenticator}  oauth              OAuthAuthenticator instance.
    */
   constructor(options, oauth) {
@@ -48,6 +49,7 @@ class PasswordlessAuthenticator {
     const clientOptions = {
       errorFormatter: { message: 'message', name: 'error' },
       headers: options.headers,
+      proxy: options.proxy,
     };
 
     this.oauth = oauth;
diff --git a/src/auth/index.js b/src/auth/index.js
index c2a4412ea..d2dc7e284 100644
--- a/src/auth/index.js
+++ b/src/auth/index.js
@@ -42,6 +42,7 @@ class AuthenticationClient {
    * @param   {string}  [options.supportedAlgorithms]     Algorithms that your application expects to receive
    * @param  {boolean}  [options.__bypassIdTokenValidation] Whether the id_token should be validated or not
    * @param   {object}  [options.headers]                 Additional headers that will be added to the outgoing requests.
+   * @param   {string}  [options.proxy]                   Add the `superagent-proxy` dependency and specify a proxy url eg 'https://myproxy.com:1234'
    */
   constructor(options) {
     if (!options || typeof options !== 'object') {
@@ -67,6 +68,7 @@ class AuthenticationClient {
       baseUrl: util.format(BASE_URL_FORMAT, options.domain),
       supportedAlgorithms: options.supportedAlgorithms,
       __bypassIdTokenValidation: options.__bypassIdTokenValidation,
+      proxy: options.proxy,
     };
 
     if (options.telemetry !== false) {
diff --git a/src/management/ManagementTokenProvider.js b/src/management/ManagementTokenProvider.js
index 4b12d048c..78a2fe757 100644
--- a/src/management/ManagementTokenProvider.js
+++ b/src/management/ManagementTokenProvider.js
@@ -19,6 +19,7 @@ class ManagementTokenProvider {
    * @param {boolean} [options.enableCache=true]      Enabled or Disable Cache
    * @param {number}  [options.cacheTTLInSeconds]     By default the `expires_in` value will be used to determine the cached time of the token, this can be overridden.
    * @param {object}  [options.headers]               Additional headers that will be added to the outgoing requests.
+   * @param {string}  [options.proxy]                 Add the `superagent-proxy` dependency and specify a proxy url eg 'https://myproxy.com:1234'
    */
   constructor(options) {
     if (!options || typeof options !== 'object') {
@@ -74,6 +75,7 @@ class ManagementTokenProvider {
       telemetry: this.options.telemetry,
       clientInfo: this.options.clientInfo,
       headers: this.options.headers,
+      proxy: this.options.proxy,
     };
     this.authenticationClient = new AuthenticationClient(authenticationClientOptions);
 
diff --git a/test/auth/database-auth.tests.js b/test/auth/database-auth.tests.js
index c1bfb82a5..4165b7d79 100644
--- a/test/auth/database-auth.tests.js
+++ b/test/auth/database-auth.tests.js
@@ -1,5 +1,8 @@
 const { expect } = require('chai');
 const nock = require('nock');
+const sinon = require('sinon');
+const { Client } = require('rest-facade');
+const proxyquire = require('proxyquire');
 
 const DOMAIN = 'tenant.auth0.com';
 const API_URL = `https://${DOMAIN}`;
@@ -181,6 +184,41 @@ describe('DatabaseAuthenticator', () => {
       await this.authenticator.signIn(userData);
       expect(request.isDone()).to.be.true;
     });
+
+    it('should make request with proxy', async () => {
+      nock.cleanAll();
+
+      const spy = sinon.spy();
+
+      class MockClient extends Client {
+        constructor(...args) {
+          spy(...args);
+          super(...args);
+        }
+      }
+      const MockAuthenticator = proxyquire(`../../src/auth/DatabaseAuthenticator`, {
+        'rest-facade': {
+          Client: MockClient,
+        },
+      });
+
+      const request = nock(API_URL).post(path).reply(200);
+
+      const authenticator = new MockAuthenticator(
+        {
+          ...validOptions,
+          proxy: 'http://proxy',
+        },
+        new OAuth(validOptions)
+      );
+
+      return authenticator.signIn(userData).then(() => {
+        sinon.assert.calledWithMatch(spy, API_URL, {
+          proxy: 'http://proxy',
+        });
+        expect(request.isDone()).to.be.true;
+      });
+    });
   });
 
   describe('#signUp', () => {
@@ -273,6 +311,38 @@ describe('DatabaseAuthenticator', () => {
       await this.authenticator.signUp(userData);
       expect(request.isDone()).to.be.true;
     });
+
+    it('should make request with proxy', async () => {
+      nock.cleanAll();
+
+      const spy = sinon.spy();
+
+      class MockClient extends Client {
+        constructor(...args) {
+          spy(...args);
+          super(...args);
+        }
+      }
+      const MockAuthenticator = proxyquire(`../../src/auth/DatabaseAuthenticator`, {
+        'rest-facade': {
+          Client: MockClient,
+        },
+      });
+
+      const request = nock(API_URL).post(path).reply(200);
+
+      const authenticator = new MockAuthenticator(
+        { ...validOptions, proxy: 'http://proxy' },
+        new OAuth(validOptions)
+      );
+
+      return authenticator.signUp(userData).then(() => {
+        sinon.assert.calledWithMatch(spy, API_URL, {
+          proxy: 'http://proxy',
+        });
+        expect(request.isDone()).to.be.true;
+      });
+    });
   });
 
   describe('#changePassword', () => {
@@ -368,6 +438,38 @@ describe('DatabaseAuthenticator', () => {
       await this.authenticator.changePassword(userData);
       expect(request.isDone()).to.be.true;
     });
+
+    it('should make request with proxy', async () => {
+      nock.cleanAll();
+
+      const spy = sinon.spy();
+
+      class MockClient extends Client {
+        constructor(...args) {
+          spy(...args);
+          super(...args);
+        }
+      }
+      const MockAuthenticator = proxyquire(`../../src/auth/DatabaseAuthenticator`, {
+        'rest-facade': {
+          Client: MockClient,
+        },
+      });
+
+      const request = nock(API_URL).post(path).reply(200);
+
+      const authenticator = new MockAuthenticator(
+        { ...validOptions, proxy: 'http://proxy' },
+        new OAuth(validOptions)
+      );
+
+      return authenticator.changePassword(userData).then(() => {
+        sinon.assert.calledWithMatch(spy, API_URL, {
+          proxy: 'http://proxy',
+        });
+        expect(request.isDone()).to.be.true;
+      });
+    });
   });
 
   describe('#requestChangePasswordEmail', () => {
@@ -450,5 +552,37 @@ describe('DatabaseAuthenticator', () => {
       await this.authenticator.requestChangePasswordEmail(userData);
       expect(request.isDone()).to.be.true;
     });
+
+    it('should make request with proxy', async () => {
+      nock.cleanAll();
+
+      const spy = sinon.spy();
+
+      class MockClient extends Client {
+        constructor(...args) {
+          spy(...args);
+          super(...args);
+        }
+      }
+      const MockAuthenticator = proxyquire(`../../src/auth/DatabaseAuthenticator`, {
+        'rest-facade': {
+          Client: MockClient,
+        },
+      });
+
+      const request = nock(API_URL).post(path).reply(200);
+
+      const authenticator = new MockAuthenticator(
+        { ...validOptions, proxy: 'http://proxy' },
+        new OAuth(validOptions)
+      );
+
+      return authenticator.requestChangePasswordEmail(userData).then(() => {
+        sinon.assert.calledWithMatch(spy, API_URL, {
+          proxy: 'http://proxy',
+        });
+        expect(request.isDone()).to.be.true;
+      });
+    });
   });
 });
diff --git a/test/auth/oauth.tests.js b/test/auth/oauth.tests.js
index 22608673e..7387e722d 100644
--- a/test/auth/oauth.tests.js
+++ b/test/auth/oauth.tests.js
@@ -1,6 +1,8 @@
 const { expect } = require('chai');
 const nock = require('nock');
 const sinon = require('sinon');
+const { Client } = require('rest-facade');
+const proxyquire = require('proxyquire');
 
 const DOMAIN = 'tenant.auth0.com';
 const API_URL = `https://${DOMAIN}`;
@@ -246,6 +248,35 @@ describe('OAuthAuthenticator', () => {
         })
         .catch(done);
     });
+
+    it('should make request with proxy', async () => {
+      nock.cleanAll();
+
+      const spy = sinon.spy();
+
+      class MockClient extends Client {
+        constructor(...args) {
+          spy(...args);
+          super(...args);
+        }
+      }
+      const MockAuthenticator = proxyquire(`../../src/auth/OAuthAuthenticator`, {
+        'rest-facade': {
+          Client: MockClient,
+        },
+      });
+
+      const request = nock(API_URL).post(path).reply(200);
+
+      const authenticator = new MockAuthenticator({ ...validOptions, proxy: 'http://proxy' });
+
+      return authenticator.signIn(userData).then(() => {
+        sinon.assert.calledWithMatch(spy, API_URL, {
+          proxy: 'http://proxy',
+        });
+        expect(request.isDone()).to.be.true;
+      });
+    });
   });
 
   describe('#passwordGrant', () => {
@@ -431,6 +462,35 @@ describe('OAuthAuthenticator', () => {
         })
         .catch(done);
     });
+
+    it('should make request with proxy', async () => {
+      nock.cleanAll();
+
+      const spy = sinon.spy();
+
+      class MockClient extends Client {
+        constructor(...args) {
+          spy(...args);
+          super(...args);
+        }
+      }
+      const MockAuthenticator = proxyquire(`../../src/auth/OAuthAuthenticator`, {
+        'rest-facade': {
+          Client: MockClient,
+        },
+      });
+
+      const request = nock(API_URL).post(path).reply(200);
+
+      const authenticator = new MockAuthenticator({ ...validOptions, proxy: 'http://proxy' });
+
+      return authenticator.passwordGrant(userData).then(() => {
+        sinon.assert.calledWithMatch(spy, API_URL, {
+          proxy: 'http://proxy',
+        });
+        expect(request.isDone()).to.be.true;
+      });
+    });
   });
 
   describe('#refreshToken', () => {
@@ -529,6 +589,35 @@ describe('OAuthAuthenticator', () => {
         })
         .catch(done);
     });
+
+    it('should make request with proxy', async () => {
+      nock.cleanAll();
+
+      const spy = sinon.spy();
+
+      class MockClient extends Client {
+        constructor(...args) {
+          spy(...args);
+          super(...args);
+        }
+      }
+      const MockAuthenticator = proxyquire(`../../src/auth/OAuthAuthenticator`, {
+        'rest-facade': {
+          Client: MockClient,
+        },
+      });
+
+      const request = nock(API_URL).post(path).reply(200);
+
+      const authenticator = new MockAuthenticator({ ...validOptions, proxy: 'http://proxy' });
+
+      return authenticator.refreshToken(userData).then(() => {
+        sinon.assert.calledWithMatch(spy, API_URL, {
+          proxy: 'http://proxy',
+        });
+        expect(request.isDone()).to.be.true;
+      });
+    });
   });
 
   describe('#socialSignIn', () => {
@@ -653,6 +742,35 @@ describe('OAuthAuthenticator', () => {
         done();
       });
     });
+
+    it('should make request with proxy', async () => {
+      nock.cleanAll();
+
+      const spy = sinon.spy();
+
+      class MockClient extends Client {
+        constructor(...args) {
+          spy(...args);
+          super(...args);
+        }
+      }
+      const MockAuthenticator = proxyquire(`../../src/auth/OAuthAuthenticator`, {
+        'rest-facade': {
+          Client: MockClient,
+        },
+      });
+
+      const request = nock(API_URL).post(path).reply(200);
+
+      const authenticator = new MockAuthenticator({ ...validOptions, proxy: 'http://proxy' });
+
+      return authenticator.socialSignIn(userData).then(() => {
+        sinon.assert.calledWithMatch(spy, API_URL, {
+          proxy: 'http://proxy',
+        });
+        expect(request.isDone()).to.be.true;
+      });
+    });
   });
 
   describe('#clientCredentials', () => {
@@ -785,6 +903,35 @@ describe('OAuthAuthenticator', () => {
         expect(originalRequestData.client_secret).to.not.equal(CLIENT_SECRET);
       });
     });
+
+    it('should make request with proxy', async () => {
+      nock.cleanAll();
+
+      const spy = sinon.spy();
+
+      class MockClient extends Client {
+        constructor(...args) {
+          spy(...args);
+          super(...args);
+        }
+      }
+      const MockAuthenticator = proxyquire(`../../src/auth/OAuthAuthenticator`, {
+        'rest-facade': {
+          Client: MockClient,
+        },
+      });
+
+      const request = nock(API_URL).post(path).reply(200);
+
+      const authenticator = new MockAuthenticator({ ...validOptions, proxy: 'http://proxy' });
+
+      return authenticator.clientCredentialsGrant(options).then(() => {
+        sinon.assert.calledWithMatch(spy, API_URL, {
+          proxy: 'http://proxy',
+        });
+        expect(request.isDone()).to.be.true;
+      });
+    });
   });
 
   describe('#authorizationCodeGrant', () => {
@@ -929,5 +1076,34 @@ describe('OAuthAuthenticator', () => {
         })
         .catch(done);
     });
+
+    it('should make request with proxy', async () => {
+      nock.cleanAll();
+
+      const spy = sinon.spy();
+
+      class MockClient extends Client {
+        constructor(...args) {
+          spy(...args);
+          super(...args);
+        }
+      }
+      const MockAuthenticator = proxyquire(`../../src/auth/OAuthAuthenticator`, {
+        'rest-facade': {
+          Client: MockClient,
+        },
+      });
+
+      const request = nock(API_URL).post(path).reply(200);
+
+      const authenticator = new MockAuthenticator({ ...validOptions, proxy: 'http://proxy' });
+
+      return authenticator.authorizationCodeGrant(data).then(() => {
+        sinon.assert.calledWithMatch(spy, API_URL, {
+          proxy: 'http://proxy',
+        });
+        expect(request.isDone()).to.be.true;
+      });
+    });
   });
 });
diff --git a/test/auth/passwordless.tests.js b/test/auth/passwordless.tests.js
index 6175ee2c8..68e44c825 100644
--- a/test/auth/passwordless.tests.js
+++ b/test/auth/passwordless.tests.js
@@ -1,5 +1,8 @@
 const { expect } = require('chai');
 const nock = require('nock');
+const sinon = require('sinon');
+const { Client } = require('rest-facade');
+const proxyquire = require('proxyquire');
 
 const DOMAIN = 'tenant.auth0.com';
 const API_URL = `https://${DOMAIN}`;
@@ -254,6 +257,38 @@ describe('PasswordlessAuthenticator', () => {
           })
           .catch(done);
       });
+
+      it('should make request with proxy', async () => {
+        nock.cleanAll();
+
+        const spy = sinon.spy();
+
+        class MockClient extends Client {
+          constructor(...args) {
+            spy(...args);
+            super(...args);
+          }
+        }
+        const MockAuthenticator = proxyquire(`../../src/auth/PasswordlessAuthenticator`, {
+          'rest-facade': {
+            Client: MockClient,
+          },
+        });
+
+        const request = nock(API_URL).post(path).reply(200);
+
+        const authenticator = new MockAuthenticator(
+          { ...validOptions, proxy: 'http://proxy' },
+          new OAuth(validOptions)
+        );
+
+        return authenticator.signIn(userData, options).then(() => {
+          sinon.assert.calledWithMatch(spy, API_URL, {
+            proxy: 'http://proxy',
+          });
+          expect(request.isDone()).to.be.true;
+        });
+      });
     });
 
     describe('/oauth/token', () => {
@@ -488,6 +523,38 @@ describe('PasswordlessAuthenticator', () => {
           })
           .catch(done);
       });
+
+      it('should make request with proxy', async () => {
+        nock.cleanAll();
+
+        const spy = sinon.spy();
+
+        class MockClient extends Client {
+          constructor(...args) {
+            spy(...args);
+            super(...args);
+          }
+        }
+        const MockAuthenticator = proxyquire(`../../src/auth/PasswordlessAuthenticator`, {
+          'rest-facade': {
+            Client: MockClient,
+          },
+        });
+
+        const request = nock(API_URL).post(path).reply(200);
+
+        const authenticator = new MockAuthenticator(
+          { ...validOptions, proxy: 'http://proxy' },
+          new OAuth(validOptions)
+        );
+
+        return authenticator.signIn(userData, options).then(() => {
+          sinon.assert.calledWithMatch(spy, API_URL, {
+            proxy: 'http://proxy',
+          });
+          expect(request.isDone()).to.be.true;
+        });
+      });
     });
   });
 
@@ -668,6 +735,38 @@ describe('PasswordlessAuthenticator', () => {
         })
         .catch(done);
     });
+
+    it('should make request with proxy', async () => {
+      nock.cleanAll();
+
+      const spy = sinon.spy();
+
+      class MockClient extends Client {
+        constructor(...args) {
+          spy(...args);
+          super(...args);
+        }
+      }
+      const MockAuthenticator = proxyquire(`../../src/auth/PasswordlessAuthenticator`, {
+        'rest-facade': {
+          Client: MockClient,
+        },
+      });
+
+      const request = nock(API_URL).post(path).reply(200);
+
+      const authenticator = new MockAuthenticator(
+        { ...validOptions, proxy: 'http://proxy' },
+        new OAuth(validOptions)
+      );
+
+      return authenticator.sendEmail(userData, options).then(() => {
+        sinon.assert.calledWithMatch(spy, API_URL, {
+          proxy: 'http://proxy',
+        });
+        expect(request.isDone()).to.be.true;
+      });
+    });
   });
 
   describe('#sendSMS', () => {
@@ -816,5 +915,37 @@ describe('PasswordlessAuthenticator', () => {
         })
         .catch(done);
     });
+
+    it('should make request with proxy', async () => {
+      nock.cleanAll();
+
+      const spy = sinon.spy();
+
+      class MockClient extends Client {
+        constructor(...args) {
+          spy(...args);
+          super(...args);
+        }
+      }
+      const MockAuthenticator = proxyquire(`../../src/auth/PasswordlessAuthenticator`, {
+        'rest-facade': {
+          Client: MockClient,
+        },
+      });
+
+      const request = nock(API_URL).post(path).reply(200);
+
+      const authenticator = new MockAuthenticator(
+        { ...validOptions, proxy: 'http://proxy' },
+        new OAuth(validOptions)
+      );
+
+      return authenticator.sendSMS(userData, options).then(() => {
+        sinon.assert.calledWithMatch(spy, API_URL, {
+          proxy: 'http://proxy',
+        });
+        expect(request.isDone()).to.be.true;
+      });
+    });
   });
 });