Skip to content

Commit 1598600

Browse files
authored
feat: add codeName to all OIDC errors coming from the plugin (#219)
It came up in conversations that OIDC connections in Compass fail frequently, but we do not have real insight into why that is and what the most common errors are overall.
1 parent 161eb6d commit 1598600

File tree

4 files changed

+72
-31
lines changed

4 files changed

+72
-31
lines changed

src/plugin.ts

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -253,15 +253,17 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
253253
throw new MongoDBOIDCError(
254254
`Stored OIDC data could not be deserialized: ${
255255
(err as Error).message
256-
}`
256+
}`,
257+
{ cause: err, codeName: 'DeserializeFormatMismatch' }
257258
);
258259
}
259260

260261
if (original.oidcPluginStateVersion !== 1) {
261262
throw new MongoDBOIDCError(
262263
`Stored OIDC data could not be deserialized because of a version mismatch (got ${JSON.stringify(
263264
original.oidcPluginStateVersion
264-
)}, expected 1)`
265+
)}, expected 1)`,
266+
{ codeName: 'DeserializeVersionMismatch' }
265267
);
266268
}
267269

@@ -353,13 +355,16 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
353355
serverMetadata: IdPServerInfo & Pick<OIDCCallbackParams, 'username'>
354356
): UserOIDCAuthState {
355357
if (!serverMetadata.issuer || typeof serverMetadata.issuer !== 'string') {
356-
throw new MongoDBOIDCError(`'issuer' is missing`);
358+
throw new MongoDBOIDCError(`'issuer' is missing`, {
359+
codeName: 'MissingIssuer',
360+
});
357361
}
358362
validateSecureHTTPUrl(serverMetadata.issuer, 'issuer');
359363

360364
if (!serverMetadata.clientId) {
361365
throw new MongoDBOIDCError(
362-
'No clientId passed in server OIDC metadata object'
366+
'No clientId passed in server OIDC metadata object',
367+
{ codeName: 'MissingClientId' }
363368
);
364369
}
365370

@@ -502,6 +507,7 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
502507
)}: ${messageFromError(err)}`,
503508
{
504509
cause: err,
510+
codeName: 'IssuerMetadataDiscoveryFailed',
505511
}
506512
);
507513
}
@@ -538,7 +544,8 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
538544
new URL(options.url);
539545
if (!/^[a-zA-Z0-9%/:;_.,=@-]+$/.test(options.url)) {
540546
throw new MongoDBOIDCError(
541-
`Unexpected format for internally generated URL: '${options.url}'`
547+
`Unexpected format for internally generated URL: '${options.url}'`,
548+
{ codeName: 'GeneratedUrlInvalidForOpenBrowserCommand' }
542549
);
543550
}
544551
this.logger.emit('mongodb-oidc-plugin:open-browser', {
@@ -547,7 +554,8 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
547554
if (this.options.openBrowser === false) {
548555
// We should never really get to this point
549556
throw new MongoDBOIDCError(
550-
'Cannot open browser if `openBrowser` is false'
557+
'Cannot open browser if `openBrowser` is false',
558+
{ codeName: 'OpenBrowserDisabled' }
551559
);
552560
}
553561
if (typeof this.options.openBrowser === 'function') {
@@ -567,7 +575,9 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
567575
child.unref();
568576
return child;
569577
}
570-
throw new MongoDBOIDCError('Unknown format for `openBrowser`');
578+
throw new MongoDBOIDCError('Unknown format for `openBrowser`', {
579+
codeName: 'OpenBrowserOptionFormatUnknown',
580+
});
571581
}
572582

573583
private async notifyDeviceFlow(
@@ -580,7 +590,8 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
580590
if (!this.options.notifyDeviceFlow) {
581591
// Should never happen.
582592
throw new MongoDBOIDCError(
583-
'notifyDeviceFlow() requested but not provided'
593+
'notifyDeviceFlow() requested but not provided',
594+
{ codeName: 'DeviceFlowNotEnabled' }
584595
);
585596
}
586597
this.logger.emit('mongodb-oidc-plugin:notify-device-flow');
@@ -605,7 +616,8 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
605616
throw new MongoDBOIDCError(
606617
`ID token expected, but not found. Expected claims: ${JSON.stringify(
607618
state.lastIdTokenClaims
608-
)}`
619+
)}`,
620+
{ codeName: 'IDTokenClaimsMismatchTokenMissing' }
609621
);
610622
}
611623

