openid-client 4.9.0 → 5.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,110 @@
1
+ const objectHash = require('object-hash');
2
+ const LRU = require('lru-cache');
3
+
4
+ const { RPError } = require('../errors');
5
+
6
+ const { assertIssuerConfiguration } = require('./assert');
7
+ const KeyStore = require('./keystore');
8
+ const { keystores } = require('./weak_cache');
9
+ const processResponse = require('./process_response');
10
+ const request = require('./request');
11
+
12
+ const inFlight = new WeakMap();
13
+ const caches = new WeakMap();
14
+ const lrus = (ctx) => {
15
+ if (!caches.has(ctx)) {
16
+ caches.set(ctx, new LRU({ max: 100 }));
17
+ }
18
+ return caches.get(ctx);
19
+ };
20
+
21
+ async function getKeyStore(reload = false) {
22
+ assertIssuerConfiguration(this, 'jwks_uri');
23
+
24
+ const keystore = keystores.get(this);
25
+ const cache = lrus(this);
26
+
27
+ if (reload || !keystore) {
28
+ if (inFlight.has(this)) {
29
+ return inFlight.get(this);
30
+ }
31
+ cache.reset();
32
+ inFlight.set(
33
+ this,
34
+ (async () => {
35
+ const response = await request
36
+ .call(this, {
37
+ method: 'GET',
38
+ responseType: 'json',
39
+ url: this.jwks_uri,
40
+ headers: {
41
+ Accept: 'application/json',
42
+ },
43
+ })
44
+ .finally(() => {
45
+ inFlight.delete(this);
46
+ });
47
+ const jwks = processResponse(response);
48
+
49
+ const joseKeyStore = KeyStore.fromJWKS(jwks, { onlyPublic: true });
50
+ cache.set('throttle', true, 60 * 1000);
51
+ keystores.set(this, joseKeyStore);
52
+
53
+ return joseKeyStore;
54
+ })(),
55
+ );
56
+
57
+ return inFlight.get(this);
58
+ }
59
+
60
+ return keystore;
61
+ }
62
+
63
+ async function queryKeyStore({ kid, kty, alg, use }, { allowMulti = false } = {}) {
64
+ const cache = lrus(this);
65
+
66
+ const def = {
67
+ kid,
68
+ kty,
69
+ alg,
70
+ use,
71
+ };
72
+
73
+ const defHash = objectHash(def, {
74
+ algorithm: 'sha256',
75
+ ignoreUnknown: true,
76
+ unorderedArrays: true,
77
+ unorderedSets: true,
78
+ });
79
+
80
+ // refresh keystore on every unknown key but also only upto once every minute
81
+ const freshJwksUri = cache.get(defHash) || cache.get('throttle');
82
+
83
+ const keystore = await getKeyStore.call(this, !freshJwksUri);
84
+ const keys = keystore.all(def);
85
+
86
+ delete def.use;
87
+ if (keys.length === 0) {
88
+ throw new RPError({
89
+ printf: ["no valid key found in issuer's jwks_uri for key parameters %j", def],
90
+ jwks: keystore,
91
+ });
92
+ }
93
+
94
+ if (!allowMulti && keys.length > 1 && !kid) {
95
+ throw new RPError({
96
+ printf: [
97
+ "multiple matching keys found in issuer's jwks_uri for key parameters %j, kid must be provided in this case",
98
+ def,
99
+ ],
100
+ jwks: keystore,
101
+ });
102
+ }
103
+
104
+ cache.set(defHash, true);
105
+
106
+ return keys;
107
+ }
108
+
109
+ module.exports.queryKeyStore = queryKeyStore;
110
+ module.exports.keystore = getKeyStore;
@@ -0,0 +1,312 @@
1
+ const v8 = require('v8');
2
+
3
+ const jose = require('jose');
4
+
5
+ const clone = globalThis.structuredClone || ((value) => v8.deserialize(v8.serialize(value)));
6
+
7
+ const isPlainObject = require('./is_plain_object');
8
+ const isKeyObject = require('./is_key_object');
9
+
10
+ const internal = Symbol();
11
+
12
+ function fauxAlg(kty) {
13
+ switch (kty) {
14
+ case 'RSA':
15
+ return 'RSA-OAEP';
16
+ case 'EC':
17
+ return 'ECDH-ES';
18
+ case 'OKP':
19
+ return 'ECDH-ES';
20
+ case 'oct':
21
+ return 'HS256';
22
+ default:
23
+ return undefined;
24
+ }
25
+ }
26
+
27
+ const keyscore = (key, { alg, use }) => {
28
+ let score = 0;
29
+
30
+ if (alg && key.alg) {
31
+ score++;
32
+ }
33
+
34
+ if (use && key.use) {
35
+ score++;
36
+ }
37
+
38
+ return score;
39
+ };
40
+
41
+ function getKtyFromAlg(alg) {
42
+ switch (typeof alg === 'string' && alg.substr(0, 2)) {
43
+ case 'RS':
44
+ case 'PS':
45
+ return 'RSA';
46
+ case 'ES':
47
+ return 'EC';
48
+ case 'Ed':
49
+ return 'OKP';
50
+ default:
51
+ return undefined;
52
+ }
53
+ }
54
+
55
+ function getAlgorithms(use, alg, kty, crv) {
56
+ // Ed25519, Ed448, and secp256k1 always have "alg"
57
+ // OKP always has use
58
+ if (alg) {
59
+ return new Set([alg]);
60
+ }
61
+
62
+ switch (kty) {
63
+ case 'EC': {
64
+ let algs = [];
65
+
66
+ if (use === 'enc' || use === undefined) {
67
+ algs = algs.concat(['ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW']);
68
+ }
69
+
70
+ if (use === 'sig' || use === undefined) {
71
+ algs = algs.concat([`ES${crv.substr(-3)}`.replace('21', '12')]);
72
+ }
73
+
74
+ return new Set(algs);
75
+ }
76
+ case 'OKP': {
77
+ return new Set(['ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW']);
78
+ }
79
+ case 'RSA': {
80
+ let algs = [];
81
+
82
+ if (use === 'enc' || use === undefined) {
83
+ algs = algs.concat(['RSA-OAEP', 'RSA-OAEP-256', 'RSA-OAEP-384', 'RSA-OAEP-512', 'RSA1_5']);
84
+ }
85
+
86
+ if (use === 'sig' || use === undefined) {
87
+ algs = algs.concat(['PS256', 'PS384', 'PS512', 'RS256', 'RS384', 'RS512']);
88
+ }
89
+
90
+ return new Set(algs);
91
+ }
92
+ default:
93
+ throw new Error('unreachable');
94
+ }
95
+ }
96
+
97
+ module.exports = class KeyStore {
98
+ #keys;
99
+
100
+ constructor(i, keys) {
101
+ if (i !== internal) throw new Error('invalid constructor call');
102
+ this.#keys = keys;
103
+ }
104
+
105
+ toJWKS() {
106
+ return {
107
+ keys: this.map(({ jwk: { d, p, q, dp, dq, qi, ...jwk } }) => jwk),
108
+ };
109
+ }
110
+
111
+ all({ alg, kid, use } = {}) {
112
+ if (!use || !alg) {
113
+ throw new Error();
114
+ }
115
+
116
+ const kty = getKtyFromAlg(alg);
117
+
118
+ const search = { alg, use };
119
+ return this.filter((key) => {
120
+ let candidate = true;
121
+
122
+ if (candidate && kty !== undefined && key.jwk.kty !== kty) {
123
+ candidate = false;
124
+ }
125
+
126
+ if (candidate && kid !== undefined && key.jwk.kid !== kid) {
127
+ candidate = false;
128
+ }
129
+
130
+ if (candidate && use !== undefined && key.jwk.use !== undefined && key.jwk.use !== use) {
131
+ candidate = false;
132
+ }
133
+
134
+ if (candidate && key.jwk.alg && key.jwk.alg !== alg) {
135
+ candidate = false;
136
+ } else if (!key.algorithms.has(alg)) {
137
+ candidate = false;
138
+ }
139
+
140
+ return candidate;
141
+ }).sort((first, second) => keyscore(second, search) - keyscore(first, search));
142
+ }
143
+
144
+ get(...args) {
145
+ return this.all(...args)[0];
146
+ }
147
+
148
+ static async fromJWKS(jwks, { onlyPublic = false, onlyPrivate = false } = {}) {
149
+ if (
150
+ !isPlainObject(jwks) ||
151
+ !Array.isArray(jwks.keys) ||
152
+ jwks.keys.some((k) => !isPlainObject(k) || !('kty' in k))
153
+ ) {
154
+ throw new TypeError('jwks must be a JSON Web Key Set formatted object');
155
+ }
156
+
157
+ const keys = [];
158
+
159
+ for (let jwk of jwks.keys) {
160
+ jwk = clone(jwk);
161
+ const { kty, kid, crv } = jwk;
162
+
163
+ let { alg, use } = jwk;
164
+
165
+ if (typeof kty !== 'string' || !kty) {
166
+ continue;
167
+ }
168
+
169
+ if (use !== undefined && use !== 'sig' && use !== 'enc') {
170
+ continue;
171
+ }
172
+
173
+ if (typeof alg !== 'string' && alg !== undefined) {
174
+ continue;
175
+ }
176
+
177
+ if (typeof kid !== 'string' && kid !== undefined) {
178
+ continue;
179
+ }
180
+
181
+ if (kty === 'EC' && use === 'sig') {
182
+ switch (crv) {
183
+ case 'P-256':
184
+ alg = 'ES256';
185
+ break;
186
+ case 'P-384':
187
+ alg = 'ES384';
188
+ break;
189
+ case 'P-521':
190
+ alg = 'ES512';
191
+ break;
192
+ default:
193
+ break;
194
+ }
195
+ }
196
+
197
+ if (crv === 'secp256k1') {
198
+ use = 'sig';
199
+ alg = 'ES256K';
200
+ }
201
+
202
+ if (kty === 'OKP') {
203
+ switch (crv) {
204
+ case 'Ed25519':
205
+ case 'Ed448':
206
+ use = 'sig';
207
+ alg = 'EdDSA';
208
+ break;
209
+ case 'X25519':
210
+ case 'X448':
211
+ use = 'enc';
212
+ break;
213
+ default:
214
+ break;
215
+ }
216
+ }
217
+
218
+ if (alg && !use) {
219
+ switch (true) {
220
+ case alg.startsWith('ECDH'):
221
+ use = 'enc';
222
+ break;
223
+ case alg.startsWith('RSA'):
224
+ use = 'enc';
225
+ break;
226
+ default:
227
+ break;
228
+ }
229
+ }
230
+
231
+ const keyObject = await jose.importJWK(jwk, alg || fauxAlg(jwk.kty)).catch(() => {});
232
+
233
+ if (!keyObject) continue;
234
+
235
+ if (keyObject instanceof Uint8Array || keyObject.type === 'secret') {
236
+ if (onlyPrivate) {
237
+ throw new Error('jwks must only contain private keys');
238
+ }
239
+ continue;
240
+ }
241
+
242
+ if (!isKeyObject(keyObject)) {
243
+ throw new Error('what?!');
244
+ }
245
+
246
+ if (onlyPrivate && keyObject.type !== 'private') {
247
+ throw new Error('jwks must only contain private keys');
248
+ }
249
+
250
+ if (onlyPublic && keyObject.type !== 'public') {
251
+ continue;
252
+ }
253
+
254
+ if (kty === 'RSA' && keyObject.asymmetricKeySize < 2048) {
255
+ continue;
256
+ }
257
+
258
+ keys.push({
259
+ jwk: { ...jwk, alg, use },
260
+ keyObject,
261
+ get algorithms() {
262
+ Object.defineProperty(this, 'algorithms', {
263
+ value: getAlgorithms(this.jwk.use, this.jwk.alg, this.jwk.kty, this.jwk.crv),
264
+ enumerable: true,
265
+ configurable: false,
266
+ });
267
+ return this.algorithms;
268
+ },
269
+ });
270
+ }
271
+
272
+ return new this(internal, keys);
273
+ }
274
+
275
+ filter(...args) {
276
+ return this.#keys.filter(...args);
277
+ }
278
+
279
+ find(...args) {
280
+ return this.#keys.find(...args);
281
+ }
282
+
283
+ every(...args) {
284
+ return this.#keys.every(...args);
285
+ }
286
+
287
+ some(...args) {
288
+ return this.#keys.some(...args);
289
+ }
290
+
291
+ map(...args) {
292
+ return this.#keys.map(...args);
293
+ }
294
+
295
+ forEach(...args) {
296
+ return this.#keys.forEach(...args);
297
+ }
298
+
299
+ reduce(...args) {
300
+ return this.#keys.reduce(...args);
301
+ }
302
+
303
+ sort(...args) {
304
+ return this.#keys.sort(...args);
305
+ }
306
+
307
+ *[Symbol.iterator]() {
308
+ for (const key of this.#keys) {
309
+ yield key;
310
+ }
311
+ }
312
+ };
@@ -1,5 +1,3 @@
1
- /* eslint-disable no-restricted-syntax, no-param-reassign, no-continue */
2
-
3
1
  const isPlainObject = require('./is_plain_object');
