diff --git a/lib/client.js b/lib/client.js index d37f92e4..0024c573 100644 --- a/lib/client.js +++ b/lib/client.js @@ -10,14 +10,12 @@ const url = require('url'); const { ParseError } = require('got'); const jose = require('jose'); const base64url = require('base64url'); -const defaultsDeep = require('lodash/defaultsDeep'); -const defaults = require('lodash/defaults'); -const merge = require('lodash/merge'); -const isPlainObject = require('lodash/isPlainObject'); const tokenHash = require('oidc-token-hash'); +const defaults = require('./helpers/defaults'); const { assertSigningAlgValuesSupport, assertIssuerConfiguration } = require('./helpers/assert'); const pick = require('./helpers/pick'); +const isPlainObject = require('./helpers/is_plain_object'); const processResponse = require('./helpers/process_response'); const TokenSet = require('./token_set'); const { OPError, RPError } = require('./errors'); @@ -32,6 +30,8 @@ const instance = require('./helpers/weak_cache'); const { authenticatedPost, resolveResponseType, resolveRedirectUri } = require('./helpers/client'); const DeviceFlowHandle = require('./device_flow_handle'); +const { deep: defaultsDeep } = defaults; + function pickCb(input) { return pick(input, ...CALLBACK_PROPERTIES); } @@ -1488,26 +1488,21 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base * @name requestObject * @api public */ - async requestObject(requestObject = {}, algorithms = {}) { + async requestObject(requestObject = {}, { + sign: signingAlgorithm = this.request_object_signing_alg || 'none', + encrypt: { + alg: eKeyManagement = this.request_object_encryption_alg, + enc: eContentEncryption = this.request_object_encryption_enc || 'A128CBC-HS256', + } = {}, + } = {}) { if (!isPlainObject(requestObject)) { throw new TypeError('requestObject must be a plain object'); } - defaults(algorithms, { - sign: this.request_object_signing_alg, - encrypt: { - alg: this.request_object_encryption_alg, - enc: this.request_object_encryption_enc || 'A128CBC-HS256', - }, - }, { - sign: 'none', - }); - let signed; let key; - const alg = algorithms.sign; - const header = { alg, typ: 'JWT' }; + const header = { alg: signingAlgorithm, typ: 'JWT' }; const payload = JSON.stringify(defaults({}, requestObject, { iss: this.client_id, aud: this.issuer.issuer, @@ -1517,25 +1512,25 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base exp: now() + 300, })); - if (alg === 'none') { + if (signingAlgorithm === 'none') { signed = [ base64url.encode(JSON.stringify(header)), base64url.encode(payload), '', ].join('.'); } else { - const symmetric = alg.startsWith('HS'); + const symmetric = signingAlgorithm.startsWith('HS'); if (symmetric) { key = await this.joseSecret(); } else { const keystore = instance(this).get('keystore'); if (!keystore) { - throw new TypeError(`no keystore present for client, cannot sign using alg ${alg}`); + throw new TypeError(`no keystore present for client, cannot sign using alg ${signingAlgorithm}`); } - key = keystore.get({ alg, use: 'sig' }); + key = keystore.get({ alg: signingAlgorithm, use: 'sig' }); if (!key) { - throw new TypeError(`no key to sign with found for alg ${alg}`); + throw new TypeError(`no key to sign with found for alg ${signingAlgorithm}`); } } @@ -1545,11 +1540,11 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base }); } - if (!algorithms.encrypt.alg) { + if (!eKeyManagement) { return signed; } - const fields = { alg: algorithms.encrypt.alg, enc: algorithms.encrypt.enc, cty: 'JWT' }; + const fields = { alg: eKeyManagement, enc: eContentEncryption, cty: 'JWT' }; if (fields.alg.match(/^(RSA|ECDH)/)) { [key] = await this.issuer.queryKeyStore({ @@ -1599,10 +1594,10 @@ BaseClient.prototype.resource = deprecate( /* istanbul ignore next */ async function resource(resourceUrl, accessToken, options) { let token = accessToken; - const opts = merge({ + const opts = defaultsDeep({}, options, { verb: 'GET', via: 'header', - }, options); + }); if (token instanceof TokenSet) { if (!token.access_token) { diff --git a/lib/errors.js b/lib/errors.js index 83ed4033..cc45b835 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -1,7 +1,6 @@ /* eslint-disable camelcase */ const { format } = require('util'); -const assign = require('lodash/assign'); const makeError = require('make-error'); function OPError({ @@ -14,7 +13,7 @@ function OPError({ }, response) { OPError.super.call(this, !error_description ? error : `${error} (${error_description})`); - assign( + Object.assign( this, { error }, (error_description && { error_description }), @@ -45,7 +44,7 @@ function RPError(...args) { } else { RPError.super.call(this, message); } - assign(this, rest); + Object.assign(this, rest); if (response) { Object.defineProperty(this, 'response', { value: response, diff --git a/lib/helpers/client.js b/lib/helpers/client.js index e26923f1..3dc98644 100644 --- a/lib/helpers/client.js +++ b/lib/helpers/client.js @@ -1,5 +1,3 @@ -const merge = require('lodash/merge'); -const omitBy = require('lodash/omitBy'); const jose = require('jose'); const { assertIssuerConfiguration } = require('./assert'); @@ -7,6 +5,7 @@ const { random } = require('./generators'); const now = require('./unix_timestamp'); const request = require('./request'); const instance = require('./weak_cache'); +const { deep: defaultsDeep } = require('./defaults'); const formUrlEncode = (value) => encodeURIComponent(value).replace(/%20/g, '+'); @@ -126,7 +125,7 @@ async function authenticatedPost(endpoint, opts, { clientAssertionPayload, endpointAuthMethod = endpoint, } = {}) { const auth = await authFor.call(this, endpointAuthMethod, { clientAssertionPayload }); - const requestOpts = merge(opts, auth, { form: true }); + const requestOpts = defaultsDeep({ form: true }, auth, opts); const mTLS = this[`${endpointAuthMethod}_endpoint_auth_method`].includes('tls_client_auth') || (endpoint === 'token' && this.tls_client_certificate_bound_access_tokens); @@ -139,7 +138,11 @@ async function authenticatedPost(endpoint, opts, { targetUrl = targetUrl || this.issuer[`${endpoint}_endpoint`]; if ('body' in requestOpts) { - requestOpts.body = omitBy(requestOpts.body, (arg) => arg === undefined); + for (const [key, value] of Object.entries(requestOpts.body)) { // eslint-disable-line no-restricted-syntax, max-len + if (typeof value === 'undefined') { + delete requestOpts.body[key]; + } + } } return request.call(this, { diff --git a/lib/helpers/deep_clone.js b/lib/helpers/deep_clone.js new file mode 100644 index 00000000..74f6de77 --- /dev/null +++ b/lib/helpers/deep_clone.js @@ -0,0 +1 @@ +module.exports = (obj) => JSON.parse(JSON.stringify(obj)); diff --git a/lib/helpers/defaults.js b/lib/helpers/defaults.js new file mode 100644 index 00000000..2c7e3473 --- /dev/null +++ b/lib/helpers/defaults.js @@ -0,0 +1,25 @@ +/* eslint-disable no-restricted-syntax */ + +const isPlainObject = require('./is_plain_object'); + +function defaults(deep, target, ...sources) { + for (const source of sources) { + if (!isPlainObject(source)) { + continue; // eslint-disable-line no-continue + } + for (const [key, value] of Object.entries(source)) { + if (typeof target[key] === 'undefined' && typeof value !== 'undefined') { + target[key] = value; + } + + if (deep && isPlainObject(target[key]) && isPlainObject(value)) { + defaults(true, target[key], value); + } + } + } + + return target; +} + +module.exports = defaults.bind(undefined, false); +module.exports.deep = defaults.bind(undefined, true); diff --git a/lib/helpers/is_plain_object.js b/lib/helpers/is_plain_object.js new file mode 100644 index 00000000..d3020126 --- /dev/null +++ b/lib/helpers/is_plain_object.js @@ -0,0 +1 @@ +module.exports = (a) => !!a && a.constructor === Object; diff --git a/lib/helpers/request.js b/lib/helpers/request.js index 065c1e3d..ac494ddf 100644 --- a/lib/helpers/request.js +++ b/lib/helpers/request.js @@ -1,8 +1,8 @@ const Got = require('got'); -const defaultsDeep = require('lodash/defaultsDeep'); const pkg = require('../../package.json'); +const { deep: defaultsDeep } = require('./defaults'); const isAbsoluteUrl = require('./is_absolute_url'); const { HTTP_OPTIONS } = require('./consts'); @@ -10,7 +10,7 @@ let DEFAULT_HTTP_OPTIONS; let got; const setDefaults = (options) => { - DEFAULT_HTTP_OPTIONS = defaultsDeep(options, DEFAULT_HTTP_OPTIONS); + DEFAULT_HTTP_OPTIONS = defaultsDeep({}, options, DEFAULT_HTTP_OPTIONS); got = Got.extend(DEFAULT_HTTP_OPTIONS); }; @@ -28,7 +28,7 @@ module.exports = function request(options, { mTLS = false } = {}) { const optsFn = this[HTTP_OPTIONS]; let opts; if (optsFn) { - opts = optsFn.call(this, defaultsDeep(options, DEFAULT_HTTP_OPTIONS)); + opts = optsFn.call(this, defaultsDeep({}, options, DEFAULT_HTTP_OPTIONS)); } else { opts = options; } diff --git a/lib/passport_strategy.js b/lib/passport_strategy.js index abd633d1..4d37ce41 100644 --- a/lib/passport_strategy.js +++ b/lib/passport_strategy.js @@ -3,8 +3,7 @@ const url = require('url'); const { format } = require('util'); -const cloneDeep = require('lodash/cloneDeep'); - +const cloneDeep = require('./helpers/deep_clone'); const { RPError, OPError } = require('./errors'); const { BaseClient } = require('./client'); const { random, codeChallenge } = require('./helpers/generators'); diff --git a/lib/token_set.js b/lib/token_set.js index b3c188c3..96fece3e 100644 --- a/lib/token_set.js +++ b/lib/token_set.js @@ -1,5 +1,4 @@ const base64url = require('base64url'); -const assign = require('lodash/assign'); const now = require('./helpers/unix_timestamp'); @@ -9,7 +8,7 @@ class TokenSet { * @api public */ constructor(values) { - assign(this, values); + Object.assign(this, values); } /** diff --git a/package.json b/package.json index 6c3cc375..5022acf1 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ "base64url": "^3.0.1", "got": "^9.6.0", "jose": "^1.27.1", - "lodash": "^4.17.15", "lru-cache": "^5.1.1", "make-error": "^1.3.6", "object-hash": "^2.0.1",