@mongodb-js/oidc-plugin 1.1.8 → 2.0.1
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/api.d.ts +12 -4
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +3 -3
- package/dist/api.js.map +1 -1
- package/dist/log-hook.d.ts.map +1 -1
- package/dist/log-hook.js +38 -33
- package/dist/log-hook.js.map +1 -1
- package/dist/plugin.d.ts +7 -7
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +206 -150
- package/dist/plugin.js.map +1 -1
- package/dist/rfc-8252-http-server.d.ts.map +1 -1
- package/dist/rfc-8252-http-server.js +165 -150
- package/dist/rfc-8252-http-server.js.map +1 -1
- package/dist/types.d.ts +12 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -6
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts +41 -5
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +128 -30
- package/dist/util.js.map +1 -1
- package/index.d.ts +24 -4
- package/package.json +18 -16
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 (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 =
|
|
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
|
|
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.
|
|
101
|
+
const expiresAt = (tokenSet.idTokenClaims?.exp !== undefined &&
|
|
92
102
|
passIdTokenAsAccessToken &&
|
|
93
|
-
tokenSet.
|
|
94
|
-
tokenSet.
|
|
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.
|
|
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,7 +120,6 @@ 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;
|
|
116
125
|
let authStateIdCounter = 0;
|
|
@@ -126,9 +135,13 @@ function allowFallbackIfFailed(promise) {
|
|
|
126
135
|
}
|
|
127
136
|
/** @internal */
|
|
128
137
|
class MongoDBOIDCPluginImpl {
|
|
138
|
+
options;
|
|
139
|
+
logger;
|
|
140
|
+
mapIdpToAuthState = new Map();
|
|
141
|
+
mongoClientOptions;
|
|
142
|
+
timers;
|
|
143
|
+
destroyed = false;
|
|
129
144
|
constructor(options) {
|
|
130
|
-
this.mapIdpToAuthState = new Map();
|
|
131
|
-
this.destroyed = false;
|
|
132
145
|
this.options = options;
|
|
133
146
|
this.logger = options.logger ?? new events_1.EventEmitter();
|
|
134
147
|
this.mongoClientOptions = {
|
|
@@ -154,10 +167,10 @@ class MongoDBOIDCPluginImpl {
|
|
|
154
167
|
original = JSON.parse(Buffer.from(serialized, 'base64').toString('utf8'));
|
|
155
168
|
}
|
|
156
169
|
catch (err) {
|
|
157
|
-
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' });
|
|
158
171
|
}
|
|
159
|
-
if (original.oidcPluginStateVersion !==
|
|
160
|
-
throw new types_1.MongoDBOIDCError(`Stored OIDC data could not be deserialized because of a version mismatch (got ${JSON.stringify(original.oidcPluginStateVersion)}, expected
|
|
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' });
|
|
161
174
|
}
|
|
162
175
|
for (const [key, serializedState] of original.state) {
|
|
163
176
|
const state = {
|
|
@@ -170,7 +183,9 @@ class MongoDBOIDCPluginImpl {
|
|
|
170
183
|
: undefined,
|
|
171
184
|
discardingTokenSets: serializedState.discardingTokenSets,
|
|
172
185
|
};
|
|
173
|
-
|
|
186
|
+
if (serializedState.currentTokenSet.set) {
|
|
187
|
+
this.updateStateWithTokenSet(state, util_1.TokenSet.fromSerialized(serializedState.currentTokenSet.set));
|
|
188
|
+
}
|
|
174
189
|
this.mapIdpToAuthState.set(key, state);
|
|
175
190
|
}
|
|
176
191
|
}
|
|
@@ -188,7 +203,7 @@ class MongoDBOIDCPluginImpl {
|
|
|
188
203
|
// Separate method so we can re-use the inferred return type in _deserialize()
|
|
189
204
|
_serialize() {
|
|
190
205
|
return {
|
|
191
|
-
oidcPluginStateVersion:
|
|
206
|
+
oidcPluginStateVersion: 1,
|
|
192
207
|
state: [...this.mapIdpToAuthState]
|
|
193
208
|
.filter(([, state]) => !!state.currentTokenSet)
|
|
194
209
|
.map(([key, state]) => {
|
|
@@ -198,7 +213,7 @@ class MongoDBOIDCPluginImpl {
|
|
|
198
213
|
id: state.id,
|
|
199
214
|
serverOIDCMetadata: { ...state.serverOIDCMetadata },
|
|
200
215
|
currentTokenSet: {
|
|
201
|
-
set:
|
|
216
|
+
set: state.currentTokenSet?.set?.serialize(),
|
|
202
217
|
},
|
|
203
218
|
lastIdTokenClaims: state.lastIdTokenClaims
|
|
204
219
|
? { ...state.lastIdTokenClaims }
|
|
@@ -231,11 +246,13 @@ class MongoDBOIDCPluginImpl {
|
|
|
231
246
|
// or create a new one if none exists.
|
|
232
247
|
getAuthState(serverMetadata) {
|
|
233
248
|
if (!serverMetadata.issuer || typeof serverMetadata.issuer !== 'string') {
|
|
234
|
-
throw new types_1.MongoDBOIDCError(`'issuer' is missing
|
|
249
|
+
throw new types_1.MongoDBOIDCError(`'issuer' is missing`, {
|
|
250
|
+
codeName: 'MissingIssuer',
|
|
251
|
+
});
|
|
235
252
|
}
|
|
236
253
|
(0, util_1.validateSecureHTTPUrl)(serverMetadata.issuer, 'issuer');
|
|
237
254
|
if (!serverMetadata.clientId) {
|
|
238
|
-
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' });
|
|
239
256
|
}
|
|
240
257
|
const key = JSON.stringify({
|
|
241
258
|
// If any part of the server metadata changes, we should probably use
|
|
@@ -254,95 +271,120 @@ class MongoDBOIDCPluginImpl {
|
|
|
254
271
|
this.mapIdpToAuthState.set(key, newState);
|
|
255
272
|
return newState;
|
|
256
273
|
}
|
|
257
|
-
getSupportedDefaultScopes(
|
|
274
|
+
getSupportedDefaultScopes(idpMetadata) {
|
|
258
275
|
return ['openid', 'offline_access'].filter((scope) => {
|
|
259
276
|
// Only add `openid` / `offline_access` if the IdP announces support
|
|
260
277
|
// for those scopes, or if the IdP does not provide a list of scopes
|
|
261
278
|
// and we cannot tell which it supports.
|
|
262
279
|
// https://jira.mongodb.org/browser/COMPASS-7437
|
|
263
|
-
return (!Array.isArray(
|
|
264
|
-
|
|
280
|
+
return (!Array.isArray(idpMetadata.scopes_supported) ||
|
|
281
|
+
idpMetadata.scopes_supported.includes(scope));
|
|
265
282
|
});
|
|
266
283
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
284
|
+
fetch = async (url, init) => {
|
|
285
|
+
this.logger.emit('mongodb-oidc-plugin:outbound-http-request', { url });
|
|
286
|
+
try {
|
|
287
|
+
const response = await this.doFetch(url, init);
|
|
288
|
+
this.logger.emit('mongodb-oidc-plugin:outbound-http-request-completed', {
|
|
289
|
+
url,
|
|
290
|
+
status: response.status,
|
|
291
|
+
statusText: response.statusText,
|
|
292
|
+
});
|
|
293
|
+
return response;
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
this.logger.emit('mongodb-oidc-plugin:outbound-http-request-failed', {
|
|
297
|
+
url,
|
|
298
|
+
error: (0, util_1.errorString)(err),
|
|
299
|
+
});
|
|
300
|
+
throw err;
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
doFetch = async (url, init) => {
|
|
304
|
+
if (this.options.customFetch) {
|
|
305
|
+
return await this.options.customFetch(url, init);
|
|
306
|
+
}
|
|
307
|
+
const options = typeof this.options.customHttpOptions === 'function'
|
|
308
|
+
? this.options.customHttpOptions(url, {})
|
|
309
|
+
: this.options.customHttpOptions;
|
|
310
|
+
// Same comments as in getDefaultOpenBrowser() apply regarding the import/eval fallbacks here.
|
|
311
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
312
|
+
let fetch;
|
|
313
|
+
try {
|
|
314
|
+
fetch = (await Promise.resolve().then(() => __importStar(require('node-fetch')))).default;
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
if (err &&
|
|
318
|
+
typeof err === 'object' &&
|
|
319
|
+
'code' in err &&
|
|
320
|
+
err.code === 'ERR_REQUIRE_ESM' &&
|
|
321
|
+
typeof __webpack_require__ === 'undefined') {
|
|
322
|
+
fetch = (await eval('import("node-fetch")')).default;
|
|
290
323
|
}
|
|
291
|
-
|
|
292
|
-
|
|
324
|
+
else {
|
|
325
|
+
throw err;
|
|
293
326
|
}
|
|
294
327
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
328
|
+
const AgentClass = new URL(url).protocol === 'https:' ? https_1.Agent : http_1.Agent;
|
|
329
|
+
return (await fetch(url, {
|
|
330
|
+
...init,
|
|
331
|
+
agent: options?.agent ?? new AgentClass(options),
|
|
332
|
+
...options,
|
|
333
|
+
headers: { ...options?.headers },
|
|
334
|
+
// TS is not convinced that node-fetch and built-in fetch are compatible enough
|
|
335
|
+
}));
|
|
336
|
+
};
|
|
337
|
+
async getOIDCClientConfiguration(state, redirectURI) {
|
|
300
338
|
const serverMetadata = state.serverOIDCMetadata;
|
|
301
|
-
const makeScope = (
|
|
339
|
+
const makeScope = (idpMetadata) => [
|
|
302
340
|
...new Set([
|
|
303
|
-
...this.getSupportedDefaultScopes(
|
|
341
|
+
...this.getSupportedDefaultScopes(idpMetadata),
|
|
304
342
|
...(serverMetadata.requestScopes ?? []),
|
|
305
343
|
]),
|
|
306
344
|
].join(' ');
|
|
307
|
-
if (state.
|
|
345
|
+
if (state.config) {
|
|
346
|
+
const config = new openid_client_1.Configuration(state.config.serverMetadata(), state.config.clientMetadata().client_id, {
|
|
347
|
+
...state.config.clientMetadata(),
|
|
348
|
+
redirect_uri: redirectURI,
|
|
349
|
+
});
|
|
350
|
+
if ((0, util_1.validateSecureHTTPUrl)(config.serverMetadata().issuer, 'issuer') ===
|
|
351
|
+
'http-allowed') {
|
|
352
|
+
(0, openid_client_1.allowInsecureRequests)(config);
|
|
353
|
+
}
|
|
308
354
|
return {
|
|
309
|
-
scope: makeScope(
|
|
310
|
-
|
|
311
|
-
// need to re-create Client here because redirect_uris might
|
|
312
|
-
// differ between calls to this method
|
|
313
|
-
client: new state.client.issuer.Client({
|
|
314
|
-
...state.client.metadata,
|
|
315
|
-
redirect_uris: redirectURIs,
|
|
316
|
-
}),
|
|
355
|
+
scope: makeScope(config.serverMetadata()),
|
|
356
|
+
config,
|
|
317
357
|
};
|
|
318
358
|
}
|
|
319
|
-
(0, util_1.validateSecureHTTPUrl)(serverMetadata.issuer, 'issuer');
|
|
320
|
-
|
|
359
|
+
const httpAllowed = (0, util_1.validateSecureHTTPUrl)(serverMetadata.issuer, 'issuer');
|
|
360
|
+
const discoveryOptions = {
|
|
361
|
+
[openid_client_1.customFetch]: this.fetch,
|
|
362
|
+
execute: httpAllowed === 'http-allowed' ? [openid_client_1.allowInsecureRequests] : [],
|
|
363
|
+
};
|
|
364
|
+
let config;
|
|
321
365
|
try {
|
|
322
|
-
|
|
366
|
+
config = await (0, openid_client_1.discovery)(new URL(serverMetadata.issuer), serverMetadata.clientId, {
|
|
367
|
+
redirect_uri: redirectURI,
|
|
368
|
+
}, (0, openid_client_1.None)(), discoveryOptions);
|
|
369
|
+
// NB: The fact that `customFetch` is transfered from `discoveryOptions` to `config`
|
|
370
|
+
// is tested by our unit tests when they verify that all outgoing HTTP calls are logged.
|
|
323
371
|
}
|
|
324
372
|
catch (err) {
|
|
325
373
|
// openid-client just forwards the raw Node.js HTTP error, we'll want to
|
|
326
374
|
// at least include the target URL here
|
|
327
375
|
throw new types_1.MongoDBOIDCError(`Unable to fetch issuer metadata for ${JSON.stringify(serverMetadata.issuer)}: ${(0, util_1.messageFromError)(err)}`, {
|
|
328
376
|
cause: err,
|
|
377
|
+
codeName: 'IssuerMetadataDiscoveryFailed',
|
|
329
378
|
});
|
|
330
379
|
}
|
|
331
|
-
(0, util_1.validateSecureHTTPUrl)(
|
|
332
|
-
(0, util_1.validateSecureHTTPUrl)(
|
|
333
|
-
(0, util_1.validateSecureHTTPUrl)(
|
|
334
|
-
(0, util_1.validateSecureHTTPUrl)(
|
|
335
|
-
|
|
336
|
-
client_id: serverMetadata.clientId,
|
|
337
|
-
redirect_uris: redirectURIs,
|
|
338
|
-
response_types: ['code'],
|
|
339
|
-
token_endpoint_auth_method: 'none',
|
|
340
|
-
});
|
|
341
|
-
state.client = client;
|
|
380
|
+
(0, util_1.validateSecureHTTPUrl)(config.serverMetadata().authorization_endpoint, 'authorization_endpoint');
|
|
381
|
+
(0, util_1.validateSecureHTTPUrl)(config.serverMetadata().device_authorization_endpoint, 'device_authorization_endpoint');
|
|
382
|
+
(0, util_1.validateSecureHTTPUrl)(config.serverMetadata().token_endpoint, 'token_endpoint');
|
|
383
|
+
(0, util_1.validateSecureHTTPUrl)(config.serverMetadata().jwks_uri, 'jwks_uri');
|
|
384
|
+
state.config = config;
|
|
342
385
|
return {
|
|
343
|
-
scope: makeScope(
|
|
344
|
-
|
|
345
|
-
client,
|
|
386
|
+
scope: makeScope(config.serverMetadata()),
|
|
387
|
+
config,
|
|
346
388
|
};
|
|
347
389
|
}
|
|
348
390
|
async openBrowser(options) {
|
|
@@ -354,14 +396,14 @@ class MongoDBOIDCPluginImpl {
|
|
|
354
396
|
// URL has this format.
|
|
355
397
|
new URL(options.url);
|
|
356
398
|
if (!/^[a-zA-Z0-9%/:;_.,=@-]+$/.test(options.url)) {
|
|
357
|
-
throw new types_1.MongoDBOIDCError(`Unexpected format for internally generated URL: '${options.url}'
|
|
399
|
+
throw new types_1.MongoDBOIDCError(`Unexpected format for internally generated URL: '${options.url}'`, { codeName: 'GeneratedUrlInvalidForOpenBrowserCommand' });
|
|
358
400
|
}
|
|
359
401
|
this.logger.emit('mongodb-oidc-plugin:open-browser', {
|
|
360
402
|
customOpener: !!this.options.openBrowser,
|
|
361
403
|
});
|
|
362
404
|
if (this.options.openBrowser === false) {
|
|
363
405
|
// We should never really get to this point
|
|
364
|
-
throw new types_1.MongoDBOIDCError('Cannot open browser if `openBrowser` is false');
|
|
406
|
+
throw new types_1.MongoDBOIDCError('Cannot open browser if `openBrowser` is false', { codeName: 'OpenBrowserDisabled' });
|
|
365
407
|
}
|
|
366
408
|
if (typeof this.options.openBrowser === 'function') {
|
|
367
409
|
return await this.options.openBrowser(options);
|
|
@@ -380,13 +422,15 @@ class MongoDBOIDCPluginImpl {
|
|
|
380
422
|
child.unref();
|
|
381
423
|
return child;
|
|
382
424
|
}
|
|
383
|
-
throw new types_1.MongoDBOIDCError('Unknown format for `openBrowser`'
|
|
425
|
+
throw new types_1.MongoDBOIDCError('Unknown format for `openBrowser`', {
|
|
426
|
+
codeName: 'OpenBrowserOptionFormatUnknown',
|
|
427
|
+
});
|
|
384
428
|
}
|
|
385
429
|
async notifyDeviceFlow(deviceFlowInformation) {
|
|
386
430
|
(0, util_1.validateSecureHTTPUrl)(deviceFlowInformation.verificationUrl, 'verificationUrl');
|
|
387
431
|
if (!this.options.notifyDeviceFlow) {
|
|
388
432
|
// Should never happen.
|
|
389
|
-
throw new types_1.MongoDBOIDCError('notifyDeviceFlow() requested but not provided');
|
|
433
|
+
throw new types_1.MongoDBOIDCError('notifyDeviceFlow() requested but not provided', { codeName: 'DeviceFlowNotEnabled' });
|
|
390
434
|
}
|
|
391
435
|
this.logger.emit('mongodb-oidc-plugin:notify-device-flow');
|
|
392
436
|
await this.options.notifyDeviceFlow(deviceFlowInformation);
|
|
@@ -398,18 +442,22 @@ class MongoDBOIDCPluginImpl {
|
|
|
398
442
|
// for client A, the token expires before it is requested again by client A,
|
|
399
443
|
// then the plugin is passed to client B which requests a token, and we
|
|
400
444
|
// receive mismatching tokens for different users or different audiences.
|
|
401
|
-
if (!tokenSet.
|
|
445
|
+
if (!tokenSet.idToken &&
|
|
402
446
|
state.lastIdTokenClaims &&
|
|
403
447
|
!state.lastIdTokenClaims.noIdToken) {
|
|
404
|
-
throw new types_1.MongoDBOIDCError(`ID token expected, but not found. Expected claims: ${JSON.stringify(state.lastIdTokenClaims)}
|
|
448
|
+
throw new types_1.MongoDBOIDCError(`ID token expected, but not found. Expected claims: ${JSON.stringify(state.lastIdTokenClaims)}`, { codeName: 'IDTokenClaimsMismatchTokenMissing' });
|
|
405
449
|
}
|
|
406
|
-
if (tokenSet.
|
|
450
|
+
if (tokenSet.idToken &&
|
|
407
451
|
state.lastIdTokenClaims &&
|
|
408
452
|
state.lastIdTokenClaims.noIdToken) {
|
|
409
|
-
throw new types_1.MongoDBOIDCError(`Unexpected ID token received
|
|
453
|
+
throw new types_1.MongoDBOIDCError(`Unexpected ID token received.`, {
|
|
454
|
+
codeName: 'IDTokenClaimsMismatchTokenUnexpectedlyPresent',
|
|
455
|
+
});
|
|
410
456
|
}
|
|
411
|
-
if (tokenSet.
|
|
412
|
-
const idTokenClaims = tokenSet.
|
|
457
|
+
if (tokenSet.idToken) {
|
|
458
|
+
const idTokenClaims = tokenSet.idTokenClaims;
|
|
459
|
+
if (!idTokenClaims)
|
|
460
|
+
throw new types_1.MongoDBOIDCError('Internal error: id_token set but claims() unavailable', { codeName: 'IDTokenClaimsUnavailable' });
|
|
413
461
|
if (state.lastIdTokenClaims && !state.lastIdTokenClaims.noIdToken) {
|
|
414
462
|
for (const claim of ['aud', 'sub']) {
|
|
415
463
|
const normalize = (value) => {
|
|
@@ -418,7 +466,7 @@ class MongoDBOIDCPluginImpl {
|
|
|
418
466
|
const knownClaim = normalize(state.lastIdTokenClaims[claim]);
|
|
419
467
|
const newClaim = normalize(idTokenClaims[claim]);
|
|
420
468
|
if (knownClaim !== newClaim) {
|
|
421
|
-
throw new types_1.MongoDBOIDCError(`Unexpected '${claim}' field in id token: Expected ${knownClaim}, saw ${newClaim}
|
|
469
|
+
throw new types_1.MongoDBOIDCError(`Unexpected '${claim}' field in id token: Expected ${knownClaim}, saw ${newClaim}`, { codeName: 'IDTokenClaimsMismatchClaimMismatch' });
|
|
422
470
|
}
|
|
423
471
|
}
|
|
424
472
|
}
|
|
@@ -431,7 +479,7 @@ class MongoDBOIDCPluginImpl {
|
|
|
431
479
|
state.lastIdTokenClaims = { noIdToken: true };
|
|
432
480
|
this.logger.emit('mongodb-oidc-plugin:missing-id-token');
|
|
433
481
|
}
|
|
434
|
-
const refreshTokenId = (0, util_1.getRefreshTokenId)(tokenSet.
|
|
482
|
+
const refreshTokenId = (0, util_1.getRefreshTokenId)(tokenSet.refreshToken);
|
|
435
483
|
const updateId = updateIdCounter++;
|
|
436
484
|
const timerDuration = automaticRefreshTimeoutMS(tokenSet, this.options.passIdTokenAsAccessToken);
|
|
437
485
|
// Use `.call()` because in browsers, `setTimeout()` requires that it is called
|
|
@@ -453,7 +501,7 @@ class MongoDBOIDCPluginImpl {
|
|
|
453
501
|
this.logger.emit('mongodb-oidc-plugin:refresh-skipped', {
|
|
454
502
|
triggeringUpdateId: updateId,
|
|
455
503
|
expectedRefreshToken: refreshTokenId,
|
|
456
|
-
actualRefreshToken: (0, util_1.getRefreshTokenId)(state.currentTokenSet?.set?.
|
|
504
|
+
actualRefreshToken: (0, util_1.getRefreshTokenId)(state.currentTokenSet?.set?.refreshToken),
|
|
457
505
|
});
|
|
458
506
|
return false;
|
|
459
507
|
}
|
|
@@ -462,15 +510,17 @@ class MongoDBOIDCPluginImpl {
|
|
|
462
510
|
triggeringUpdateId: updateId,
|
|
463
511
|
refreshToken: refreshTokenId,
|
|
464
512
|
});
|
|
465
|
-
|
|
466
|
-
|
|
513
|
+
if (!tokenSet.refreshToken)
|
|
514
|
+
return false;
|
|
515
|
+
const { config } = await this.getOIDCClientConfiguration(state);
|
|
516
|
+
const refreshedTokens = await (0, openid_client_1.refreshTokenGrant)(config, tokenSet.refreshToken);
|
|
467
517
|
// Check again to avoid race conditions.
|
|
468
518
|
if (state.currentTokenSet?.set === tokenSet) {
|
|
469
519
|
this.logger.emit('mongodb-oidc-plugin:refresh-succeeded', {
|
|
470
520
|
triggeringUpdateId: updateId,
|
|
471
521
|
refreshToken: refreshTokenId,
|
|
472
522
|
});
|
|
473
|
-
this.updateStateWithTokenSet(state, refreshedTokens);
|
|
523
|
+
this.updateStateWithTokenSet(state, new util_1.TokenSet(refreshedTokens));
|
|
474
524
|
return true;
|
|
475
525
|
}
|
|
476
526
|
}
|
|
@@ -490,14 +540,15 @@ class MongoDBOIDCPluginImpl {
|
|
|
490
540
|
this.logger.emit('mongodb-oidc-plugin:state-updated', {
|
|
491
541
|
updateId,
|
|
492
542
|
timerDuration,
|
|
493
|
-
tokenSetId:
|
|
543
|
+
tokenSetId: tokenSet.stableId(),
|
|
494
544
|
});
|
|
495
545
|
}
|
|
546
|
+
static defaultRedirectURI = 'http://localhost:27097/redirect';
|
|
496
547
|
async authorizationCodeFlow(state, signal) {
|
|
497
548
|
const configuredRedirectURI = this.options.redirectURI ?? MongoDBOIDCPluginImpl.defaultRedirectURI;
|
|
498
|
-
const codeVerifier =
|
|
499
|
-
const codeChallenge =
|
|
500
|
-
const oidcStateParam = (
|
|
549
|
+
const codeVerifier = (0, openid_client_1.randomPKCECodeVerifier)();
|
|
550
|
+
const codeChallenge = await (0, openid_client_1.calculatePKCECodeChallenge)(codeVerifier);
|
|
551
|
+
const oidcStateParam = (0, openid_client_1.randomState)();
|
|
501
552
|
const server = new rfc_8252_http_server_1.RFC8252HTTPServer({
|
|
502
553
|
redirectUrl: configuredRedirectURI,
|
|
503
554
|
logger: this.logger,
|
|
@@ -506,11 +557,11 @@ class MongoDBOIDCPluginImpl {
|
|
|
506
557
|
});
|
|
507
558
|
let paramsUrl = '';
|
|
508
559
|
let scope;
|
|
509
|
-
let
|
|
560
|
+
let config;
|
|
510
561
|
let actualRedirectURI;
|
|
511
562
|
const nonce = this.options.skipNonceInAuthCodeRequest
|
|
512
563
|
? undefined
|
|
513
|
-
:
|
|
564
|
+
: (0, openid_client_1.randomNonce)();
|
|
514
565
|
try {
|
|
515
566
|
await (0, util_1.withAbortCheck)(signal, async ({ signalCheck, signalPromise }) => {
|
|
516
567
|
// We mark the operations that we want to allow to result in a fallback
|
|
@@ -524,18 +575,17 @@ class MongoDBOIDCPluginImpl {
|
|
|
524
575
|
signalPromise,
|
|
525
576
|
]);
|
|
526
577
|
actualRedirectURI = server.listeningRedirectUrl;
|
|
527
|
-
({ scope,
|
|
528
|
-
|
|
529
|
-
]));
|
|
530
|
-
const authCodeFlowUrl = client.authorizationUrl({
|
|
578
|
+
({ scope, config } = await this.getOIDCClientConfiguration(state, actualRedirectURI));
|
|
579
|
+
const authCodeFlowUrl = (0, openid_client_1.buildAuthorizationUrl)(config, {
|
|
531
580
|
scope,
|
|
532
581
|
code_challenge: codeChallenge,
|
|
533
582
|
code_challenge_method: 'S256',
|
|
534
583
|
state: oidcStateParam,
|
|
535
|
-
nonce,
|
|
584
|
+
...(nonce ? { nonce } : {}),
|
|
585
|
+
redirect_uri: actualRedirectURI,
|
|
536
586
|
});
|
|
537
587
|
(0, util_1.validateSecureHTTPUrl)(authCodeFlowUrl, 'authCodeFlowUrl');
|
|
538
|
-
const { localUrl, onAccessed: onLocalUrlAccessed } = await server.addRedirect(authCodeFlowUrl);
|
|
588
|
+
const { localUrl, onAccessed: onLocalUrlAccessed } = await server.addRedirect(authCodeFlowUrl.toString());
|
|
539
589
|
signalCheck();
|
|
540
590
|
// Handle errors from opening a browser but do not await the Promise
|
|
541
591
|
// in case it only resolves when the browser exits (which is the case
|
|
@@ -547,10 +597,10 @@ class MongoDBOIDCPluginImpl {
|
|
|
547
597
|
const extraErrorInfo = () => browserHandle?.spawnargs
|
|
548
598
|
? ` (${JSON.stringify(browserHandle.spawnargs)})`
|
|
549
599
|
: '';
|
|
550
|
-
browserHandle?.once('error', (err) => reject(new types_1.MongoDBOIDCError(`Opening browser failed with '${(0, util_1.messageFromError)(err)}'${extraErrorInfo()}`, { cause: err })));
|
|
600
|
+
browserHandle?.once('error', (err) => reject(new types_1.MongoDBOIDCError(`Opening browser failed with '${(0, util_1.messageFromError)(err)}'${extraErrorInfo()}`, { cause: err, codeName: 'BrowserOpenFailedSpawnError' })));
|
|
551
601
|
browserHandle?.once('exit', (code) => {
|
|
552
602
|
if (code !== 0)
|
|
553
|
-
reject(new types_1.MongoDBOIDCError(`Opening browser failed with exit code ${code}${extraErrorInfo()}
|
|
603
|
+
reject(new types_1.MongoDBOIDCError(`Opening browser failed with exit code ${code}${extraErrorInfo()}`, { codeName: 'BrowserOpenFailedNonZeroExit' }));
|
|
554
604
|
});
|
|
555
605
|
})
|
|
556
606
|
.catch(reject);
|
|
@@ -558,7 +608,9 @@ class MongoDBOIDCPluginImpl {
|
|
|
558
608
|
const timeout = allowFallbackIfFailed(new Promise((resolve, reject) => {
|
|
559
609
|
if (this.options.openBrowserTimeout !== 0) {
|
|
560
610
|
this.timers.setTimeout
|
|
561
|
-
.call(null, () => reject(new types_1.MongoDBOIDCError('Opening browser timed out'
|
|
611
|
+
.call(null, () => reject(new types_1.MongoDBOIDCError('Opening browser timed out', {
|
|
612
|
+
codeName: 'BrowserOpenTimeout',
|
|
613
|
+
})), this.options.openBrowserTimeout ?? api_1.kDefaultOpenBrowserTimeout)
|
|
562
614
|
?.unref?.();
|
|
563
615
|
}
|
|
564
616
|
}));
|
|
@@ -580,20 +632,18 @@ class MongoDBOIDCPluginImpl {
|
|
|
580
632
|
finally {
|
|
581
633
|
await server.close();
|
|
582
634
|
}
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
nonce,
|
|
635
|
+
const tokenSet = await (0, openid_client_1.authorizationCodeGrant)(config, new URL(paramsUrl), {
|
|
636
|
+
pkceCodeVerifier: codeVerifier,
|
|
637
|
+
expectedState: oidcStateParam,
|
|
638
|
+
expectedNonce: nonce,
|
|
588
639
|
});
|
|
589
|
-
this.updateStateWithTokenSet(state, tokenSet);
|
|
640
|
+
this.updateStateWithTokenSet(state, new util_1.TokenSet(tokenSet));
|
|
590
641
|
}
|
|
591
642
|
async deviceAuthorizationFlow(state, signal) {
|
|
592
|
-
const { scope,
|
|
643
|
+
const { scope, config } = await this.getOIDCClientConfiguration(state);
|
|
593
644
|
await (0, util_1.withAbortCheck)(signal, async ({ signalCheck, signalPromise }) => {
|
|
594
645
|
const deviceFlowHandle = await Promise.race([
|
|
595
|
-
|
|
596
|
-
client_id: client.metadata.client_id,
|
|
646
|
+
(0, openid_client_1.initiateDeviceAuthorization)(config, {
|
|
597
647
|
scope,
|
|
598
648
|
}),
|
|
599
649
|
signalPromise,
|
|
@@ -603,8 +653,8 @@ class MongoDBOIDCPluginImpl {
|
|
|
603
653
|
userCode: deviceFlowHandle.user_code,
|
|
604
654
|
verificationUrl: deviceFlowHandle.verification_uri,
|
|
605
655
|
});
|
|
606
|
-
const tokenSet = await
|
|
607
|
-
this.updateStateWithTokenSet(state, tokenSet);
|
|
656
|
+
const tokenSet = await (0, openid_client_1.pollDeviceAuthorizationGrant)(config, deviceFlowHandle, {}, { signal });
|
|
657
|
+
this.updateStateWithTokenSet(state, new util_1.TokenSet(tokenSet));
|
|
608
658
|
});
|
|
609
659
|
}
|
|
610
660
|
async initiateAuthAttempt(state, driverAbortSignal, { forceRefreshOrReauth = false } = {}) {
|
|
@@ -690,11 +740,15 @@ class MongoDBOIDCPluginImpl {
|
|
|
690
740
|
if (error)
|
|
691
741
|
throw error;
|
|
692
742
|
}
|
|
693
|
-
if (passIdTokenAsAccessToken && !state.currentTokenSet?.set?.
|
|
694
|
-
throw new types_1.MongoDBOIDCError('Could not retrieve valid ID token'
|
|
743
|
+
if (passIdTokenAsAccessToken && !state.currentTokenSet?.set?.idToken) {
|
|
744
|
+
throw new types_1.MongoDBOIDCError('Could not retrieve valid ID token', {
|
|
745
|
+
codeName: 'IDTokenMissingFromTokenSet',
|
|
746
|
+
});
|
|
695
747
|
}
|
|
696
|
-
else if (!state.currentTokenSet?.set?.
|
|
697
|
-
throw new types_1.MongoDBOIDCError('Could not retrieve valid access token'
|
|
748
|
+
else if (!state.currentTokenSet?.set?.accessToken) {
|
|
749
|
+
throw new types_1.MongoDBOIDCError('Could not retrieve valid access token', {
|
|
750
|
+
codeName: 'AccessTokenMissingFromTokenSet',
|
|
751
|
+
});
|
|
698
752
|
}
|
|
699
753
|
}
|
|
700
754
|
catch (err) {
|
|
@@ -702,28 +756,29 @@ class MongoDBOIDCPluginImpl {
|
|
|
702
756
|
authStateId: state.id,
|
|
703
757
|
error: (0, util_1.errorString)(err),
|
|
704
758
|
});
|
|
705
|
-
throw err;
|
|
759
|
+
throw await (0, util_1.improveHTTPResponseBasedError)(err);
|
|
706
760
|
}
|
|
707
761
|
finally {
|
|
708
762
|
this.options.signal?.removeEventListener('abort', optionsAbortCb);
|
|
709
763
|
driverAbortSignal?.removeEventListener('abort', driverAbortCb);
|
|
710
764
|
}
|
|
711
|
-
const {
|
|
712
|
-
const
|
|
765
|
+
const { tokenType, accessToken, idToken, refreshToken } = state.currentTokenSet.set;
|
|
766
|
+
const expiresAt = state.currentTokenSet.set.expiresAt;
|
|
767
|
+
const tokenSetId = state.currentTokenSet.set.stableId();
|
|
713
768
|
// We would not want to return the access token or ID token of a token set whose
|
|
714
769
|
// accompanying refresh token was passed to us by the driver
|
|
715
770
|
const willRetryWithForceRefreshOrReauth = !forceRefreshOrReauth &&
|
|
716
771
|
!!state.discardingTokenSets?.includes(tokenSetId);
|
|
717
772
|
this.logger.emit('mongodb-oidc-plugin:auth-succeeded', {
|
|
718
773
|
authStateId: state.id,
|
|
719
|
-
tokenType:
|
|
720
|
-
refreshToken: (0, util_1.getRefreshTokenId)(
|
|
721
|
-
expiresAt:
|
|
774
|
+
tokenType: tokenType ?? null, // DPoP or Bearer
|
|
775
|
+
refreshToken: (0, util_1.getRefreshTokenId)(refreshToken),
|
|
776
|
+
expiresAt: expiresAt ? new Date(expiresAt * 1000).toISOString() : null,
|
|
722
777
|
passIdTokenAsAccessToken: !!passIdTokenAsAccessToken,
|
|
723
778
|
tokens: {
|
|
724
|
-
accessToken:
|
|
725
|
-
idToken:
|
|
726
|
-
refreshToken:
|
|
779
|
+
accessToken: accessToken,
|
|
780
|
+
idToken: idToken,
|
|
781
|
+
refreshToken: refreshToken,
|
|
727
782
|
},
|
|
728
783
|
tokenSetId,
|
|
729
784
|
forceRefreshOrReauth,
|
|
@@ -735,7 +790,7 @@ class MongoDBOIDCPluginImpl {
|
|
|
735
790
|
});
|
|
736
791
|
}
|
|
737
792
|
return {
|
|
738
|
-
accessToken: passIdTokenAsAccessToken ?
|
|
793
|
+
accessToken: passIdTokenAsAccessToken ? idToken || '' : accessToken,
|
|
739
794
|
refreshToken: tokenSetId,
|
|
740
795
|
// Passing `expiresInSeconds: 0` results in the driver not caching the token.
|
|
741
796
|
// We perform our own caching here inside the plugin, so interactions with the
|
|
@@ -753,13 +808,15 @@ class MongoDBOIDCPluginImpl {
|
|
|
753
808
|
if (params.version !== 1) {
|
|
754
809
|
throw new types_1.MongoDBOIDCError(
|
|
755
810
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
756
|
-
`OIDC MongoDB driver protocol mismatch: unknown version ${params.version}
|
|
811
|
+
`OIDC MongoDB driver protocol mismatch: unknown version ${params.version}`, { codeName: 'ProtocolVersionMismatch' });
|
|
757
812
|
}
|
|
758
813
|
if (this.destroyed) {
|
|
759
|
-
throw new types_1.MongoDBOIDCError('This OIDC plugin instance has been destroyed and is no longer active');
|
|
814
|
+
throw new types_1.MongoDBOIDCError('This OIDC plugin instance has been destroyed and is no longer active', { codeName: 'PluginInstanceDestroyed' });
|
|
760
815
|
}
|
|
761
816
|
if (!params.idpInfo) {
|
|
762
|
-
throw new types_1.MongoDBOIDCError('No IdP information provided'
|
|
817
|
+
throw new types_1.MongoDBOIDCError('No IdP information provided', {
|
|
818
|
+
codeName: 'IdPInfoMissing',
|
|
819
|
+
});
|
|
763
820
|
}
|
|
764
821
|
const state = this.getAuthState({
|
|
765
822
|
...params.idpInfo,
|
|
@@ -777,7 +834,7 @@ class MongoDBOIDCPluginImpl {
|
|
|
777
834
|
// If the driver called us with a refresh token, that means that its corresponding
|
|
778
835
|
// access token has become invalid and we should always return a new one.
|
|
779
836
|
if (params.refreshToken) {
|
|
780
|
-
(state.discardingTokenSets
|
|
837
|
+
(state.discardingTokenSets ??= []).push(params.refreshToken);
|
|
781
838
|
this.logger.emit('mongodb-oidc-plugin:discarding-token-set', {
|
|
782
839
|
tokenSetId: params.refreshToken,
|
|
783
840
|
});
|
|
@@ -827,5 +884,4 @@ class MongoDBOIDCPluginImpl {
|
|
|
827
884
|
}
|
|
828
885
|
}
|
|
829
886
|
exports.MongoDBOIDCPluginImpl = MongoDBOIDCPluginImpl;
|
|
830
|
-
MongoDBOIDCPluginImpl.defaultRedirectURI = 'http://localhost:27097/redirect';
|
|
831
887
|
//# sourceMappingURL=plugin.js.map
|