diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e7d18f8f..344bccb03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Okta Node SDK Changelog +## 6.4.0 + +### Features + +- [#307](https://github.com/okta/okta-sdk-nodejs/pull/307) Supports HTTPS proxy for Okta http client + ## 6.3.1 ### Bug Fixes diff --git a/README.md b/README.md index a45413a82..4bcf96ec8 100644 --- a/README.md +++ b/README.md @@ -806,6 +806,23 @@ const client = new okta.Client({ }) ``` +## Proxy + +If you need to use a proxy, you can configure it with `httpsProxy` property. +```javascript +const okta = require('@okta/okta-sdk-nodejs'); + +const client = new okta.Client({ + orgUrl: 'https://dev-1234.oktapreview.com/', + token: 'xYzabc', // Obtained from Developer Dashboard + httpsProxy: 'http://proxy.example.net:8080/' +}); +``` + +When the proxy configuration is not overridden as shown above, Okta client relies on the proxy configuration defined by standard environment variable `https_proxy` or its uppercase variant `HTTPS_PROXY`. + +To use HTTP Basic Auth with your proxy, use the `http://user:password@host/` syntax. + ## TypeScript usage ### 4.5.x diff --git a/package.json b/package.json index 719de15f6..f8eee67c9 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "dependencies": { "deep-copy": "^1.4.2", "form-data": "^4.0.0", + "https-proxy-agent": "^5.0.0", "isomorphic-fetch": "^3.0.0", "js-yaml": "^4.1.0", "lodash": "^4.17.20", @@ -42,6 +43,7 @@ "safe-flat": "^2.0.2" }, "devDependencies": { + "@faker-js/faker": "^5.5.3", "@okta/openapi": "^2.10.0", "@types/chai": "^4.2.22", "@types/mocha": "^9.0.0", @@ -53,7 +55,6 @@ "eslint": "^8.2.0", "eslint-plugin-jest": "^25.2.4", "fake-fs": "^0.5.0", - "@faker-js/faker": "^5.5.3", "globby": "^11.0.4", "ink-docstrap": "^1.3.2", "jest": "^27.3.1", @@ -86,4 +87,4 @@ "tsd": { "directory": "test/type" } -} \ No newline at end of file +} diff --git a/src/client.js b/src/client.js index 4c172e24d..bf6eaa16a 100644 --- a/src/client.js +++ b/src/client.js @@ -76,6 +76,7 @@ class Client extends GeneratedApiClient { } this.http = new Http({ + httpsProxy: clientConfig.httpsProxy, cacheStore: clientConfig.cacheStore, cacheMiddleware: clientConfig.cacheMiddleware, defaultCacheMiddlewareResponseBufferSize: clientConfig.defaultCacheMiddlewareResponseBufferSize, diff --git a/src/http.js b/src/http.js index 18e3c268b..2ab5531cf 100644 --- a/src/http.js +++ b/src/http.js @@ -16,6 +16,7 @@ const OktaApiError = require('./api-error'); const HttpError = require('./http-error'); const MemoryStore = require('./memory-store'); const defaultCacheMiddleware = require('./default-cache-middleware'); +const HttpsProxyAgent = require('https-proxy-agent'); /** * It's like fetch :) plus some extra convenience methods. @@ -55,6 +56,10 @@ class Http { this.defaultCacheMiddlewareResponseBufferSize = httpConfig.defaultCacheMiddlewareResponseBufferSize; } this.oauth = httpConfig.oauth; + const proxy = httpConfig.httpsProxy || process.env.https_proxy || process.env.HTTPS_PROXY; + if (proxy) { + this.agent = new HttpsProxyAgent(proxy); + } } prepareRequest(request) { @@ -74,6 +79,9 @@ class Http { request.url = uri; request.headers = Object.assign({}, this.defaultHeaders, request.headers); request.method = request.method || 'get'; + if (this.agent) { + request.agent = this.agent; + } let retriedOnAuthError = false; const execute = () => { diff --git a/src/types/client.d.ts b/src/types/client.d.ts index d1c71ba3a..8b5d1ee02 100644 --- a/src/types/client.d.ts +++ b/src/types/client.d.ts @@ -30,6 +30,7 @@ export declare class Client extends ParameterizedOperationsClient { cacheStore?: CacheStorage, cacheMiddleware?: typeof defaultCacheMiddleware | unknown defaultCacheMiddlewareResponseBufferSize?: number, + httpsProxy?: string | unknown, // https://github.com/TooTallNate/node-agent-base/issues/56 }); requestExecutor: RequestExecutor; diff --git a/src/types/http.d.ts b/src/types/http.d.ts index 4ba20298e..5c4aff326 100644 --- a/src/types/http.d.ts +++ b/src/types/http.d.ts @@ -29,11 +29,13 @@ export declare class Http { oauth: OAuth, cacheStore?: CacheStorage, cacheMiddleware?: typeof defaultCacheMiddleware | unknown, + httpsProxy?: string | unknown, // https://github.com/TooTallNate/node-agent-base/issues/56 }); defaultHeaders: Record; requestExecutor: RequestExecutor; cacheStore: CacheStorage; cacheMiddleware: typeof defaultCacheMiddleware | unknown; + agent: any; // https://github.com/TooTallNate/node-agent-base/issues/56 oauth: OAuth; prepareRequest(request: RequestOptions): Promise; http(uri: string, request?: RequestOptions, context?: { diff --git a/src/types/request-options.d.ts b/src/types/request-options.d.ts index 11709c060..5e502eb40 100644 --- a/src/types/request-options.d.ts +++ b/src/types/request-options.d.ts @@ -16,4 +16,5 @@ import { RequestInit } from 'node-fetch'; export interface RequestOptions extends RequestInit { startTime?: Date, url?: string, + agent?: any, // https://github.com/TooTallNate/node-agent-base/issues/56 } diff --git a/test/jest/http.test.js b/test/jest/http.test.js index 97e92a7ca..07c5cd385 100644 --- a/test/jest/http.test.js +++ b/test/jest/http.test.js @@ -4,9 +4,19 @@ const MemoryStore = require('../../src/memory-store'); const defaultCacheMiddleware = require('../../src/default-cache-middleware'); const OktaApiError = require('../../src/api-error'); const HttpError = require('../../src/http-error'); +const HttpsProxyAgent = require('https-proxy-agent'); describe('Http class', () => { describe('constructor', () => { + const OLD_ENV = process.env; + beforeEach(() => { + jest.resetModules(); + process.env = { ...OLD_ENV }; + }); + afterAll(() => { + process.env = OLD_ENV; + }); + it('creates empty defaultHeaders object', () => { const http = new Http({}); expect(http.defaultHeaders).toEqual({}); @@ -39,6 +49,30 @@ describe('Http class', () => { const http = new Http({ oauth }); expect(http.oauth).toBe(oauth); }); + it('accepts a "httpsProxy"', () => { + const http = new Http({ httpsProxy: 'http://proxy.example.net:8080/' }); + expect(http.agent).toBeInstanceOf(HttpsProxyAgent); + expect(http.agent.proxy.host).toBe('proxy.example.net'); + }); + it('uses a "https_proxy" env var', () => { + process.env.https_proxy = 'http://proxy.example.net:8080/'; + const http = new Http({ }); + expect(http.agent).toBeInstanceOf(HttpsProxyAgent); + expect(http.agent.proxy.host).toBe('proxy.example.net'); + }); + it('uses a "HTTPS_PROXY" env var', () => { + process.env.HTTPS_PROXY = 'http://proxy.example.net:8080/'; + const http = new Http({ }); + expect(http.agent).toBeInstanceOf(HttpsProxyAgent); + expect(http.agent.proxy.host).toBe('proxy.example.net'); + }); + it('uses a "httpsProxy" over "https_proxy"/"HTTPS_PROXY" env vars', () => { + process.env.https_proxy = 'http://proxy1.example.net:8080/'; + process.env.HTTPS_PROXY = 'http://proxy2.example.net:8080/'; + const http = new Http({ httpsProxy: 'http://proxy.example.net:8080/' }); + expect(http.agent).toBeInstanceOf(HttpsProxyAgent); + expect(http.agent.proxy.host).toBe('proxy.example.net'); + }); }); describe('errorFilter', () => { it('should resolve promise for status in 200 - 300 range', () => { @@ -476,5 +510,24 @@ describe('Http class', () => { }); }); }); + + describe('proxy', () => { + it('should use proxy agent in fetch method', () => { + const http = new Http({ requestExecutor, httpsProxy: 'http://proxy.example.net:8080/' }); + return http.http('http://fakey.local') + .then(() => { + expect(requestExecutor.fetch).toHaveBeenCalledWith({ + headers: {}, + method: 'get', + url: 'http://fakey.local', + agent: expect.objectContaining({ + proxy: expect.objectContaining({ + host: 'proxy.example.net' + }) + }), + }); + }); + }); + }); }); });