Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 49 additions & 20 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,15 +253,17 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
throw new MongoDBOIDCError(
`Stored OIDC data could not be deserialized: ${
(err as Error).message
}`
}`,
{ cause: err, codeName: 'DeserializeFormatMismatch' }
);
}

if (original.oidcPluginStateVersion !== 1) {
throw new MongoDBOIDCError(
`Stored OIDC data could not be deserialized because of a version mismatch (got ${JSON.stringify(
original.oidcPluginStateVersion
)}, expected 1)`
)}, expected 1)`,
{ codeName: 'DeserializeVersionMismatch' }
);
}

Expand Down Expand Up @@ -353,13 +355,16 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
serverMetadata: IdPServerInfo & Pick<OIDCCallbackParams, 'username'>
): UserOIDCAuthState {
if (!serverMetadata.issuer || typeof serverMetadata.issuer !== 'string') {
throw new MongoDBOIDCError(`'issuer' is missing`);
throw new MongoDBOIDCError(`'issuer' is missing`, {
codeName: 'MissingIssuer',
});
}
validateSecureHTTPUrl(serverMetadata.issuer, 'issuer');

if (!serverMetadata.clientId) {
throw new MongoDBOIDCError(
'No clientId passed in server OIDC metadata object'
'No clientId passed in server OIDC metadata object',
{ codeName: 'MissingClientId' }
);
}

Expand Down Expand Up @@ -502,6 +507,7 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
)}: ${messageFromError(err)}`,
{
cause: err,
codeName: 'IssuerMetadataDiscoveryFailed',
}
);
}
Expand Down Expand Up @@ -538,7 +544,8 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
new URL(options.url);
if (!/^[a-zA-Z0-9%/:;_.,=@-]+$/.test(options.url)) {
throw new MongoDBOIDCError(
`Unexpected format for internally generated URL: '${options.url}'`
`Unexpected format for internally generated URL: '${options.url}'`,
{ codeName: 'GeneratedUrlInvalidForOpenBrowserCommand' }
);
}
this.logger.emit('mongodb-oidc-plugin:open-browser', {
Expand All @@ -547,7 +554,8 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
if (this.options.openBrowser === false) {
// We should never really get to this point
throw new MongoDBOIDCError(
'Cannot open browser if `openBrowser` is false'
'Cannot open browser if `openBrowser` is false',
{ codeName: 'OpenBrowserDisabled' }
);
}
if (typeof this.options.openBrowser === 'function') {
Expand All @@ -567,7 +575,9 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
child.unref();
return child;
}
throw new MongoDBOIDCError('Unknown format for `openBrowser`');
throw new MongoDBOIDCError('Unknown format for `openBrowser`', {
codeName: 'OpenBrowserOptionFormatUnknown',
});
}

private async notifyDeviceFlow(
Expand All @@ -580,7 +590,8 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
if (!this.options.notifyDeviceFlow) {
// Should never happen.
throw new MongoDBOIDCError(
'notifyDeviceFlow() requested but not provided'
'notifyDeviceFlow() requested but not provided',
{ codeName: 'DeviceFlowNotEnabled' }
);
}
this.logger.emit('mongodb-oidc-plugin:notify-device-flow');
Expand All @@ -605,7 +616,8 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
throw new MongoDBOIDCError(
`ID token expected, but not found. Expected claims: ${JSON.stringify(
state.lastIdTokenClaims
)}`
)}`,
{ codeName: 'IDTokenClaimsMismatchTokenMissing' }
);
}

Expand All @@ -614,14 +626,17 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
state.lastIdTokenClaims &&
state.lastIdTokenClaims.noIdToken
) {
throw new MongoDBOIDCError(`Unexpected ID token received.`);
throw new MongoDBOIDCError(`Unexpected ID token received.`, {
codeName: 'IDTokenClaimsMismatchTokenUnexpectedlyPresent',
});
}

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

if (knownClaim !== newClaim) {
throw new MongoDBOIDCError(
`Unexpected '${claim}' field in id token: Expected ${knownClaim}, saw ${newClaim}`
`Unexpected '${claim}' field in id token: Expected ${knownClaim}, saw ${newClaim}`,
{ codeName: 'IDTokenClaimsMismatchClaimMismatch' }
);
}
}
Expand Down Expand Up @@ -807,15 +823,16 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
`Opening browser failed with '${messageFromError(
err
)}'${extraErrorInfo()}`,
{ cause: err }
{ cause: err, codeName: 'BrowserOpenFailedSpawnError' }
)
)
);
browserHandle?.once('exit', (code) => {
if (code !== 0)
reject(
new MongoDBOIDCError(
`Opening browser failed with exit code ${code}${extraErrorInfo()}`
`Opening browser failed with exit code ${code}${extraErrorInfo()}`,
{ codeName: 'BrowserOpenFailedNonZeroExit' }
)
);
});
Expand All @@ -831,7 +848,11 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
.call(
null,
() =>
reject(new MongoDBOIDCError('Opening browser timed out')),
reject(
new MongoDBOIDCError('Opening browser timed out', {
codeName: 'BrowserOpenTimeout',
})
),
this.options.openBrowserTimeout ?? kDefaultOpenBrowserTimeout
)
?.unref?.();
Expand Down Expand Up @@ -988,9 +1009,13 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
}

