Skip to content

Commit cb07f51

Browse files
authored
fix: improve error message for generic openid-client error MONGOSH-2487 (#230)
* fix: improve error message for generic openid-client error MONGOSH-2487 Similar to previous commits, this change ensures that diagnostic information is not lost through recent changes to openid-client's error system, where currently a number of errors are grouped together under "something went wrong" (https://github.com/panva/openid-client/blob/a1e4a4fd88fcdff12d88e3ff73f3e27fe5df4252/src/index.ts#L975) and the library expects consumers to consistently expose the `cause` property of the error object. This also unifies the two existing `messageFromError` and `errorString` implementations, which happened to duplicate each other. * fixup: electron compat * fixup: do not add "[object Response]" to errors * fixup: add anchors to regex
1 parent deb2f56 commit cb07f51

File tree

5 files changed

+145
-19
lines changed

5 files changed

+145
-19
lines changed

src/plugin.spec.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ import type {
4040
TokenMetadata,
4141
} from '@mongodb-js/oidc-mock-provider';
4242
import { OIDCMockProvider } from '@mongodb-js/oidc-mock-provider';
43+
import type { Server as HTTPSServer } from 'https';
44+
import { createServer as createHTTPSServer } from 'https';
4345

4446
// node-fetch@3 is ESM-only...
4547
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
@@ -1417,6 +1419,36 @@ describe('OIDC plugin (mock OIDC provider)', function () {
14171419
});
14181420

14191421
context('HTTP request handling', function () {
1422+
let badHTTPSServer: HTTPSServer;
1423+
1424+
before(async function () {
1425+
badHTTPSServer = createHTTPSServer(
1426+
{
1427+
key: await fs.readFile(
1428+
// https://github.com/nodejs/node/blob/becb55aac3f7eb93b03223744c35c6194f11e3e9/test/fixtures/keys/agent2-key.pem
1429+
path.resolve(__dirname, '..', 'test', 'self-signed-key.pem')
1430+
),
1431+
cert: await fs.readFile(
1432+
// https://github.com/nodejs/node/blob/becb55aac3f7eb93b03223744c35c6194f11e3e9/test/fixtures/keys/agent2-cert.pem
1433+
path.resolve(__dirname, '..', 'test', 'self-signed-cert.pem')
1434+
),
1435+
},
1436+
(req, res) => {
1437+
res.writeHead(200, { 'Content-Type': 'application/json' });
1438+
res.end(JSON.stringify({ message: 'Hello World' }));
1439+
}
1440+
);
1441+
badHTTPSServer.listen(0, 'localhost');
1442+
await once(badHTTPSServer, 'listening');
1443+
});
1444+
1445+
after(async function () {
1446+
if (badHTTPSServer) {
1447+
badHTTPSServer.close();
1448+
await once(badHTTPSServer, 'close');
1449+
}
1450+
});
1451+
14201452
it('will track all outgoing HTTP requests', async function () {
14211453
const pluginHttpRequests: string[] = [];
14221454
const localServerHttpRequests: string[] = [];
@@ -1555,7 +1587,7 @@ describe('OIDC plugin (mock OIDC provider)', function () {
15551587
expect(customFetch).to.have.been.called;
15561588
});
15571589

1558-
it('logs helpful error messages', async function () {
1590+
it('logs helpful error messages for HTTP failures', async function () {
15591591
const outboundHTTPRequestsCompleted: any[] = [];
15601592

15611593
getTokenPayload = () => Promise.reject(new Error('test failure'));
@@ -1595,6 +1627,48 @@ describe('OIDC plugin (mock OIDC provider)', function () {
15951627
});
15961628
});
15971629

1630+
it('logs helpful error messages for TLS failures', async function () {
1631+
const outboundHTTPRequestsFailed: any[] = [];
1632+
const port = (badHTTPSServer.address() as AddressInfo).port;
1633+
1634+
const plugin = createMongoDBOIDCPlugin({
1635+
openBrowserTimeout: 60_000,
1636+
openBrowser: fetchBrowser,
1637+
allowedFlows: ['auth-code'],
1638+
redirectURI: 'http://localhost:0/callback',
1639+
});
1640+
1641+
plugin.logger.on(
1642+
'mongodb-oidc-plugin:outbound-http-request-failed',
1643+
(ev) => outboundHTTPRequestsFailed.push(ev)
1644+
);
1645+
1646+
let selfSignedReason = 'self-signed certificate';
1647+
try {
1648+
await requestToken(plugin, {
1649+
issuer: `https://localhost:${port}/`,
1650+
clientId: 'mockclientid',
1651+
requestScopes: [],
1652+
});
1653+
expect.fail('missed exception');
1654+
} catch (err: any) {
1655+
// Electron and Node.js provide different error messages
1656+
selfSignedReason = err.message.includes('self-signed certificate')
1657+
? 'self-signed certificate'
1658+
: 'self signed certificate';
1659+
expect(err.message).to.include(
1660+
`Unable to fetch issuer metadata for "https://localhost:${port}/": something went wrong (caused by: request to https://localhost:${port}/.well-known/openid-configuration failed, reason: ${selfSignedReason}`
1661+
);
1662+
}
1663+
const entry = outboundHTTPRequestsFailed.find(
1664+
(ev) =>
1665+
ev.url ===
1666+
`https://localhost:${port}/.well-known/openid-configuration`
1667+
);
1668+
expect(entry).to.exist;
1669+
expect(entry.error).to.include(selfSignedReason);
1670+
});
1671+
15981672
it('handles JSON failure responses from the IDP', async function () {
15991673
overrideRequestHandler = (url, req, res) => {
16001674
if (new URL(url).pathname.endsWith('/token')) {

src/plugin.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
errorString,
1212
getRefreshTokenId,
1313
improveHTTPResponseBasedError,
14-
messageFromError,
1514
nodeFetchCompat,
1615
normalizeObject,
1716
throwIfAborted,
@@ -525,7 +524,7 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
525524
throw new MongoDBOIDCError(
526525
`Unable to fetch issuer metadata for ${JSON.stringify(
527526
serverMetadata.issuer
528-
)}: ${messageFromError(err)}`,
527+
)}: ${errorString(err)}`,
529528
{
530529
cause: err,
531530
codeName: 'IssuerMetadataDiscoveryFailed',
@@ -844,7 +843,7 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
844843
browserHandle?.once('error', (err) =>
845844
reject(
846845
new MongoDBOIDCError(
847-
`Opening browser failed with '${messageFromError(
846+
`Opening browser failed with '${errorString(
848847
err
849848
)}'${extraErrorInfo()}`,
850849
{ cause: err, codeName: 'BrowserOpenFailedSpawnError' }

src/util.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,6 @@ export async function withAbortCheck<
4747
}
4848
}
4949

50-
export function errorString(err: unknown): string {
51-
return String(
52-
typeof err === 'object' && err && 'message' in err ? err.message : err
53-
);
54-
}
55-
5650
// AbortSignal.timeout, but consistently .unref()ed
5751
export function timeoutSignal(ms: number): AbortSignal {
5852
const controller = new AbortController();
@@ -127,15 +121,26 @@ export function validateSecureHTTPUrl(
127121
}
128122
}
129123

130-
export function messageFromError(err: unknown): string {
131-
return String(
132-
err &&
133-
typeof err === 'object' &&
134-
'message' in err &&
135-
typeof err.message === 'string'
136-
? err.message
137-
: err
138-
);
124+
export function errorString(err: unknown): string {
125+
if (
126+
!err ||
127+
typeof err !== 'object' ||
128+
!('message' in err) ||
129+
typeof err.message !== 'string'
130+
) {
131+
return String(err);
132+
}
133+
const cause = getCause(err);
134+
let { message } = err;
135+
if (cause) {
136+
const causeMessage = errorString(cause);
137+
if (
138+
!message.includes(causeMessage) &&
139+
!causeMessage.match(/^\[object.+\]$/i)
140+
)
141+
message += ` (caused by: ${causeMessage})`;
142+
}
143+
return message;
139144
}
140145

141146
const salt = randomBytes(16);

test/self-signed-cert.pem

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDgzCCAmsCFF3cqhUsBYLNh3bCVatENxZcoY7rMA0GCSqGSIb3DQEBCwUAMH0x
3+
CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTELMAkGA1UEBwwCU0YxDzANBgNVBAoM
4+
BkpveWVudDEQMA4GA1UECwwHTm9kZS5qczEPMA0GA1UEAwwGYWdlbnQyMSAwHgYJ
5+
KoZIhvcNAQkBFhFyeUB0aW55Y2xvdWRzLm9yZzAgFw0yMjA5MDMxNDQ2NTFaGA8y
6+
Mjk2MDYxNzE0NDY1MVowfTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYD
7+
VQQHDAJTRjEPMA0GA1UECgwGSm95ZW50MRAwDgYDVQQLDAdOb2RlLmpzMQ8wDQYD
8+
VQQDDAZhZ2VudDIxIDAeBgkqhkiG9w0BCQEWEXJ5QHRpbnljbG91ZHMub3JnMIIB
9+
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvqt/4KDehQLLDH+I2KXOxg4G
10+
WfNBISWmKlExPBfz9i1LY/rwoRwryv3Lpr40M05Dx+Rt4LMC+If7NGvrV8hdNSOz
11+
jW7P7R6upVdNXpeZDmHvhq+G/xv+x/Hdv3+Sdm/JC8TD2HRYcHSSWsirRbfA9eJe
12+
L0ADh1mJGNpWS+9FNXtbR3LRWsRwNjP1Lb39tXIsfHiWrJ/F6yAhWOU+ZZvvjazp
13+
bZX7Kes0lxVtyWCzLFpnzYa/gajGLdGJwTrfKXsSz2wk6szKlbO0mzX0aHviPRPT
14+
ftUVs91qORJ8tkAU4u78bpV0eCM8tVJh/N/oSm7ysLUjxhJrfNxHmmkGyaRL/QID
15+
AQABMA0GCSqGSIb3DQEBCwUAA4IBAQA+94z0pI0JEU1dX4bHGkhP6hwmv5tu7KlA
16+
R0hK33pF+boiagbySHrXW/y119VLp+o1FjuOlS4ETgAjcIjN2dDmJc0JEj6jnXyc
17+
4IYhRMDg4INAnmXX9bdCmpYuvhw/73cuxkdkMxH8p4O7v5HSqfpwjTEX8tWtpeMI
18+
IZ4+H/ddOKyvF3SO8lfrYJ7TXyypWfxzEiBuJnhZgpMG7zpZMGIzTkcN9VFTCv8d
19+
DCW0Lr2Ix/GY7nf/R9zDFnEZTW6IIkRp9UsUdOrgqgfSxp/C48foFv7gqMO/9PD8
20+
E8uE8986AFd5cK67imYPspHXv5UycySifwsSixi0hI9lDZqUIoWH
21+
-----END CERTIFICATE-----

test/self-signed-key.pem

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIIEpAIBAAKCAQEAvqt/4KDehQLLDH+I2KXOxg4GWfNBISWmKlExPBfz9i1LY/rw
3+
oRwryv3Lpr40M05Dx+Rt4LMC+If7NGvrV8hdNSOzjW7P7R6upVdNXpeZDmHvhq+G
4+
/xv+x/Hdv3+Sdm/JC8TD2HRYcHSSWsirRbfA9eJeL0ADh1mJGNpWS+9FNXtbR3LR
5+
WsRwNjP1Lb39tXIsfHiWrJ/F6yAhWOU+ZZvvjazpbZX7Kes0lxVtyWCzLFpnzYa/
6+
gajGLdGJwTrfKXsSz2wk6szKlbO0mzX0aHviPRPTftUVs91qORJ8tkAU4u78bpV0
7+
eCM8tVJh/N/oSm7ysLUjxhJrfNxHmmkGyaRL/QIDAQABAoIBAQCPnrT7KZGTVTBH
8+
IMWekv52ltfX53BWnHpWg8P3RP+hniqci8e3Q3YFODivR7QgNUK/DeRqDc0eEad5
9+
rBSgka8LuPGlhiOes67PojwIFV7Xw5Ndu1ePT7IRP7FNbrWO+tLQR41RvQlk45ne
10+
Qison6n8TF+vbaN6z0mCa+v21KsoBYxQHM7IJ6pgMxg24aNW0WTk7PXdJp1LWRiJ
11+
uixlXjOcKWQXaP+HxiQuXGj7isvv0O6xH2cl3GfgQ5rx8mG/APvLIz+dc/QBGQAr
12+
v6IVlDtd3AiYS7YeB7/5OvY+0emJ7U15ZJLNnCzlrqNDjxCN+cbXAeTdlKRJp21F
13+
rpjiZdfhAoGBAPw7EbWMq9ooluQ0bs+v6tvNCvXBfd/VAvG3w1/z3MvhVVYLx1Ag
14+
zleZom3YUXRv24WW3qHXFEGgyz2Sd3mJ4AuR9TDhvij6rHO6E0C8shB0oBlLoNo0
15+
4Ve28VQfaI77AKd7BYIoCWsCA5oTV+34AYlApMYkkaplRwSs9X5wIZ8ZAoGBAMGE
16+
7I1ASqMnQqdjzpBpGom+XpSPXGdBiH9mNPUajb0sPFvnnhpTSa3/k9m036Q9vQNH
17+
PEOeyjFbF7c/QKsPZLUbFl4uXdEmN4BUab5qQMSQVB9SlQOUB5G/qk/M90TgSbBm
18+
hFrpJrlf0Vsgnm1EMMOhoGdXbkB147AFnOcIekSFAoGAa31c0arOPd1YWI5Dvvxw
19+
MRWTmyHHW9EyPQKcH1MUgEpaDJ5eZTZl2Q0fHIK4S8+zlJ2z6PJ4rnMwyd+WTNRG
20+
B4g/HoLFgD87qOHefJMtqzeYVs9VEEjC05eiBsCP1YcAQ194/HvFb7XfBRVDPqWX
21+
Of+zeMFy1lPszQBMaoKswVkCgYAElrRNPSMH71xjP7icMAHTFlKDz0pvoFwuOSw0
22+
S6bkv3HG9B0JnsP2fkLxPJq4+EXNGBlTuSYuOWy8iaFs7PaEXNoQ7aSH2xIh1t6T
23+
B0312z5DZ9/kr9PmHtdZAREz7uWQaz3kMfcbGiyKrqFTEfTeDq0RBj+1A5aci+WG
24+
jOrpSQKBgQCvf/R/le3m8EsMe5AmNMl1mvpZ5wWn0yVS0vnjJRbS4TCUGK1lSf74
25+
tIZ+PEMp9CRaS2eTGtsWwQvuORlWlgYuYvJfxwvvnbLjln66SuE7pZHG2UILw4vZ
26+
5xkxTmL93VXFWRaH418mifGDiLIYr14+yzbW366r9L72BuN1dZJNzg==
27+
-----END RSA PRIVATE KEY-----

0 commit comments

Comments
 (0)