@@ -614,14 +626,17 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
614626
state.lastIdTokenClaims &&
615627
state.lastIdTokenClaims.noIdToken
616628
) {
617-
throw new MongoDBOIDCError(`Unexpected ID token received.`);
629+
throw new MongoDBOIDCError(`Unexpected ID token received.`, {
630+
codeName: 'IDTokenClaimsMismatchTokenUnexpectedlyPresent',
631+
});
618632
}
619633

620634
if (tokenSet.idToken) {
621635
const idTokenClaims = tokenSet.idTokenClaims;
622636
if (!idTokenClaims)
623637
throw new MongoDBOIDCError(
624-
'Internal error: id_token set but claims() unavailable'
638+
'Internal error: id_token set but claims() unavailable',
639+
{ codeName: 'IDTokenClaimsUnavailable' }
625640
);
626641
if (state.lastIdTokenClaims && !state.lastIdTokenClaims.noIdToken) {
627642
for (const claim of ['aud', 'sub'] as const) {
@@ -635,7 +650,8 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
635650

636651
if (knownClaim !== newClaim) {
637652
throw new MongoDBOIDCError(
638-
`Unexpected '${claim}' field in id token: Expected ${knownClaim}, saw ${newClaim}`
653+
`Unexpected '${claim}' field in id token: Expected ${knownClaim}, saw ${newClaim}`,
654+
{ codeName: 'IDTokenClaimsMismatchClaimMismatch' }
639655
);
640656
}
641657
}
@@ -807,15 +823,16 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
807823
`Opening browser failed with '${messageFromError(
808824
err
809825
)}'${extraErrorInfo()}`,
810-
{ cause: err }
826+
{ cause: err, codeName: 'BrowserOpenFailedSpawnError' }
811827
)
812828
)
813829
);
814830
browserHandle?.once('exit', (code) => {
815831
if (code !== 0)
816832
reject(
817833
new MongoDBOIDCError(
818-
`Opening browser failed with exit code ${code}${extraErrorInfo()}`
834+
`Opening browser failed with exit code ${code}${extraErrorInfo()}`,
835+
{ codeName: 'BrowserOpenFailedNonZeroExit' }
819836
)
820837
);
821838
});
@@ -831,7 +848,11 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
831848
.call(
832849
null,
833850
() =>
834-
reject(new MongoDBOIDCError('Opening browser timed out')),
851+
reject(
852+
new MongoDBOIDCError('Opening browser timed out', {
853+
codeName: 'BrowserOpenTimeout',
854+
})
855+
),
835856
this.options.openBrowserTimeout ?? kDefaultOpenBrowserTimeout
836857
)
837858
?.unref?.();
@@ -988,9 +1009,13 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
9881009
}
9891010