if (passIdTokenAsAccessToken && !state.currentTokenSet?.set?.idToken) {
throw new MongoDBOIDCError('Could not retrieve valid ID token');
throw new MongoDBOIDCError('Could not retrieve valid ID token', {
codeName: 'IDTokenMissingFromTokenSet',
});
} else if (!state.currentTokenSet?.set?.accessToken) {
throw new MongoDBOIDCError('Could not retrieve valid access token');
throw new MongoDBOIDCError('Could not retrieve valid access token', {
codeName: 'AccessTokenMissingFromTokenSet',
});
}
} catch (err: unknown) {
this.logger.emit('mongodb-oidc-plugin:auth-failed', {
Expand Down Expand Up @@ -1058,18 +1083,22 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
if (params.version !== 1) {
throw new MongoDBOIDCError(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`OIDC MongoDB driver protocol mismatch: unknown version ${params.version}`
`OIDC MongoDB driver protocol mismatch: unknown version ${params.version}`,
{ codeName: 'ProtocolVersionMismatch' }
);
}

if (this.destroyed) {
throw new MongoDBOIDCError(
'This OIDC plugin instance has been destroyed and is no longer active'
'This OIDC plugin instance has been destroyed and is no longer active',
{ codeName: 'PluginInstanceDestroyed' }
);
}

if (!params.idpInfo) {
throw new MongoDBOIDCError('No IdP information provided');
throw new MongoDBOIDCError('No IdP information provided', {
codeName: 'IdPInfoMissing',
});
}

const state = this.getAuthState({
Expand Down
22 changes: 15 additions & 7 deletions src/rfc-8252-http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,9 @@ export class RFC8252HTTPServer {
private _handleOIDCCallback: RequestHandler = (req, res) => {
const baseUrl = this.listeningRedirectUrl;
if (!baseUrl) {
throw new MongoDBOIDCError('Received HTTP request while not listening');
throw new MongoDBOIDCError('Received HTTP request while not listening', {
codeName: 'InvalidRequestNotListening',
});
}

let isAcceptedOIDCResponse = false;
Expand Down Expand Up @@ -193,7 +195,8 @@ export class RFC8252HTTPServer {
new MongoDBOIDCError(
`${info.error || 'unknown_code'}: ${
info.errorDescription || '[no details]'
}`
}`,
{ codeName: 'IDPRejectedAuthCodeFlow' }
)
);
}
Expand Down Expand Up @@ -331,7 +334,8 @@ export class RFC8252HTTPServer {
.join(',');
// Should never happen
throw new MongoDBOIDCError(
`Server is listening in inconsistent state: ${addressesDebugInfo}`
`Server is listening in inconsistent state: ${addressesDebugInfo}`,
{ codeName: 'InvalidServerStatePortMismatch' }
);
}
return port;
Expand All @@ -357,13 +361,15 @@ export class RFC8252HTTPServer {
public async listen(): Promise<void> {
if (this.listeningPort !== undefined) {
throw new MongoDBOIDCError(
`Already listening on ${this.redirectUrl.toString()}`
`Already listening on ${this.redirectUrl.toString()}`,
{ codeName: 'InvalidServerStateAlreadyListening' }
);
}

if (this.redirectUrl.protocol !== 'http:') {
throw new MongoDBOIDCError(
`Cannot handle listening on non-HTTP URL, got ${this.redirectUrl.protocol}`
`Cannot handle listening on non-HTTP URL, got ${this.redirectUrl.protocol}`,
{ codeName: 'InvalidRedirectURLProtocol' }
);
}

Expand Down Expand Up @@ -403,7 +409,8 @@ export class RFC8252HTTPServer {

if (dnsResults.length === 0) {
throw new MongoDBOIDCError(
`DNS query for ${this.redirectUrl.hostname} returned no results`
`DNS query for ${this.redirectUrl.hostname} returned no results`,
{ codeName: 'LocalEndpointResolutionFailedNoResults' }
);
}

Expand Down Expand Up @@ -436,7 +443,8 @@ export class RFC8252HTTPServer {
if (typeof port !== 'number') {
// Should never happen
throw new MongoDBOIDCError(
`Listening on ${dnsResults[0].address} (family = ${dnsResults[0].family}) did not return a port`
`Listening on ${dnsResults[0].address} (family = ${dnsResults[0].family}) did not return a port`,
{ codeName: 'LocalEndpointResolutionFailedNoPort' }
);
}
}
Expand Down
8 changes: 6 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,14 @@ const MongoDBOIDCErrorTag = Symbol.for('@@mdb.oidcplugin.MongoDBOIDCErrorTag');
export class MongoDBOIDCError extends Error {
/** @internal */
private [MongoDBOIDCErrorTag] = true;
public readonly codeName: `MongoDBOIDC${string}`;

constructor(message: string, { cause }: { cause?: unknown } = {}) {
// @ts-expect-error `cause` is not supported in Node.js 14
constructor(
message: string,
{ cause, codeName }: { cause?: unknown; codeName: string }
) {
super(message, { cause });
this.codeName = `MongoDBOIDC${codeName}`;
}

static isMongoDBOIDCError(value: unknown): value is MongoDBOIDCError {
Expand Down
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"outDir": "dist",
"removeComments": false,
"sourceMap": true,
"target": "es2020",
"lib": ["es2020"],
"target": "es2023",
"lib": ["es2023"],
"erasableSyntaxOnly": true
},
"include": ["src/**/*"],
Expand Down
Loading