Skip to content

Commit

Permalink
refactor: only use the native http(s) client
Browse files Browse the repository at this point in the history
BREAKING CHANGE: HTTP(S) request customization now only recognizes the
following options 'agent', 'ca', 'cert', 'crl', 'headers', 'key',
'lookup', 'passphrase', 'pfx', and 'timeout'. These are standard node
http/https module request options, got-library specific options such
as 'followRedirect', 'retry', or 'throwHttpErrors' are no longer
recognized.

BREAKING CHANGE: The arguments inside individual HTTP request
customization changed, first argument is now an instance of
[URL](https://nodejs.org/api/url.html#class-url), the http request options object is passed in as a second
argument.

BREAKING CHANGE: The `response` property attached to some RPError or
OPError instances is now an instance of [http.IncomingMessage](https://nodejs.org/api/http.html#class-httpincomingmessage). Its
body is available on its `body` property as either JSON if it could be
parsed, or a Buffer if it failed to pass as JSON.
  • Loading branch information
panva committed Oct 27, 2021
1 parent 8b3044e commit 83376ac
Show file tree
Hide file tree
Showing 18 changed files with 205 additions and 146 deletions.
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,10 +277,6 @@ See [Client Authentication Methods (docs)][documentation-methods].

See [Customizing (docs)](https://github.com/panva/node-openid-client/blob/master/docs/README.md#customizing).

#### How can I debug the requests and responses?

See [Customizing (docs)](https://github.com/panva/node-openid-client/blob/master/docs/README.md#customizing).


[openid-connect]: https://openid.net/connect/
[feature-core]: https://openid.net/specs/openid-connect-core-1_0.html
Expand Down
62 changes: 19 additions & 43 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -659,65 +659,41 @@ This function will then be called before executing each and every request on the
const { custom } = require('openid-client');

// you can also set this on Issuer constructor, Issuer instance, or Client constructor
client[custom.http_options] = function (options) {
client[custom.http_options] = function (url, options) {
// console.log(url);
// console.log(options);
options.timeout = 5000;
return options;
return { timeout: 5000 };
}
```

This is meant to change request options on per-request basis should there be a specific IdP quirk
you need to work around, e.g. adding custom headers or body payload parameters.
The following options can be provided `agent`, `ca`, `cert`, `crl`, `headers`, `key`, `lookup`, `passphrase`,
`pfx`, `timeout`. These are all relayed to https://nodejs.org/api/https.html#httpsrequesturl-options-callback

<details>
<summary><em><strong>Example</strong></em> (Click to expand) providing mutual-TLS client certificate and key</summary>

```js
const { custom } = require('openid-client');
client[custom.http_options] = function (options) {
// see https://github.com/sindresorhus/got/tree/v11.8.0#advanced-https-api
options.https = options.https || {};
options.https.certificate = certificate; // <string> | <string[]> | <Buffer> | <Buffer[]>
options.https.key = key; // <string> | <string[]> | <Buffer> | <Buffer[]> | <Object[]>
// custom CA
// options.https.ca = ca; // <string> | <string[]> | <Buffer> | <Buffer[]>

// use with .p12/.pfx files
// options.https.pfx = pfx; // <string> | <string[]> | <Buffer> | <Buffer[]> | <Object[]>
// options.https.passphrase = passphrase; // <string>

// use HTTP(S)_PROXY
// https://github.com/sindresorhus/got/tree/v11.8.0#agent
// options.agent = agent;

return options;
}
```
</details>

<details>
<summary><em><strong>Example</strong></em> (Click to expand) Other options</summary>

```js
const { custom } = require('openid-client');
client[custom.http_options] = function (options) {
// https://github.com/sindresorhus/got/tree/v11.8.0#headers
// options.headers = Object.assign(options.headers, { 'custom': 'foo' });
client[custom.http_options] = function (url, options) {
// https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions
const result = {};

// https://github.com/sindresorhus/got/tree/v11.8.0#timeout
// options.timeout = timeout;
result.cert = cert; // <string> | <string[]> | <Buffer> | <Buffer[]>
result.key = key; // <string> | <string[]> | <Buffer> | <Buffer[]> | <Object[]>

// https://github.com/sindresorhus/got/tree/v11.8.0#retry
// options.retry = retry;
// custom CA
// result.ca = ca; // <string> | <string[]> | <Buffer> | <Buffer[]>

// https://github.com/sindresorhus/got/tree/v11.8.0#followredirect
// options.followRedirect = false;
// use with .p12/.pfx files
// result.pfx = pfx; // <string> | <string[]> | <Buffer> | <Buffer[]> | <Object[]>
// result.passphrase = passphrase; // <string>

// use HTTP(S)_PROXY
// https://github.com/sindresorhus/got/tree/v11.8.0#agent
// options.agent = agent;
// https://nodejs.org/api/http.html#httprequesturl-options-callback
// e.g. using https://www.npmjs.com/package/proxy-agent
// result.agent = agent;

return options;
return result;
}
```
</details>
Expand Down
10 changes: 7 additions & 3 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ const { strict: assert } = require('assert');
const querystring = require('querystring');
const url = require('url');

const { ParseError } = require('got');
const jose = require('jose');
const tokenHash = require('oidc-token-hash');

Expand Down Expand Up @@ -95,6 +94,10 @@ function authorizationParams(params) {

async function claimJWT(label, jwt) {
try {
if (Buffer.isBuffer(jwt)) {
// eslint-disable-next-line no-param-reassign
jwt = jwt.toString('ascii');
}
const { header, payload } = jose.JWT.decode(jwt, { complete: true });
const { iss } = payload;

Expand Down Expand Up @@ -1160,8 +1163,9 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
} else {
try {
parsed = JSON.parse(response.body);
} catch (error) {
throw new ParseError(error, response);
} catch (err) {
Object.defineProperty(err, 'response', response);
throw err;
}
}

Expand Down
12 changes: 0 additions & 12 deletions lib/helpers/is_absolute_url.js

This file was deleted.

2 changes: 1 addition & 1 deletion lib/helpers/process_response.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const isStandardBodyError = (response) => {
jsonbody = response.body;
}
result = typeof jsonbody.error === 'string' && jsonbody.error.length;
if (result) response.body = jsonbody;
if (result) Object.defineProperty(response, 'body', { value: jsonbody, configurable: true });
} catch (err) {}

return result;
Expand Down
167 changes: 145 additions & 22 deletions lib/helpers/request.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,57 @@
const Got = require('got');
const assert = require('assert');
const querystring = require('querystring');
const http = require('http');
const https = require('https');
const { once } = require('events');

const CacheableLookup = require('cacheable-lookup');
const QuickLRU = require('quick-lru');

const pkg = require('../../package.json');
const { RPError } = require('../errors');

const pick = require('./pick');
const { deep: defaultsDeep } = require('./defaults');
const isAbsoluteUrl = require('./is_absolute_url');
const { HTTP_OPTIONS } = require('./consts');

let DEFAULT_HTTP_OPTIONS;
let got;

const setDefaults = (options) => {
DEFAULT_HTTP_OPTIONS = defaultsDeep({}, options, DEFAULT_HTTP_OPTIONS);
got = Got.extend(DEFAULT_HTTP_OPTIONS);
const cacheable = new CacheableLookup({
cache: new QuickLRU({ maxSize: 1000 }),
});
const allowed = [
'agent',
'ca',
'cert',
'crl',
'headers',
'key',
'lookup',
'passphrase',
'pfx',
'timeout',
];

const setDefaults = (props, options) => {
// eslint-disable-next-line max-len
DEFAULT_HTTP_OPTIONS = defaultsDeep({}, props.length ? pick(options, ...props) : options, DEFAULT_HTTP_OPTIONS);
};

setDefaults({
followRedirect: false,
setDefaults([], {
headers: { 'User-Agent': `${pkg.name}/${pkg.version} (${pkg.homepage})` },
retry: 0,
timeout: 3500,
throwHttpErrors: false,
lookup: cacheable.lookup,
});

module.exports = async function request(options, { accessToken, mTLS = false, DPoP } = {}) {
const { url } = options;
isAbsoluteUrl(url);
let url;
try {
url = new URL(options.url);
delete options.url;
assert(/^(https?:)$/.test(url.protocol));
} catch (err) {
throw new TypeError('only valid absolute URLs can be requested');
}
const optsFn = this[HTTP_OPTIONS];
let opts = options;

Expand All @@ -36,21 +63,117 @@ module.exports = async function request(options, { accessToken, mTLS = false, DP
}, DPoP, accessToken);
}

let userOptions;
if (optsFn) {
opts = optsFn.call(this, defaultsDeep({}, opts, DEFAULT_HTTP_OPTIONS));
userOptions = pick(
optsFn.call(this, url, defaultsDeep({}, opts, DEFAULT_HTTP_OPTIONS)),
...allowed,
);
}
opts = defaultsDeep({}, userOptions, opts, DEFAULT_HTTP_OPTIONS);

if (
mTLS
&& (
(!opts.key || !opts.cert)
&& (!opts.https || !((opts.https.key && opts.https.certificate) || opts.https.pfx))
)
) {
if (mTLS && (!opts.pfx && !(opts.key && opts.cert))) {
throw new TypeError('mutual-TLS certificate and key not set');
}

return got(opts);
if (opts.searchParams) {
// eslint-disable-next-line no-restricted-syntax
for (const [key, value] of Object.entries(opts.searchParams)) {
url.searchParams.delete(key);
url.searchParams.set(key, value);
}
}

let responseType;
let form;
let json;
let body;
({
// eslint-disable-next-line prefer-const
form, responseType, json, body, ...opts
} = opts);

// eslint-disable-next-line no-restricted-syntax
for (const [key, value] of Object.entries(opts.headers || {})) {
if (value === undefined) {
delete opts.headers[key];
}
}

let response;
const req = (url.protocol === 'https:' ? https.request : http.request)(url, opts);
return (async () => {
// if (GET (and other && form, json, body)) throw;
if (json) {
req.removeHeader('content-type');
req.setHeader('content-type', 'application/json');
req.write(JSON.stringify(json));
} else if (form) {
req.removeHeader('content-type');
req.setHeader('content-type', 'application/x-www-form-urlencoded');
req.write(querystring.stringify(form));
} else if (body) {
req.write(body);
}

req.end();

[response] = await Promise.race([once(req, 'response'), once(req, 'timeout')]);

// timeout reached
if (!response) {
req.destroy();
throw new RPError(`outgoing request timed out after ${opts.timeout}ms`);
}

const parts = [];
// eslint-disable-next-line no-restricted-syntax
for await (const part of response) {
parts.push(part);
}

if (parts.length) {
switch (responseType) {
case 'json': {
Object.defineProperty(response, 'body', {
get() {
let value = Buffer.concat(parts);
try {
value = JSON.parse(value);
} catch (err) {
Object.defineProperty(err, 'response', response);
throw err;
} finally {
Object.defineProperty(response, 'body', { value, configurable: true });
}
return value;
},
configurable: true,
});
break;
}
case undefined:
case 'buffer': {
Object.defineProperty(response, 'body', {
get() {
const value = Buffer.concat(parts);
Object.defineProperty(response, 'body', { value, configurable: true });
return value;
},
configurable: true,
});
break;
}
default:
throw new TypeError('unsupported responseType request option');
}
}

return response;
})().catch((err) => {
Object.defineProperty(err, 'response', response);
throw err;
});
};

module.exports.setDefaults = setDefaults;
module.exports.setDefaults = setDefaults.bind(undefined, allowed);
7 changes: 3 additions & 4 deletions lib/issuer.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const { inspect } = require('util');
const url = require('url');

const jose = require('jose');
const LRU = require('lru-cache');
const QuickLRU = require('quick-lru');
const objectHash = require('object-hash');

const { RPError } = require('./errors');
Expand Down Expand Up @@ -57,7 +57,7 @@ class Issuer {
}
});

instance(this).set('cache', new LRU({ max: 100 }));
instance(this).set('cache', new QuickLRU({ maxSize: 100 }));

registry.set(this.issuer, this);

Expand All @@ -80,7 +80,7 @@ class Issuer {
const cache = instance(this).get('cache');

if (reload || !keystore) {
cache.reset();
cache.clear();
const response = await request.call(this, {
method: 'GET',
responseType: 'json',
Expand Down Expand Up @@ -168,7 +168,6 @@ class Issuer {
url: webfingerUrl,
responseType: 'json',
searchParams: { resource, rel: REL },
followRedirect: true,
});
const body = processResponse(response);

Expand Down
Loading

0 comments on commit 83376ac

Please sign in to comment.