Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds HTTPS proxy support for Okta client #307

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -86,4 +87,4 @@
"tsd": {
"directory": "test/type"
}
}
}
1 change: 1 addition & 0 deletions src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class Client extends GeneratedApiClient {
}

this.http = new Http({
httpsProxy: clientConfig.httpsProxy,
cacheStore: clientConfig.cacheStore,
cacheMiddleware: clientConfig.cacheMiddleware,
defaultCacheMiddlewareResponseBufferSize: clientConfig.defaultCacheMiddlewareResponseBufferSize,
Expand Down
8 changes: 8 additions & 0 deletions src/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand All @@ -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 = () => {
Expand Down
2 changes: 2 additions & 0 deletions src/types/client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Http } from './http';
import { RequestExecutor } from './request-executor';
import { defaultCacheMiddleware } from './default-cache-middleware';
import { CacheStorage } from './memory-store';
import { HttpsProxyAgentOptions } from 'https-proxy-agent';


export declare class Client extends ParameterizedOperationsClient {
Expand All @@ -30,6 +31,7 @@ export declare class Client extends ParameterizedOperationsClient {
cacheStore?: CacheStorage,
cacheMiddleware?: typeof defaultCacheMiddleware | unknown
defaultCacheMiddlewareResponseBufferSize?: number,
httpsProxy?: string | HttpsProxyAgentOptions,
});

requestExecutor: RequestExecutor;
Expand Down
3 changes: 3 additions & 0 deletions src/types/http.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { RequestExecutor } from './request-executor';
import { CacheStorage } from './memory-store';
import { defaultCacheMiddleware } from './default-cache-middleware';
import { RequestOptions } from './request-options';
import { HttpsProxyAgent, HttpsProxyAgentOptions } from 'https-proxy-agent';

interface RequestContext {
isCollection?: boolean,
Expand All @@ -29,11 +30,13 @@ export declare class Http {
oauth: OAuth,
cacheStore?: CacheStorage,
cacheMiddleware?: typeof defaultCacheMiddleware | unknown,
httpsProxy?: string | HttpsProxyAgentOptions,
});
defaultHeaders: Record<string, unknown>;
requestExecutor: RequestExecutor;
cacheStore: CacheStorage;
cacheMiddleware: typeof defaultCacheMiddleware | unknown;
agent: typeof HttpsProxyAgent | unknown;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use any here, so allowSyntheticDefaultImports won't be needed for types test. The concern for allowSyntheticDefaultImports is it may break downstream types by introducing this loose flag.

oauth: OAuth;
prepareRequest(request: RequestOptions): Promise<RequestOptions>;
http(uri: string, request?: RequestOptions, context?: {
Expand Down
2 changes: 2 additions & 0 deletions src/types/request-options.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
*/

import { RequestInit } from 'node-fetch';
import { HttpsProxyAgent } from 'https-proxy-agent';

/** An extension of node-fetch's request options. */
export interface RequestOptions extends RequestInit {
startTime?: Date,
url?: string,
agent?: HttpsProxyAgent,
}
53 changes: 53 additions & 0 deletions test/jest/http.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({});
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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'
})
}),
});
});
});
});
});
});