9901011
if (passIdTokenAsAccessToken && !state.currentTokenSet?.set?.idToken) {
991-
throw new MongoDBOIDCError('Could not retrieve valid ID token');
1012+
throw new MongoDBOIDCError('Could not retrieve valid ID token', {
1013+
codeName: 'IDTokenMissingFromTokenSet',
1014+
});
9921015
} else if (!state.currentTokenSet?.set?.accessToken) {
993-
throw new MongoDBOIDCError('Could not retrieve valid access token');
1016+
throw new MongoDBOIDCError('Could not retrieve valid access token', {
1017+
codeName: 'AccessTokenMissingFromTokenSet',
1018+
});
9941019
}
9951020
} catch (err: unknown) {
9961021
this.logger.emit('mongodb-oidc-plugin:auth-failed', {
@@ -1058,18 +1083,22 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
10581083
if (params.version !== 1) {
10591084
throw new MongoDBOIDCError(
10601085
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1061-
`OIDC MongoDB driver protocol mismatch: unknown version ${params.version}`
1086+
`OIDC MongoDB driver protocol mismatch: unknown version ${params.version}`,
1087+
{ codeName: 'ProtocolVersionMismatch' }
10621088
);
10631089
}
10641090

10651091
if (this.destroyed) {
10661092
throw new MongoDBOIDCError(
1067-
'This OIDC plugin instance has been destroyed and is no longer active'
1093+
'This OIDC plugin instance has been destroyed and is no longer active',
1094+
{ codeName: 'PluginInstanceDestroyed' }
10681095
);
10691096
}
10701097

10711098
if (!params.idpInfo) {
1072-
throw new MongoDBOIDCError('No IdP information provided');
1099+
throw new MongoDBOIDCError('No IdP information provided', {
1100+
codeName: 'IdPInfoMissing',
1101+
});
10731102
}
10741103

10751104
const state = this.getAuthState({

src/rfc-8252-http-server.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,9 @@ export class RFC8252HTTPServer {
164164
private _handleOIDCCallback: RequestHandler = (req, res) => {
165165
const baseUrl = this.listeningRedirectUrl;
166166
if (!baseUrl) {
167-
throw new MongoDBOIDCError('Received HTTP request while not listening');
167+
throw new MongoDBOIDCError('Received HTTP request while not listening', {
168+
codeName: 'InvalidRequestNotListening',
169+
});
168170
}
169171

170172
let isAcceptedOIDCResponse = false;
@@ -193,7 +195,8 @@ export class RFC8252HTTPServer {
193195
new MongoDBOIDCError(
194196
`${info.error || 'unknown_code'}: ${
195197
info.errorDescription || '[no details]'
196-
}`
198+
}`,
199+
{ codeName: 'IDPRejectedAuthCodeFlow' }
197200
)
198201
);
199202
}
@@ -331,7 +334,8 @@ export class RFC8252HTTPServer {
331334
.join(',');
332335
// Should never happen
333336
throw new MongoDBOIDCError(
334-
`Server is listening in inconsistent state: ${addressesDebugInfo}`
337+
`Server is listening in inconsistent state: ${addressesDebugInfo}`,
338+
{ codeName: 'InvalidServerStatePortMismatch' }
335339
);
336340
}
337341
return port;
@@ -357,13 +361,15 @@ export class RFC8252HTTPServer {
357361
public async listen(): Promise<void> {
358362
if (this.listeningPort !== undefined) {
359363
throw new MongoDBOIDCError(
360-
`Already listening on ${this.redirectUrl.toString()}`
364+
`Already listening on ${this.redirectUrl.toString()}`,
365+
{ codeName: 'InvalidServerStateAlreadyListening' }
361366
);
362367
}
363368

364369
if (this.redirectUrl.protocol !== 'http:') {
365370
throw new MongoDBOIDCError(
366-
`Cannot handle listening on non-HTTP URL, got ${this.redirectUrl.protocol}`
371+
`Cannot handle listening on non-HTTP URL, got ${this.redirectUrl.protocol}`,
372+
{ codeName: 'InvalidRedirectURLProtocol' }
367373
);
368374
}
369375

@@ -403,7 +409,8 @@ export class RFC8252HTTPServer {
403409

404410
if (dnsResults.length === 0) {
405411
throw new MongoDBOIDCError(
406-
`DNS query for ${this.redirectUrl.hostname} returned no results`
412+
`DNS query for ${this.redirectUrl.hostname} returned no results`,
413+
{ codeName: 'LocalEndpointResolutionFailedNoResults' }
407414
);
408415
}
409416

@@ -436,7 +443,8 @@ export class RFC8252HTTPServer {
436443
if (typeof port !== 'number') {
437444
// Should never happen
438445
throw new MongoDBOIDCError(
439-
`Listening on ${dnsResults[0].address} (family = ${dnsResults[0].family}) did not return a port`
446+
`Listening on ${dnsResults[0].address} (family = ${dnsResults[0].family}) did not return a port`,
447+
{ codeName: 'LocalEndpointResolutionFailedNoPort' }
440448
);
441449
}
442450
}

src/types.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,10 +211,14 @@ const MongoDBOIDCErrorTag = Symbol.for('@@mdb.oidcplugin.MongoDBOIDCErrorTag');
211211
export class MongoDBOIDCError extends Error {
212212
/** @internal */
213213
private [MongoDBOIDCErrorTag] = true;
214+
public readonly codeName: `MongoDBOIDC${string}`;
214215

215-
constructor(message: string, { cause }: { cause?: unknown } = {}) {
216-
// @ts-expect-error `cause` is not supported in Node.js 14
216+
constructor(
217+
message: string,
218+
{ cause, codeName }: { cause?: unknown; codeName: string }
219+
) {
217220
super(message, { cause });
221+
this.codeName = `MongoDBOIDC${codeName}`;
218222
}
219223

220224
static isMongoDBOIDCError(value: unknown): value is MongoDBOIDCError {

tsconfig.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
"outDir": "dist",
55
"removeComments": false,
66
"sourceMap": true,
7-
"target": "es2020",
8-
"lib": ["es2020"],
7+
"target": "es2023",
8+
"lib": ["es2023"],
99
"erasableSyntaxOnly": true
1010
},
1111
"include": ["src/**/*"],

0 commit comments

Comments
 (0)