@mongodb-js/oidc-plugin 1.1.7 → 2.0.0

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.
package/dist/plugin.js CHANGED
@@ -15,25 +15,35 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
15
15
  }) : function(o, v) {
16
16
  o["default"] = v;
17
17
  });
18
- var __importStar = (this && this.__importStar) || function (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
24
- };
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
25
35
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.MongoDBOIDCPluginImpl = exports.automaticRefreshTimeoutMS = void 0;
36
+ exports.MongoDBOIDCPluginImpl = void 0;
37
+ exports.automaticRefreshTimeoutMS = automaticRefreshTimeoutMS;
27
38
  const types_1 = require("./types");
28
39
  const util_1 = require("./util");
29
- const openid_client_1 = require("openid-client");
30
- const openid_client_2 = require("openid-client");
31
40
  const rfc_8252_http_server_1 = require("./rfc-8252-http-server");
32
- const util_2 = require("util");
33
- const crypto_1 = require("crypto");
34
41
  const events_1 = require("events");
35
42
  const api_1 = require("./api");
36
43
  const child_process_1 = require("child_process");
44
+ const openid_client_1 = require("openid-client");
45
+ const https_1 = require("https");
46
+ const http_1 = require("http");
37
47
  // eslint-disable-next-line @typescript-eslint/consistent-type-imports
38
48
  let _electron = undefined;
