Skip to content

Commit 4e9d0ce

Browse files
authored
feat(proxy): proxy support with ConfidentialClient (#505)
1 parent 336e391 commit 4e9d0ce

8 files changed

Lines changed: 96 additions & 11 deletions

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,21 @@ async function exampleRequest() {
5353
exampleRequest();
5454
```
5555

56+
### Configure a Proxy
57+
58+
You can pass proxy settings to the ConfidentialClient if necessary. The proxy URL can be passed as an object with the proxyUrl property:
59+
60+
```ts
61+
const confidentialClient = new ConfidentialClient('/path/to/config.json', { proxyUrl: 'http://username:password@proxy.example.com:8080' });
62+
```
63+
5664
## Modules
5765

5866
Information about the various utility modules contained in this library can be found below.
5967

6068
### Authentication
6169

62-
The [authentication module](src) provides helper classes that facilitate [OAuth 2.0](https://developer.factset.com/learn/authentication-oauth2) authentication and authorization with FactSet's APIs. Currently the module has support for the [client credentials flow](https://github.com/factset/oauth2-guidelines#client-credentials-flow-1).
70+
The [authentication module](src) provides helper classes that facilitate [OAuth 2.0](https://developer.factset.com/learn/authentication-oauth2) authentication and authorization with FactSet's APIs. Currently, the module has support for the [client credentials flow](https://github.com/factset/oauth2-guidelines#client-credentials-flow-1).
6371

6472
Each helper class in the module has the following features:
6573

@@ -130,7 +138,7 @@ Please refer to the [contributing guide](CONTRIBUTING.md).
130138

131139
## Copyright
132140

133-
Copyright 2022 FactSet Research Systems Inc
141+
Copyright 2024 FactSet Research Systems Inc
134142

135143
Licensed under the Apache License, Version 2.0 (the "License");
136144
you may not use this file except in compliance with the License.

__tests__/confidentialClient.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { Client } from 'openid-client';
22
import { mocked } from 'ts-jest/utils';
33
import { ConfidentialClient } from '../src';
44
import { OpenIDClientFactory } from '../src/openIDClientFactory';
5+
import { HttpsProxyAgent } from 'https-proxy-agent';
6+
57
jest.mock('../src/openIDClientFactory');
8+
69
describe('test ConfidentialClient class', () => {
710
describe('test instanciating', () => {
811
test('should create an instance', () => {
@@ -75,5 +78,27 @@ describe('test ConfidentialClient class', () => {
7578

7679
await expect(confidentialClient.getAccessToken()).rejects.toThrow('Error attempting to get access token');
7780
});
81+
82+
test('should use the proxy agent if provided', async () => {
83+
const proxyUrl = 'http://proxy.example.com:8080';
84+
85+
mocked(OpenIDClientFactory.getClient).mockResolvedValue({
86+
grant: jest.fn().mockResolvedValue({
87+
access_token: 'test_token',
88+
expires_at: Math.floor(Date.now() / 1000) + 900,
89+
}),
90+
} as unknown as Client);
91+
92+
const confidentialClient = new ConfidentialClient('./__tests__/fixtures/validConfig.json', {
93+
proxyUrl: proxyUrl,
94+
});
95+
96+
await confidentialClient.getAccessToken();
97+
98+
expect(OpenIDClientFactory.getClient).toHaveBeenCalledWith(
99+
expect.anything(),
100+
new HttpsProxyAgent('http://proxy.example.com:8080'),
101+
);
102+
});
78103
});
79104
});

__tests__/openIDClientFactory.spec.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { OpenIDClientFactory } from '../src/openIDClientFactory';
22
import { mocked } from 'ts-jest/utils';
3-
import { Client, Issuer } from 'openid-client';
3+
import { Client, custom, Issuer } from 'openid-client';
4+
import { HttpsProxyAgent } from 'https-proxy-agent';
45

56
jest.mock('openid-client');
67

@@ -46,6 +47,23 @@ describe('Test OpenIDClientFactory class', () => {
4647
expect(Issuer.discover).toHaveBeenCalledWith('testWellKnownUri');
4748
});
4849

50+
test('should not throw an error and set proxy properly', async () => {
51+
const proxyUrl = 'http://proxy.example.com:8080';
52+
53+
mocked(Issuer.discover).mockResolvedValue({
54+
metadata: {
55+
issuer: 'test',
56+
token_endpoint: 'token_endpoint',
57+
},
58+
Client: jest.fn().mockReturnValue({
59+
grant: jest.fn().mockResolvedValue('testgrant'),
60+
}),
61+
} as unknown as Issuer<Client>);
62+
63+
await OpenIDClientFactory.getClient(config, new HttpsProxyAgent(proxyUrl));
64+
expect(custom.setHttpOptionsDefaults).toHaveBeenCalledWith({ agent: new HttpsProxyAgent(proxyUrl) });
65+
});
66+
4967
test('should throw an error while retrieving contents from well known uri', async () => {
5068
mocked(Issuer.discover).mockRejectedValue('test_error');
5169

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@
2828
],
2929
"dependencies": {
3030
"debug": "^4.3.4",
31-
"jose": "^4.15.4",
31+
"http-proxy-agent": "^7.0.2",
3232
"joi": "^17.12.3",
33+
"jose": "^4.15.4",
3334
"openid-client": "^5.6.5"
3435
},
3536
"devDependencies": {

src/confidentialClient.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { Token, AccessTokenError, ConfidentialClientConfiguration, OAuth2Client } from '.';
1+
import { AccessTokenError, ConfidentialClientConfiguration, OAuth2Client, Token } from '.';
22
import { OpenIDClientFactory } from './openIDClientFactory';
33
import { Configuration } from './configuration';
44
import { Client } from 'openid-client';
5-
import { PACKAGE_NAME, JWT_NOT_BEFORE_SECS, JWT_EXPIRE_AFTER_SECS } from './constants';
5+
import { JWT_EXPIRE_AFTER_SECS, JWT_NOT_BEFORE_SECS, PACKAGE_NAME } from './constants';
66
import { unixTimestamp } from './unixTimestamp';
77
import debugModule from 'debug';
8+
import { HttpsProxyAgent } from 'https-proxy-agent';
9+
810
const debug = debugModule(`${PACKAGE_NAME}:ConfidentialClient`);
911

1012
/**
@@ -19,11 +21,13 @@ export class ConfidentialClient implements OAuth2Client {
1921
private readonly _config: ConfidentialClientConfiguration;
2022
private _token: Token;
2123
private _openIDClient!: Client;
24+
private _options: { proxyUrl: string } | null;
2225

2326
/**
2427
* @param path Path to credentials configuration file.
28+
* @param _options HTTP proxy options.
2529
*/
26-
constructor(path: string);
30+
constructor(path: string, _options?: { proxyUrl: string });
2731

2832
/**
2933
* Example config
@@ -54,9 +58,10 @@ export class ConfidentialClient implements OAuth2Client {
5458
* @param config FacSet ConfidentialClient configuration object
5559
*/
5660
constructor(config: ConfidentialClientConfiguration);
57-
constructor(param: ConfidentialClientConfiguration | string) {
61+
constructor(param: ConfidentialClientConfiguration | string, _options?: { proxyUrl: string }) {
5862
this._config = Configuration.loadConfig(param);
5963
this._token = new Token('', 0);
64+
this._options = _options ?? null;
6065
}
6166

6267
/**
@@ -80,7 +85,11 @@ export class ConfidentialClient implements OAuth2Client {
8085
}
8186
debug('Token is expired or invalid');
8287

83-
if (this._openIDClient === undefined) {
88+
if (this._options?.proxyUrl) {
89+
const proxyAgent = new HttpsProxyAgent(`${this._options.proxyUrl}`);
90+
91+
this._openIDClient = await OpenIDClientFactory.getClient(this._config, proxyAgent);
92+
} else {
8493
this._openIDClient = await OpenIDClientFactory.getClient(this._config);
8594
}
8695

src/configuration.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ConfigurationError } from './errors';
44
import { FACTSET_WELL_KNOWN_URI, PACKAGE_NAME } from './constants';
55
import { readFileSync } from 'fs';
66
import debugModule from 'debug';
7+
78
const debug = debugModule(`${PACKAGE_NAME}:configuration`);
89

910
export type ConfidentialClientJwk = jose.JWK;

src/openIDClientFactory.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
import { ConfidentialClientConfiguration, WellKnownURIError } from '.';
2-
import { Client, Issuer, ClientAuthMethod } from 'openid-client';
2+
import { Client, ClientAuthMethod, custom, Issuer } from 'openid-client';
33
import debugModule from 'debug';
44
import { PACKAGE_NAME } from './constants';
5+
import { Agent } from 'node:http';
6+
57
const debug = debugModule(`${PACKAGE_NAME}:OpenIDClientFactory`);
68

79
export class OpenIDClientFactory {
8-
public static async getClient(config: ConfidentialClientConfiguration): Promise<Client> {
10+
public static async getClient(config: ConfidentialClientConfiguration, proxyAgent?: Agent): Promise<Client> {
911
const jwks = {
1012
keys: [config.jwk],
1113
};
1214

15+
if (proxyAgent) {
16+
custom.setHttpOptionsDefaults({
17+
agent: proxyAgent,
18+
});
19+
}
20+
1321
const clientAuthMethod: ClientAuthMethod = 'private_key_jwt';
1422

1523
const metadata = {

yarn.lock

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,6 +1092,13 @@ agent-base@6:
10921092
dependencies:
10931093
debug "4"
10941094

1095+
agent-base@^7.1.0:
1096+
version "7.1.1"
1097+
resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317"
1098+
integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==
1099+
dependencies:
1100+
debug "^4.3.4"
1101+
10951102
ajv@^6.12.4:
10961103
version "6.12.6"
10971104
resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
@@ -1940,6 +1947,14 @@ http-proxy-agent@^4.0.1:
19401947
agent-base "6"
19411948
debug "4"
19421949

1950+
http-proxy-agent@^7.0.2:
1951+
version "7.0.2"
1952+
resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e"
1953+
integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==
1954+
dependencies:
1955+
agent-base "^7.1.0"
1956+
debug "^4.3.4"
1957+
19431958
https-proxy-agent@^5.0.0:
19441959
version "5.0.0"
19451960
resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"

0 commit comments

Comments
 (0)