4
2
 
5
3
  function merge(target, ...sources) {
@@ -1,6 +1,6 @@
1
1
  module.exports = function pick(object, ...paths) {
2
2
  const obj = {};
3
- for (const path of paths) { // eslint-disable-line no-restricted-syntax
3
+ for (const path of paths) {
4
4
  if (object[path]) {
5
5
  obj[path] = object[path];
6
6
  }
@@ -7,7 +7,7 @@ const REGEXP = /(\w+)=("[^"]*")/g;
7
7
  const throwAuthenticateErrors = (response) => {
8
8
  const params = {};
9
9
  try {
10
- while ((REGEXP.exec(response.headers['www-authenticate'])) !== null) {
10
+ while (REGEXP.exec(response.headers['www-authenticate']) !== null) {
11
11
  if (RegExp.$1 && RegExp.$2) {
12
12
  params[RegExp.$1] = RegExp.$2.slice(1, -1);
13
13
  }
@@ -29,7 +29,7 @@ const isStandardBodyError = (response) => {
29
29
  jsonbody = response.body;
30
30
  }
31
31
  result = typeof jsonbody.error === 'string' && jsonbody.error.length;
32
- if (result) response.body = jsonbody;
32
+ if (result) Object.defineProperty(response, 'body', { value: jsonbody, configurable: true });
33
33
  } catch (err) {}
34
34
 
35
35
  return result;
@@ -45,15 +45,31 @@ function processResponse(response, { statusCode = 200, body = true, bearer = fal
45
45
  throw new OPError(response.body, response);
46
46
  }
47
47
 
48
- throw new OPError({
49
- error: format('expected %i %s, got: %i %s', statusCode, STATUS_CODES[statusCode], response.statusCode, STATUS_CODES[response.statusCode]),
50
- }, response);
48
+ throw new OPError(
49
+ {
50
+ error: format(
51
+ 'expected %i %s, got: %i %s',
52
+ statusCode,
53
+ STATUS_CODES[statusCode],
54
+ response.statusCode,
55
+ STATUS_CODES[response.statusCode],
56
+ ),
57
+ },
58
+ response,
59
+ );
51
60
  }
52
61
 
53
62
  if (body && !response.body) {
54
- throw new OPError({
55
- error: format('expected %i %s with body but no body was returned', statusCode, STATUS_CODES[statusCode]),
56
- }, response);
63
+ throw new OPError(
64
+ {
65
+ error: format(
66
+ 'expected %i %s with body but no body was returned',
67
+ statusCode,
68
+ STATUS_CODES[statusCode],
69
+ ),
70
+ },
71
+ response,
72
+ );
57
73
  }
58
74
 
59
75
  return response.body;
@@ -1,56 +1,182 @@
1
- const Got = require('got');
1
+ const assert = require('assert');
2
+ const querystring = require('querystring');
3
+ const http = require('http');
4
+ const https = require('https');
5
+ const { once } = require('events');
2
6
 
3
7
  const pkg = require('../../package.json');
8
+ const { RPError } = require('../errors');
4
9
 
10
+ const pick = require('./pick');
5
11
  const { deep: defaultsDeep } = require('./defaults');
6
- const isAbsoluteUrl = require('./is_absolute_url');
7
12
  const { HTTP_OPTIONS } = require('./consts');
8
13
 
9
14
  let DEFAULT_HTTP_OPTIONS;
10
- let got;
11
15
 
12
- const setDefaults = (options) => {
13
- DEFAULT_HTTP_OPTIONS = defaultsDeep({}, options, DEFAULT_HTTP_OPTIONS);
14
- got = Got.extend(DEFAULT_HTTP_OPTIONS);
16
+ const allowed = [
17
+ 'agent',
18
+ 'ca',
19
+ 'cert',
20
+ 'crl',
21
+ 'headers',
22
+ 'key',
23
+ 'lookup',
24
+ 'passphrase',
25
+ 'pfx',
26
+ 'timeout',
27
+ ];
28
+
29
+ const setDefaults = (props, options) => {
30
+ DEFAULT_HTTP_OPTIONS = defaultsDeep(
31
+ {},
32
+ props.length ? pick(options, ...props) : options,
33
+ DEFAULT_HTTP_OPTIONS,
34
+ );
15
35
  };
16
36
 
17
- setDefaults({
18
- followRedirect: false,
37
+ setDefaults([], {
19
38
  headers: { 'User-Agent': `${pkg.name}/${pkg.version} (${pkg.homepage})` },
20
- retry: 0,
21
39
  timeout: 3500,
22
- throwHttpErrors: false,
23
40
  });
24
41
 
42
+ function send(req, body, contentType) {
43
+ if (contentType) {
44
+ req.removeHeader('content-type');
45
+ req.setHeader('content-type', contentType);
46
+ }
47
+ if (body) {
48
+ req.removeHeader('content-length');
49
+ req.setHeader('content-length', Buffer.byteLength(body));
50
+ req.write(body);
51
+ }
52
+ req.end();
53
+ }
54
+
25
55
  module.exports = async function request(options, { accessToken, mTLS = false, DPoP } = {}) {
26
- const { url } = options;
27
- isAbsoluteUrl(url);
56
+ let url;
57
+ try {
58
+ url = new URL(options.url);
59
+ delete options.url;
60
+ assert(/^(https?:)$/.test(url.protocol));
61
+ } catch (err) {
62
+ throw new TypeError('only valid absolute URLs can be requested');
63
+ }
28
64
  const optsFn = this[HTTP_OPTIONS];
29
65
  let opts = options;
30
66
 
31
67
  if (DPoP && 'dpopProof' in this) {
32
68
  opts.headers = opts.headers || {};
33
- opts.headers.DPoP = this.dpopProof({
34
- htu: url,
35
- htm: options.method,
36
- }, DPoP, accessToken);
69
+ opts.headers.DPoP = await this.dpopProof(
70
+ {
71
+ htu: url,
72
+ htm: options.method,
73
+ },
74
+ DPoP,
75
+ accessToken,
76
+ );
37
77
  }
38
78
 
79
+ let userOptions;
39
80
  if (optsFn) {
40
- opts = optsFn.call(this, defaultsDeep({}, opts, DEFAULT_HTTP_OPTIONS));
81
+ userOptions = pick(
82
+ optsFn.call(this, url, defaultsDeep({}, opts, DEFAULT_HTTP_OPTIONS)),
83
+ ...allowed,
84
+ );
41
85
  }
86
+ opts = defaultsDeep({}, userOptions, opts, DEFAULT_HTTP_OPTIONS);
42
87
 
43
- if (
44
- mTLS
45
- && (
46
- (!opts.key || !opts.cert)
47
- && (!opts.https || !((opts.https.key && opts.https.certificate) || opts.https.pfx))
48
- )
49
- ) {
88
+ if (mTLS && !opts.pfx && !(opts.key && opts.cert)) {
50
89
  throw new TypeError('mutual-TLS certificate and key not set');
51
90
  }
52
91
 
53
- return got(opts);
92
+ if (opts.searchParams) {
93
+ for (const [key, value] of Object.entries(opts.searchParams)) {
94
+ url.searchParams.delete(key);
95
+ url.searchParams.set(key, value);
96
+ }
97
+ }
98
+
99
+ let responseType;
100
+ let form;
101
+ let json;
102
+ let body;
103
+ ({ form, responseType, json, body, ...opts } = opts);
104
+
105
+ for (const [key, value] of Object.entries(opts.headers || {})) {
106
+ if (value === undefined) {
107
+ delete opts.headers[key];
108
+ }
109
+ }
110
+
111
+ let response;
112
+ const req = (url.protocol === 'https:' ? https.request : http.request)(url, opts);
113
+ return (async () => {
114
+ if (json) {
115
+ send(req, JSON.stringify(json), 'application/json');
116
+ } else if (form) {
117
+ send(req, querystring.stringify(form), 'application/x-www-form-urlencoded');
118
+ } else if (body) {
119
+ send(req, body);
120
+ } else {
121
+ send(req);
122
+ }
123
+
124
+ [response] = await Promise.race([once(req, 'response'), once(req, 'timeout')]);
125
+
126
+ // timeout reached
127
+ if (!response) {
128
+ req.destroy();
129
+ throw new RPError(`outgoing request timed out after ${opts.timeout}ms`);
130
+ }
131
+
132
+ const parts = [];
133
+
134
+ for await (const part of response) {
135
+ parts.push(part);
136
+ }
137
+
138
+ if (parts.length) {
139
+ switch (responseType) {
140
+ case 'json': {
141
+ Object.defineProperty(response, 'body', {
142
+ get() {
143
+ let value = Buffer.concat(parts);
144
+ try {
145
+ value = JSON.parse(value);
146
+ } catch (err) {
147
+ Object.defineProperty(err, 'response', { value: response });
148
+ throw err;
149
+ } finally {
150
+ Object.defineProperty(response, 'body', { value, configurable: true });
151
+ }
152
+ return value;
153
+ },
154
+ configurable: true,
155
+ });
156
+ break;
157
+ }
158
+ case undefined:
159
+ case 'buffer': {
160
+ Object.defineProperty(response, 'body', {
161
+ get() {
162
+ const value = Buffer.concat(parts);
163
+ Object.defineProperty(response, 'body', { value, configurable: true });
164
+ return value;
165
+ },
166
+ configurable: true,
167
+ });
168
+ break;
169
+ }
170
+ default:
171
+ throw new TypeError('unsupported responseType request option');
172
+ }
173
+ }
174
+
175
+ return response;
176
+ })().catch((err) => {
177
+ Object.defineProperty(err, 'response', { value: response });
178
+ throw err;
179
+ });
54
180
  };
55
181
 
56
- module.exports.setDefaults = setDefaults;
182
+ module.exports.setDefaults = setDefaults.bind(undefined, allowed);
@@ -1,8 +1 @@
1
- const privateProps = new WeakMap();
2
-
3
- module.exports = (ctx) => {
4
- if (!privateProps.has(ctx)) {
5
- privateProps.set(ctx, new Map([['metadata', new Map()]]));
6
- }
7
- return privateProps.get(ctx);
8
- };
1
+ module.exports.keystores = new WeakMap();