39
49
  async function getDefaultOpenBrowser() {
@@ -41,7 +51,7 @@ async function getDefaultOpenBrowser() {
41
51
  // to open a browser.
42
52
  if (process.versions.electron && _electron !== 'cannot-require') {
43
53
  try {
44
- _electron ?? (_electron = await Promise.resolve().then(() => __importStar(require('electron'))));
54
+ _electron ??= await Promise.resolve().then(() => __importStar(require('electron')));
45
55
  return ({ url }) =>
46
56
  // eslint-disable-next-line @typescript-eslint/consistent-type-imports
47
57
  _electron.shell.openExternal(url);
@@ -88,17 +98,17 @@ function tokenExpiryInSeconds(tokenSet = {}, passIdTokenAsAccessToken = false, n
88
98
  // specifies the token expiry. Otherwise, we assume that the `expires_at`
89
99
  // value presented by the OIDC provider is correct, since OIDC clients are
90
100
  // not supposed to inspect access tokens and treat them as opaque.
91
- const expiresAt = (tokenSet.id_token &&
101
+ const expiresAt = (tokenSet.idTokenClaims?.exp !== undefined &&
92
102
  passIdTokenAsAccessToken &&
93
- tokenSet.claims?.().exp) ||
94
- tokenSet.expires_at ||
103
+ tokenSet.idTokenClaims?.exp) ||
104
+ tokenSet.expiresAt ||
95
105
  0;
96
106
  return Math.max(0, (expiresAt ?? 0) - now / 1000);
97
107
  }
98
108
  /** @internal Exported for testing only */
99
109
  function automaticRefreshTimeoutMS(tokenSet, passIdTokenAsAccessToken = false, now = Date.now()) {
100
110
  const expiresIn = tokenExpiryInSeconds(tokenSet, passIdTokenAsAccessToken, now);
101
- if (!tokenSet.refresh_token || !expiresIn)
111
+ if (!tokenSet.refreshToken || !expiresIn)
102
112
  return;
103
113
  // If the tokens expire in more than 1 minute, automatically register
104
114
  // a refresh handler. (They should not expire in less; however,
@@ -110,9 +120,9 @@ function automaticRefreshTimeoutMS(tokenSet, passIdTokenAsAccessToken = false, n
110
120
  return Math.max(expiresIn - 300 /* 5 minutes */, expiresIn / 2) * 1000;
111
121
  }
112
122
  }
113
- exports.automaticRefreshTimeoutMS = automaticRefreshTimeoutMS;
114
123
  const kEnableFallback = Symbol.for('@@mdb.oidcplugin.kEnableFallback');
115
124
  let updateIdCounter = 0;
125
+ let authStateIdCounter = 0;
116
126
  function allowFallbackIfFailed(promise) {
117
127
  return promise.catch((err) => {
118
128
  // Tell the outer logic here to fallback to device auth flow if it is
@@ -125,9 +135,13 @@ function allowFallbackIfFailed(promise) {
125
135
  }
126
136
  /** @internal */
127
137
  class MongoDBOIDCPluginImpl {
138
+ options;
139
+ logger;
140
+ mapIdpToAuthState = new Map();
141
+ mongoClientOptions;
142
+ timers;
143
+ destroyed = false;
128
144
  constructor(options) {
129
- this.mapIdpToAuthState = new Map();
130
- this.destroyed = false;
131
145
  this.options = options;
132
146
  this.logger = options.logger ?? new events_1.EventEmitter();
133
147
  this.mongoClientOptions = {
@@ -140,6 +154,12 @@ class MongoDBOIDCPluginImpl {
140
154
  this._deserialize(options.serializedState);
141
155
  }
142
156
  }
157
+ /** @internal Public for testing only. */
158
+ static createOIDCAuthStateId() {
159
+ // Use an ID for the OIDC auth state, so that we can distinguish
160
+ // between different auth states in logs.
161
+ return `${Date.now().toString(32)}-${authStateIdCounter++}`;
162
+ }
143
163
  _deserialize(serialized) {
144
164
  try {
145
165
  let original;
@@ -147,13 +167,14 @@ class MongoDBOIDCPluginImpl {
147
167
  original = JSON.parse(Buffer.from(serialized, 'base64').toString('utf8'));
148
168
  }
149
169
  catch (err) {
150
- throw new types_1.MongoDBOIDCError(`Stored OIDC data could not be deserialized: ${err.message}`);
170
+ throw new types_1.MongoDBOIDCError(`Stored OIDC data could not be deserialized: ${err.message}`, { cause: err, codeName: 'DeserializeFormatMismatch' });
151
171
  }
152
- if (original.oidcPluginStateVersion !== 0) {
153
- throw new types_1.MongoDBOIDCError(`Stored OIDC data could not be deserialized because of a version mismatch (got ${JSON.stringify(original.oidcPluginStateVersion)}, expected 0)`);
172
+ if (original.oidcPluginStateVersion !== 1) {
173
+ throw new types_1.MongoDBOIDCError(`Stored OIDC data could not be deserialized because of a version mismatch (got ${JSON.stringify(original.oidcPluginStateVersion)}, expected 1)`, { codeName: 'DeserializeVersionMismatch' });
154
174
  }
155
175
  for (const [key, serializedState] of original.state) {
156
176
  const state = {
177
+ id: serializedState.id ?? MongoDBOIDCPluginImpl.createOIDCAuthStateId(),
157
178
  serverOIDCMetadata: { ...serializedState.serverOIDCMetadata },
158
179
  currentAuthAttempt: null,
159
180
  currentTokenSet: null,
@@ -162,7 +183,9 @@ class MongoDBOIDCPluginImpl {
162
183
  : undefined,
163
184
  discardingTokenSets: serializedState.discardingTokenSets,
164
185
  };
165
- this.updateStateWithTokenSet(state, new openid_client_1.TokenSet(serializedState.currentTokenSet.set));
186
+ if (serializedState.currentTokenSet.set) {
187
+ this.updateStateWithTokenSet(state, util_1.TokenSet.fromSerialized(serializedState.currentTokenSet.set));
188
+ }
166
189
  this.mapIdpToAuthState.set(key, state);
167
190
  }
168
191
  }
@@ -180,16 +203,17 @@ class MongoDBOIDCPluginImpl {
180
203
  // Separate method so we can re-use the inferred return type in _deserialize()
181
204
  _serialize() {
182
205
  return {
183
- oidcPluginStateVersion: 0,
206
+ oidcPluginStateVersion: 1,
184
207
  state: [...this.mapIdpToAuthState]
185
208
  .filter(([, state]) => !!state.currentTokenSet)
186
209
  .map(([key, state]) => {
187
210
  return [
188
211
  key,
189
212
  {
213
+ id: state.id,
190
214
  serverOIDCMetadata: { ...state.serverOIDCMetadata },
191
215
  currentTokenSet: {
192
- set: { ...state.currentTokenSet?.set },
216
+ set: state.currentTokenSet?.set?.serialize(),
193
217
  },
194
218
  lastIdTokenClaims: state.lastIdTokenClaims
195
219
  ? { ...state.lastIdTokenClaims }
@@ -222,11 +246,13 @@ class MongoDBOIDCPluginImpl {
222
246
  // or create a new one if none exists.
223
247
  getAuthState(serverMetadata) {
224
248
  if (!serverMetadata.issuer || typeof serverMetadata.issuer !== 'string') {
225
- throw new types_1.MongoDBOIDCError(`'issuer' is missing`);
249
+ throw new types_1.MongoDBOIDCError(`'issuer' is missing`, {
250
+ codeName: 'MissingIssuer',
251
+ });
226
252
  }
227
253
  (0, util_1.validateSecureHTTPUrl)(serverMetadata.issuer, 'issuer');
228
254
  if (!serverMetadata.clientId) {
229
- throw new types_1.MongoDBOIDCError('No clientId passed in server OIDC metadata object');
255
+ throw new types_1.MongoDBOIDCError('No clientId passed in server OIDC metadata object', { codeName: 'MissingClientId' });
230
256
  }
231
257
  const key = JSON.stringify({
232
258
  // If any part of the server metadata changes, we should probably use
@@ -238,101 +264,109 @@ class MongoDBOIDCPluginImpl {
238
264
  return existing;
239
265
  const newState = {
240
266
  serverOIDCMetadata: serverMetadata,
267
+ id: MongoDBOIDCPluginImpl.createOIDCAuthStateId(),
241
268
  currentAuthAttempt: null,
242
269
  currentTokenSet: null,
243
270
  };
244
271
  this.mapIdpToAuthState.set(key, newState);
245
272
  return newState;
246
273
  }
247
- getSupportedDefaultScopes(issuer) {
274
+ getSupportedDefaultScopes(idpMetadata) {
248
275
  return ['openid', 'offline_access'].filter((scope) => {
249
276
  // Only add `openid` / `offline_access` if the IdP announces support
250
277
  // for those scopes, or if the IdP does not provide a list of scopes
251
278
  // and we cannot tell which it supports.
252
279
  // https://jira.mongodb.org/browser/COMPASS-7437
253
- return (!Array.isArray(issuer.metadata.scopes_supported) ||
254
- issuer.metadata.scopes_supported.includes(scope));
280
+ return (!Array.isArray(idpMetadata.scopes_supported) ||
281
+ idpMetadata.scopes_supported.includes(scope));
255
282
  });
256
283
  }
257
- // prettier does not understand the return type syntax, TS does
258
- getIssuerClass() {
259
- var _a, _b;
260
- const oidcPluginCustomHttpOptionsProvider = (urlObject, options) => {
261
- const url = urlObject.toString();
262
- this.logger.emit('mongodb-oidc-plugin:outbound-http-request', { url });
263
- (0, util_1.validateSecureHTTPUrl)(url, '<generic>');
264
- const { customHttpOptions } = this.options;
265
- if (customHttpOptions && typeof customHttpOptions === 'object')
266
- return customHttpOptions;
267
- else
268
- return customHttpOptions?.(url, options) ?? {};
269
- };
270
- class CustomIssuer extends openid_client_2.Issuer {
271
- constructor(...params) {
272
- super(...params);
273
- this[_a] = oidcPluginCustomHttpOptionsProvider;
274
- this.Client[openid_client_1.custom.http_options] = oidcPluginCustomHttpOptionsProvider;
275
- this.Client.prototype[openid_client_1.custom.http_options] =
276
- oidcPluginCustomHttpOptionsProvider;
277
- }
278
- static async discover(issuer) {
279
- return new this((await super.discover(issuer)).metadata);
284
+ fetch = async (url, init) => {
285
+ this.logger.emit('mongodb-oidc-plugin:outbound-http-request', { url });
286
+ if (this.options.customFetch) {
287
+ return await this.options.customFetch(url, init);
288
+ }
289
+ const options = typeof this.options.customHttpOptions === 'function'
290
+ ? this.options.customHttpOptions(url, {})
291
+ : this.options.customHttpOptions;
292
+ // Same comments as in getDefaultOpenBrowser() apply regarding the import/eval fallbacks here.
293
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
294
+ let fetch;
295
+ try {
296
+ fetch = (await Promise.resolve().then(() => __importStar(require('node-fetch')))).default;
297
+ }
298
+ catch (err) {
299
+ if (err &&
300
+ typeof err === 'object' &&
301
+ 'code' in err &&
302
+ err.code === 'ERR_REQUIRE_ESM' &&
303
+ typeof __webpack_require__ === 'undefined') {
304
+ fetch = (await eval('import("node-fetch")')).default;
280
305
  }
281
- static async webfinger(input) {
282
- return new this((await super.discover(input)).metadata);
306
+ else {
307
+ throw err;
283
308
  }
284
309
  }
285
- _a = openid_client_1.custom.http_options, _b = openid_client_1.custom.http_options;
286
- CustomIssuer[_b] = oidcPluginCustomHttpOptionsProvider;
287
- return CustomIssuer;
288
- }
289
- async getOIDCClient(state, redirectURIs) {
310
+ const AgentClass = new URL(url).protocol === 'https:' ? https_1.Agent : http_1.Agent;
311
+ return (await fetch(url, {
312
+ ...init,
313
+ agent: options?.agent ?? new AgentClass(options),
314
+ ...options,
315
+ headers: { ...options?.headers },
316
+ // TS is not convinced that node-fetch and built-in fetch are compatible enough
317
+ }));
318
+ };
319
+ async getOIDCClientConfiguration(state, redirectURI) {
290
320
  const serverMetadata = state.serverOIDCMetadata;
291
- const makeScope = (issuer) => [
321
+ const makeScope = (idpMetadata) => [
292
322
  ...new Set([
293
- ...this.getSupportedDefaultScopes(issuer),
323
+ ...this.getSupportedDefaultScopes(idpMetadata),
294
324
  ...(serverMetadata.requestScopes ?? []),
295
325
  ]),
296
326
  ].join(' ');
297
- if (state.client) {
327
+ if (state.config) {
328
+ const config = new openid_client_1.Configuration(state.config.serverMetadata(), state.config.clientMetadata().client_id, {
329
+ ...state.config.clientMetadata(),
330
+ redirect_uri: redirectURI,
331
+ });
332
+ if ((0, util_1.validateSecureHTTPUrl)(config.serverMetadata().issuer, 'issuer') ===
333
+ 'http-allowed') {
334
+ (0, openid_client_1.allowInsecureRequests)(config);
335
+ }
298
336
  return {
299
- scope: makeScope(state.client.issuer),
300
- issuer: state.client.issuer,
301
- // need to re-create Client here because redirect_uris might
302
- // differ between calls to this method
303
- client: new state.client.issuer.Client({
304
- ...state.client.metadata,
305
- redirect_uris: redirectURIs,
306
- }),
337
+ scope: makeScope(config.serverMetadata()),
338
+ config,
307
339
  };
308
340
  }
309
- (0, util_1.validateSecureHTTPUrl)(serverMetadata.issuer, 'issuer');
310
- let issuer;
341
+ const httpAllowed = (0, util_1.validateSecureHTTPUrl)(serverMetadata.issuer, 'issuer');
342
+ const discoveryOptions = {
343
+ [openid_client_1.customFetch]: this.fetch,
344
+ execute: httpAllowed === 'http-allowed' ? [openid_client_1.allowInsecureRequests] : [],
345
+ };
346
+ let config;
311
347
  try {
312
- issuer = await this.getIssuerClass().discover(serverMetadata.issuer);
348
+ config = await (0, openid_client_1.discovery)(new URL(serverMetadata.issuer), serverMetadata.clientId, {
349
+ redirect_uri: redirectURI,
350
+ }, (0, openid_client_1.None)(), discoveryOptions);
351
+ // NB: The fact that `customFetch` is transfered from `discoveryOptions` to `config`
352
+ // is tested by our unit tests when they verify that all outgoing HTTP calls are logged.
313
353
  }
314
354
  catch (err) {
315
355
  // openid-client just forwards the raw Node.js HTTP error, we'll want to
316
356
  // at least include the target URL here
317
357
  throw new types_1.MongoDBOIDCError(`Unable to fetch issuer metadata for ${JSON.stringify(serverMetadata.issuer)}: ${(0, util_1.messageFromError)(err)}`, {
318
358
  cause: err,
359
+ codeName: 'IssuerMetadataDiscoveryFailed',
319
360
  });
320
361
  }
321
- (0, util_1.validateSecureHTTPUrl)(issuer.metadata.authorization_endpoint, 'authorization_endpoint');
322
- (0, util_1.validateSecureHTTPUrl)(issuer.metadata.device_authorization_endpoint, 'device_authorization_endpoint');
323
- (0, util_1.validateSecureHTTPUrl)(issuer.metadata.token_endpoint, 'token_endpoint');
324
- (0, util_1.validateSecureHTTPUrl)(issuer.metadata.jwks_uri, 'jwks_uri');
325
- const client = new issuer.Client({
326
- client_id: serverMetadata.clientId,
327
- redirect_uris: redirectURIs,
328
- response_types: ['code'],
329
- token_endpoint_auth_method: 'none',
330
- });
331
- state.client = client;
362
+ (0, util_1.validateSecureHTTPUrl)(config.serverMetadata().authorization_endpoint, 'authorization_endpoint');
363
+ (0, util_1.validateSecureHTTPUrl)(config.serverMetadata().device_authorization_endpoint, 'device_authorization_endpoint');
364
+ (0, util_1.validateSecureHTTPUrl)(config.serverMetadata().token_endpoint, 'token_endpoint');
365
+ (0, util_1.validateSecureHTTPUrl)(config.serverMetadata().jwks_uri, 'jwks_uri');
366
+ state.config = config;
332
367
  return {
333
- scope: makeScope(issuer),
334
- issuer,
335
- client,
368
+ scope: makeScope(config.serverMetadata()),
369
+ config,
336
370
  };
337
371
  }
338
372
  async openBrowser(options) {
@@ -344,14 +378,14 @@ class MongoDBOIDCPluginImpl {
344
378
  // URL has this format.
345
379
  new URL(options.url);
346
380
  if (!/^[a-zA-Z0-9%/:;_.,=@-]+$/.test(options.url)) {
347
- throw new types_1.MongoDBOIDCError(`Unexpected format for internally generated URL: '${options.url}'`);
381
+ throw new types_1.MongoDBOIDCError(`Unexpected format for internally generated URL: '${options.url}'`, { codeName: 'GeneratedUrlInvalidForOpenBrowserCommand' });
348
382
  }
349
383
  this.logger.emit('mongodb-oidc-plugin:open-browser', {
350
384
  customOpener: !!this.options.openBrowser,
351
385
  });
352
386
  if (this.options.openBrowser === false) {
353
387
  // We should never really get to this point
354
- throw new types_1.MongoDBOIDCError('Cannot open browser if `openBrowser` is false');
388
+ throw new types_1.MongoDBOIDCError('Cannot open browser if `openBrowser` is false', { codeName: 'OpenBrowserDisabled' });
355
389
  }
356
390
  if (typeof this.options.openBrowser === 'function') {
357
391
  return await this.options.openBrowser(options);
@@ -370,13 +404,15 @@ class MongoDBOIDCPluginImpl {
370
404
  child.unref();
371
405
  return child;
372
406
  }
373
- throw new types_1.MongoDBOIDCError('Unknown format for `openBrowser`');
407
+ throw new types_1.MongoDBOIDCError('Unknown format for `openBrowser`', {
408
+ codeName: 'OpenBrowserOptionFormatUnknown',
409
+ });
374
410
  }
375
411
  async notifyDeviceFlow(deviceFlowInformation) {
376
412
  (0, util_1.validateSecureHTTPUrl)(deviceFlowInformation.verificationUrl, 'verificationUrl');
377
413
  if (!this.options.notifyDeviceFlow) {
378
414
  // Should never happen.
379
- throw new types_1.MongoDBOIDCError('notifyDeviceFlow() requested but not provided');
415
+ throw new types_1.MongoDBOIDCError('notifyDeviceFlow() requested but not provided', { codeName: 'DeviceFlowNotEnabled' });
380
416
  }
381
417
  this.logger.emit('mongodb-oidc-plugin:notify-device-flow');
382
418
  await this.options.notifyDeviceFlow(deviceFlowInformation);
@@ -388,18 +424,22 @@ class MongoDBOIDCPluginImpl {
388
424
  // for client A, the token expires before it is requested again by client A,
389
425
  // then the plugin is passed to client B which requests a token, and we
390
426
  // receive mismatching tokens for different users or different audiences.
391
- if (!tokenSet.id_token &&
427
+ if (!tokenSet.idToken &&
392
428
  state.lastIdTokenClaims &&
393
429
  !state.lastIdTokenClaims.noIdToken) {
394
- throw new types_1.MongoDBOIDCError(`ID token expected, but not found. Expected claims: ${JSON.stringify(state.lastIdTokenClaims)}`);
430
+ throw new types_1.MongoDBOIDCError(`ID token expected, but not found. Expected claims: ${JSON.stringify(state.lastIdTokenClaims)}`, { codeName: 'IDTokenClaimsMismatchTokenMissing' });
395
431
  }
396
- if (tokenSet.id_token &&
432
+ if (tokenSet.idToken &&
397
433
  state.lastIdTokenClaims &&
398
434
  state.lastIdTokenClaims.noIdToken) {
399
- throw new types_1.MongoDBOIDCError(`Unexpected ID token received.`);
435
+ throw new types_1.MongoDBOIDCError(`Unexpected ID token received.`, {
436
+ codeName: 'IDTokenClaimsMismatchTokenUnexpectedlyPresent',
437
+ });
400
438
  }
401
- if (tokenSet.id_token) {
402
- const idTokenClaims = tokenSet.claims();
439
+ if (tokenSet.idToken) {
440
+ const idTokenClaims = tokenSet.idTokenClaims;
441
+ if (!idTokenClaims)
442
+ throw new types_1.MongoDBOIDCError('Internal error: id_token set but claims() unavailable', { codeName: 'IDTokenClaimsUnavailable' });
403
443
  if (state.lastIdTokenClaims && !state.lastIdTokenClaims.noIdToken) {
404
444
  for (const claim of ['aud', 'sub']) {
405
445
  const normalize = (value) => {
@@ -408,7 +448,7 @@ class MongoDBOIDCPluginImpl {
408
448
  const knownClaim = normalize(state.lastIdTokenClaims[claim]);
409
449
  const newClaim = normalize(idTokenClaims[claim]);
410
450
  if (knownClaim !== newClaim) {
411
- throw new types_1.MongoDBOIDCError(`Unexpected '${claim}' field in id token: Expected ${knownClaim}, saw ${newClaim}`);
451
+ throw new types_1.MongoDBOIDCError(`Unexpected '${claim}' field in id token: Expected ${knownClaim}, saw ${newClaim}`, { codeName: 'IDTokenClaimsMismatchClaimMismatch' });
412
452
  }
413
453
  }
414
454
  }
@@ -421,7 +461,7 @@ class MongoDBOIDCPluginImpl {
421
461
  state.lastIdTokenClaims = { noIdToken: true };
422
462
  this.logger.emit('mongodb-oidc-plugin:missing-id-token');
423
463
  }
424
- const refreshTokenId = (0, util_1.getRefreshTokenId)(tokenSet.refresh_token);
464
+ const refreshTokenId = (0, util_1.getRefreshTokenId)(tokenSet.refreshToken);
425
465
  const updateId = updateIdCounter++;
426
466
  const timerDuration = automaticRefreshTimeoutMS(tokenSet, this.options.passIdTokenAsAccessToken);
427
467
  // Use `.call()` because in browsers, `setTimeout()` requires that it is called
@@ -443,7 +483,7 @@ class MongoDBOIDCPluginImpl {
443
483
  this.logger.emit('mongodb-oidc-plugin:refresh-skipped', {
444
484
  triggeringUpdateId: updateId,
445
485
  expectedRefreshToken: refreshTokenId,
446
- actualRefreshToken: (0, util_1.getRefreshTokenId)(state.currentTokenSet?.set?.refresh_token),
486
+ actualRefreshToken: (0, util_1.getRefreshTokenId)(state.currentTokenSet?.set?.refreshToken),
447
487
  });
448
488
  return false;
449
489
  }
@@ -452,15 +492,17 @@ class MongoDBOIDCPluginImpl {
452
492
  triggeringUpdateId: updateId,
453
493
  refreshToken: refreshTokenId,
454
494
  });
455
- const { client } = await this.getOIDCClient(state);
456
- const refreshedTokens = await client.refresh(tokenSet);
495
+ if (!tokenSet.refreshToken)
496
+ return false;
497
+ const { config } = await this.getOIDCClientConfiguration(state);
498
+ const refreshedTokens = await (0, openid_client_1.refreshTokenGrant)(config, tokenSet.refreshToken);
457
499
  // Check again to avoid race conditions.
458
500
  if (state.currentTokenSet?.set === tokenSet) {
459
501
  this.logger.emit('mongodb-oidc-plugin:refresh-succeeded', {
460
502
  triggeringUpdateId: updateId,
461
503
  refreshToken: refreshTokenId,
462
504
  });
463
- this.updateStateWithTokenSet(state, refreshedTokens);
505
+ this.updateStateWithTokenSet(state, new util_1.TokenSet(refreshedTokens));
464
506
  return true;
465
507
  }
466
508
  }
@@ -480,14 +522,15 @@ class MongoDBOIDCPluginImpl {
480
522
  this.logger.emit('mongodb-oidc-plugin:state-updated', {
481
523
  updateId,
482
524
  timerDuration,
483
- tokenSetId: (0, util_1.getStableTokenSetId)(tokenSet),
525
+ tokenSetId: tokenSet.stableId(),
484
526
  });
485
527
  }
528
+ static defaultRedirectURI = 'http://localhost:27097/redirect';
486
529
  async authorizationCodeFlow(state, signal) {
487
530
  const configuredRedirectURI = this.options.redirectURI ?? MongoDBOIDCPluginImpl.defaultRedirectURI;
488
- const codeVerifier = openid_client_2.generators.codeVerifier();
489
- const codeChallenge = openid_client_2.generators.codeChallenge(codeVerifier);
490
- const oidcStateParam = (await (0, util_2.promisify)(crypto_1.randomBytes)(16)).toString('hex');
531
+ const codeVerifier = (0, openid_client_1.randomPKCECodeVerifier)();
532
+ const codeChallenge = await (0, openid_client_1.calculatePKCECodeChallenge)(codeVerifier);
533
+ const oidcStateParam = (0, openid_client_1.randomState)();
491
534
  const server = new rfc_8252_http_server_1.RFC8252HTTPServer({
492
535
  redirectUrl: configuredRedirectURI,
493
536
  logger: this.logger,
@@ -496,11 +539,11 @@ class MongoDBOIDCPluginImpl {
496
539
  });
497
540
  let paramsUrl = '';
498
541
  let scope;
499
- let client;
542
+ let config;
500
543
  let actualRedirectURI;
501
544
  const nonce = this.options.skipNonceInAuthCodeRequest
502
545
  ? undefined
503
- : openid_client_2.generators.nonce();
546
+ : (0, openid_client_1.randomNonce)();
504
547
  try {
505
548
  await (0, util_1.withAbortCheck)(signal, async ({ signalCheck, signalPromise }) => {
506
549
  // We mark the operations that we want to allow to result in a fallback
@@ -514,18 +557,17 @@ class MongoDBOIDCPluginImpl {
514
557
  signalPromise,
515
558
  ]);
516
559
  actualRedirectURI = server.listeningRedirectUrl;
517
- ({ scope, client } = await this.getOIDCClient(state, [
518
- actualRedirectURI,
519
- ]));
520
- const authCodeFlowUrl = client.authorizationUrl({
560
+ ({ scope, config } = await this.getOIDCClientConfiguration(state, actualRedirectURI));
561
+ const authCodeFlowUrl = (0, openid_client_1.buildAuthorizationUrl)(config, {
521
562
  scope,
522
563
  code_challenge: codeChallenge,
523
564
  code_challenge_method: 'S256',
524
565
  state: oidcStateParam,
525
- nonce,
566
+ ...(nonce ? { nonce } : {}),
567
+ redirect_uri: actualRedirectURI,
526
568
  });
527
569
  (0, util_1.validateSecureHTTPUrl)(authCodeFlowUrl, 'authCodeFlowUrl');
528
- const { localUrl, onAccessed: onLocalUrlAccessed } = await server.addRedirect(authCodeFlowUrl);
570
+ const { localUrl, onAccessed: onLocalUrlAccessed } = await server.addRedirect(authCodeFlowUrl.toString());
529
571
  signalCheck();
530
572
  // Handle errors from opening a browser but do not await the Promise
531
573
  // in case it only resolves when the browser exits (which is the case
@@ -537,10 +579,10 @@ class MongoDBOIDCPluginImpl {
537
579
  const extraErrorInfo = () => browserHandle?.spawnargs
538
580
  ? ` (${JSON.stringify(browserHandle.spawnargs)})`
539
581
  : '';
540
- browserHandle?.once('error', (err) => reject(new types_1.MongoDBOIDCError(`Opening browser failed with '${(0, util_1.messageFromError)(err)}'${extraErrorInfo()}`, { cause: err })));
582
+ browserHandle?.once('error', (err) => reject(new types_1.MongoDBOIDCError(`Opening browser failed with '${(0, util_1.messageFromError)(err)}'${extraErrorInfo()}`, { cause: err, codeName: 'BrowserOpenFailedSpawnError' })));
541
583
  browserHandle?.once('exit', (code) => {
542
584
  if (code !== 0)
543
- reject(new types_1.MongoDBOIDCError(`Opening browser failed with exit code ${code}${extraErrorInfo()}`));
585
+ reject(new types_1.MongoDBOIDCError(`Opening browser failed with exit code ${code}${extraErrorInfo()}`, { codeName: 'BrowserOpenFailedNonZeroExit' }));
544
586
  });
545
587
  })
546
588
  .catch(reject);
@@ -548,7 +590,9 @@ class MongoDBOIDCPluginImpl {
548
590
  const timeout = allowFallbackIfFailed(new Promise((resolve, reject) => {
549
591
  if (this.options.openBrowserTimeout !== 0) {
550
592
  this.timers.setTimeout
551
- .call(null, () => reject(new types_1.MongoDBOIDCError('Opening browser timed out')), this.options.openBrowserTimeout ?? api_1.kDefaultOpenBrowserTimeout)
593
+ .call(null, () => reject(new types_1.MongoDBOIDCError('Opening browser timed out', {
594
+ codeName: 'BrowserOpenTimeout',
595
+ })), this.options.openBrowserTimeout ?? api_1.kDefaultOpenBrowserTimeout)
552
596
  ?.unref?.();
553
597
  }
554
598
  }));
@@ -570,20 +614,18 @@ class MongoDBOIDCPluginImpl {
570
614
  finally {
571
615
  await server.close();
572
616
  }
573
- const params = client.callbackParams(paramsUrl);
574
- const tokenSet = await client.callback(actualRedirectURI, params, {
575
- code_verifier: codeVerifier,
576
- state: oidcStateParam,
577
- nonce,
617
+ const tokenSet = await (0, openid_client_1.authorizationCodeGrant)(config, new URL(paramsUrl), {
618
+ pkceCodeVerifier: codeVerifier,
619
+ expectedState: oidcStateParam,
620
+ expectedNonce: nonce,
578
621
  });
579
- this.updateStateWithTokenSet(state, tokenSet);
622
+ this.updateStateWithTokenSet(state, new util_1.TokenSet(tokenSet));
580
623
  }
581
624
  async deviceAuthorizationFlow(state, signal) {
582
- const { scope, client } = await this.getOIDCClient(state);
625
+ const { scope, config } = await this.getOIDCClientConfiguration(state);
583
626
  await (0, util_1.withAbortCheck)(signal, async ({ signalCheck, signalPromise }) => {
584
627
  const deviceFlowHandle = await Promise.race([
585
- client.deviceAuthorization({
586
- client_id: client.metadata.client_id,
628
+ (0, openid_client_1.initiateDeviceAuthorization)(config, {
587
629
  scope,
588
630
  }),
589
631
  signalPromise,
@@ -593,8 +635,8 @@ class MongoDBOIDCPluginImpl {
593
635
  userCode: deviceFlowHandle.user_code,
594
636
  verificationUrl: deviceFlowHandle.verification_uri,
595
637
  });
596
- const tokenSet = await deviceFlowHandle.poll({ signal });
597
- this.updateStateWithTokenSet(state, tokenSet);
638
+ const tokenSet = await (0, openid_client_1.pollDeviceAuthorizationGrant)(config, deviceFlowHandle, {}, { signal });
639
+ this.updateStateWithTokenSet(state, new util_1.TokenSet(tokenSet));
598
640
  });
599
641
  }
600
642
  async initiateAuthAttempt(state, driverAbortSignal, { forceRefreshOrReauth = false } = {}) {
@@ -619,12 +661,14 @@ class MongoDBOIDCPluginImpl {
619
661
  tokenExpiryInSeconds(state.currentTokenSet?.set, passIdTokenAsAccessToken) >
620
662
  5 * 60) {
621
663
  this.logger.emit('mongodb-oidc-plugin:skip-auth-attempt', {
664
+ authStateId: state.id,
622
665
  reason: 'not-expired',
623
666
  });
624
667
  break get_tokens;
625
668
  }
626
669
  if (await state.currentTokenSet?.tryRefresh?.()) {
627
670
  this.logger.emit('mongodb-oidc-plugin:skip-auth-attempt', {
671
+ authStateId: state.id,
628
672
  reason: 'refresh-succeeded',
629
673
  });
630
674
  break get_tokens;
@@ -635,15 +679,19 @@ class MongoDBOIDCPluginImpl {
635
679
  if (currentAllowedFlowSet.includes('auth-code')) {
636
680
  try {
637
681
  this.logger.emit('mongodb-oidc-plugin:auth-attempt-started', {
682
+ authStateId: state.id,
638
683
  flow: 'auth-code',
639
684
  });
640
685
  await this.authorizationCodeFlow(state, signal);
641
- this.logger.emit('mongodb-oidc-plugin:auth-attempt-succeeded');
686
+ this.logger.emit('mongodb-oidc-plugin:auth-attempt-succeeded', {
687
+ authStateId: state.id,
688
+ });
642
689
  break get_tokens;
643
690
  }
644
691
  catch (err) {
645
692
  error = err;
646
693
  this.logger.emit('mongodb-oidc-plugin:auth-attempt-failed', {
694
+ authStateId: state.id,
647
695
  error: (0, util_1.errorString)(err),
648
696
  });
649
697
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -654,14 +702,18 @@ class MongoDBOIDCPluginImpl {
654
702
  if (currentAllowedFlowSet.includes('device-auth')) {
655
703
  try {
656
704
  this.logger.emit('mongodb-oidc-plugin:auth-attempt-started', {
705
+ authStateId: state.id,
657
706
  flow: 'device-auth',
658
707
  });
659
708
  await this.deviceAuthorizationFlow(state, signal);
660
- this.logger.emit('mongodb-oidc-plugin:auth-attempt-succeeded');
709
+ this.logger.emit('mongodb-oidc-plugin:auth-attempt-succeeded', {
710
+ authStateId: state.id,
711
+ });
661
712
  break get_tokens;
662
713
  }
663
714
  catch (err) {
664
715
  this.logger.emit('mongodb-oidc-plugin:auth-attempt-failed', {
716
+ authStateId: state.id,
665
717
  error: (0, util_1.errorString)(err),
666
718
  });
667
719
  throw err;
@@ -670,15 +722,20 @@ class MongoDBOIDCPluginImpl {
670
722
  if (error)
671
723
  throw error;
672
724
  }
673
- if (passIdTokenAsAccessToken && !state.currentTokenSet?.set?.id_token) {
674
- throw new types_1.MongoDBOIDCError('Could not retrieve valid ID token');
725
+ if (passIdTokenAsAccessToken && !state.currentTokenSet?.set?.idToken) {
726
+ throw new types_1.MongoDBOIDCError('Could not retrieve valid ID token', {
727
+ codeName: 'IDTokenMissingFromTokenSet',
728
+ });
675
729
  }
676
- else if (!state.currentTokenSet?.set?.access_token) {
677
- throw new types_1.MongoDBOIDCError('Could not retrieve valid access token');
730
+ else if (!state.currentTokenSet?.set?.accessToken) {
731
+ throw new types_1.MongoDBOIDCError('Could not retrieve valid access token', {
732
+ codeName: 'AccessTokenMissingFromTokenSet',
733
+ });
678
734
  }
679
735
  }
680
736
  catch (err) {
681
737
  this.logger.emit('mongodb-oidc-plugin:auth-failed', {
738
+ authStateId: state.id,
682
739
  error: (0, util_1.errorString)(err),
683
740
  });
684
741
  throw err;
@@ -687,21 +744,23 @@ class MongoDBOIDCPluginImpl {
687
744
  this.options.signal?.removeEventListener('abort', optionsAbortCb);
688
745
  driverAbortSignal?.removeEventListener('abort', driverAbortCb);
689
746
  }
690
- const { token_type, expires_at, access_token, id_token, refresh_token } = state.currentTokenSet.set;
691
- const tokenSetId = (0, util_1.getStableTokenSetId)(state.currentTokenSet.set);
747
+ const { tokenType, accessToken, idToken, refreshToken } = state.currentTokenSet.set;
748
+ const expiresAt = state.currentTokenSet.set.expiresAt;
749
+ const tokenSetId = state.currentTokenSet.set.stableId();
692
750
  // We would not want to return the access token or ID token of a token set whose
693
751
  // accompanying refresh token was passed to us by the driver
694
752
  const willRetryWithForceRefreshOrReauth = !forceRefreshOrReauth &&
695
753
  !!state.discardingTokenSets?.includes(tokenSetId);
696
754
  this.logger.emit('mongodb-oidc-plugin:auth-succeeded', {
697
- tokenType: token_type ?? null,
698
- refreshToken: (0, util_1.getRefreshTokenId)(refresh_token),
699
- expiresAt: expires_at ? new Date(expires_at * 1000).toISOString() : null,
755
+ authStateId: state.id,
756
+ tokenType: tokenType ?? null, // DPoP or Bearer
757
+ refreshToken: (0, util_1.getRefreshTokenId)(refreshToken),
758
+ expiresAt: expiresAt ? new Date(expiresAt * 1000).toISOString() : null,
700
759
  passIdTokenAsAccessToken: !!passIdTokenAsAccessToken,
701
760
  tokens: {
702
- accessToken: access_token,
703
- idToken: id_token,
704
- refreshToken: refresh_token,
761
+ accessToken: accessToken,
762
+ idToken: idToken,
763
+ refreshToken: refreshToken,
705
764
  },
706
765
  tokenSetId,
707
766
  forceRefreshOrReauth,
@@ -713,7 +772,7 @@ class MongoDBOIDCPluginImpl {
713
772
  });
714
773
  }
715
774
  return {
716
- accessToken: passIdTokenAsAccessToken ? id_token || '' : access_token,
775
+ accessToken: passIdTokenAsAccessToken ? idToken || '' : accessToken,
717
776
  refreshToken: tokenSetId,
718
777
  // Passing `expiresInSeconds: 0` results in the driver not caching the token.
719
778
  // We perform our own caching here inside the plugin, so interactions with the
@@ -731,22 +790,33 @@ class MongoDBOIDCPluginImpl {
731
790
  if (params.version !== 1) {
732
791
  throw new types_1.MongoDBOIDCError(
733
792
  // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
734
- `OIDC MongoDB driver protocol mismatch: unknown version ${params.version}`);
793
+ `OIDC MongoDB driver protocol mismatch: unknown version ${params.version}`, { codeName: 'ProtocolVersionMismatch' });
735
794
  }
736
795
  if (this.destroyed) {
737
- throw new types_1.MongoDBOIDCError('This OIDC plugin instance has been destroyed and is no longer active');
796
+ throw new types_1.MongoDBOIDCError('This OIDC plugin instance has been destroyed and is no longer active', { codeName: 'PluginInstanceDestroyed' });
738
797
  }
739
798
  if (!params.idpInfo) {
740
- throw new types_1.MongoDBOIDCError('No IdP information provided');
799
+ throw new types_1.MongoDBOIDCError('No IdP information provided', {
800
+ codeName: 'IdPInfoMissing',
801
+ });
741
802
  }
742
803
  const state = this.getAuthState({
743
804
  ...params.idpInfo,
744
805
  username: params.username,
745
806
  });
807
+ this.logger.emit('mongodb-oidc-plugin:request-token-started', {
808
+ authStateId: state.id,
809
+ isCurrentAuthAttemptSet: !!state.currentAuthAttempt,
810
+ tokenSetId: params.refreshToken,
811
+ username: state.serverOIDCMetadata.username,
812
+ issuer: state.serverOIDCMetadata.issuer,
813
+ clientId: state.serverOIDCMetadata.clientId,
814
+ requestScopes: state.serverOIDCMetadata.requestScopes,
815
+ });
746
816
  // If the driver called us with a refresh token, that means that its corresponding
747
817
  // access token has become invalid and we should always return a new one.
748
818
  if (params.refreshToken) {
749
- (state.discardingTokenSets ?? (state.discardingTokenSets = [])).push(params.refreshToken);
819
+ (state.discardingTokenSets ??= []).push(params.refreshToken);
750
820
  this.logger.emit('mongodb-oidc-plugin:discarding-token-set', {
751
821
  tokenSetId: params.refreshToken,
752
822
  });
@@ -766,6 +836,15 @@ class MongoDBOIDCPluginImpl {
766
836
  }
767
837
  }
768
838
  finally {
839
+ this.logger.emit('mongodb-oidc-plugin:request-token-ended', {
840
+ authStateId: state.id,
841
+ isCurrentAuthAttemptSet: !!state.currentAuthAttempt,
842
+ tokenSetId: params.refreshToken,
843
+ username: state.serverOIDCMetadata.username,
844
+ issuer: state.serverOIDCMetadata.issuer,
845
+ clientId: state.serverOIDCMetadata.clientId,
846
+ requestScopes: state.serverOIDCMetadata.requestScopes,
847
+ });
769
848
  if (params.refreshToken) {
770
849
  const index = state.discardingTokenSets?.indexOf(params.refreshToken) ?? -1;
771
850
  if (index > 0) {
@@ -787,5 +866,4 @@ class MongoDBOIDCPluginImpl {
787
866
  }
788
867
  }
789
868
  exports.MongoDBOIDCPluginImpl = MongoDBOIDCPluginImpl;
790
- MongoDBOIDCPluginImpl.defaultRedirectURI = 'http://localhost:27097/redirect';
791
869
  //# sourceMappingURL=plugin.js.map