Skip to content

Commit 781d1c9

Browse files
authored
feat: add debug flag for dumping OIDC tokens to output MONGOSH-1845 (#2112)
1 parent 975dd26 commit 781d1c9

File tree

9 files changed

+199
-10
lines changed

9 files changed

+199
-10
lines changed

package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/arg-parser/src/cli-options.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,6 @@ export interface CliOptions {
5656
oidcRedirectUri?: string;
5757
oidcTrustedEndpoint?: boolean;
5858
oidcIdTokenAsAccessToken?: boolean;
59+
oidcDumpTokens?: boolean | 'redacted' | 'include-secrets';
5960
browser?: string | false;
6061
}

packages/cli-repl/src/arg-parser.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ const OPTIONS = {
9191
'build-info': 'buildInfo',
9292
json: 'json', // List explicitly here since it can be a boolean or a string
9393
browser: 'browser', // ditto
94+
oidcDumpTokens: 'oidcDumpTokens', // ditto
9495
oidcRedirectUrl: 'oidcRedirectUri', // I'd get this wrong about 50% of the time
9596
oidcIDTokenAsAccessToken: 'oidcIdTokenAsAccessToken', // ditto
9697
},
@@ -215,6 +216,17 @@ export function verifyCliArguments(args: any /* CliOptions */): string[] {
215216
);
216217
}
217218

219+
if (
220+
![undefined, true, false, 'redacted', 'include-secrets'].includes(
221+
args.oidcDumpTokens
222+
)
223+
) {
224+
throw new MongoshUnimplementedError(
225+
'--oidcDumpTokens can only have the values redacted or include-secrets',
226+
CommonErrors.InvalidArgument
227+
);
228+
}
229+
218230
const messages = [];
219231
for (const deprecated in DEPRECATED_ARGS_WITH_REPLACEMENT) {
220232
if (deprecated in args) {

packages/cli-repl/src/cli-repl.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import path from 'path';
4747
import { getOsInfo } from './get-os-info';
4848
import { UpdateNotificationManager } from './update-notification-manager';
4949
import { getTimingData, markTime, summariseTimingData } from './startup-timing';
50+
import type { IdPInfo } from 'mongodb';
5051

5152
/**
5253
* Connecting text key.
@@ -204,6 +205,8 @@ export class CliRepl implements MongoshIOProvider {
204205
bus: this.bus,
205206
ioProvider: this,
206207
});
208+
209+
this.setupOIDCTokenDumpListener();
207210
}
208211

209212
async getIsContainerizedEnvironment() {
@@ -1254,4 +1257,100 @@ export class CliRepl implements MongoshIOProvider {
12541257
version
12551258
);
12561259
}
1260+
1261+
private setupOIDCTokenDumpListener() {
1262+
function tryParseJWT(
1263+
token: string | null | undefined,
1264+
redact: 'redact' | 'include-secrets'
1265+
): unknown {
1266+
if (!token) return token;
1267+
const jwtParts = token.split('.');
1268+
if (
1269+
// If this is a three-part token consisting of valid base64url-encoded
1270+
// parts (without trailing `=`), assume that it is a JWT access/id token.
1271+
jwtParts.length === 3 &&
1272+
jwtParts.every(
1273+
(part) =>
1274+
Buffer.from(part, 'base64url')
1275+
.toString('base64url')
1276+
.replace(/=+$/, '') === part.replace(/=+$/, '')
1277+
)
1278+
) {
1279+
const [header, payload] = jwtParts.map((part) => {
1280+
try {
1281+
return JSON.parse(Buffer.from(part, 'base64url').toString('utf8'));
1282+
} catch {
1283+
// Not a valid JWT in this case.
1284+
}
1285+
});
1286+
if (redact === 'include-secrets') {
1287+
return { header, payload, signature: jwtParts[2] };
1288+
}
1289+
if (header && payload) {
1290+
return { header, payload };
1291+
}
1292+
}
1293+
return redact === 'include-secrets' ? token : '<non-JWT token>';
1294+
}
1295+
1296+
let lastServerIdPInfo: IdPInfo | undefined;
1297+
const { oidcDumpTokens } = this.cliOptions;
1298+
if (oidcDumpTokens) {
1299+
this.bus.on(
1300+
'mongodb-oidc-plugin:received-server-params',
1301+
({ params: { idpInfo } }) => {
1302+
lastServerIdPInfo = idpInfo;
1303+
}
1304+
);
1305+
this.bus.on(
1306+
'mongodb-oidc-plugin:auth-succeeded',
1307+
({
1308+
tokenType,
1309+
refreshToken, // only an identifier, not the actual token
1310+
expiresAt,
1311+
passIdTokenAsAccessToken,
1312+
tokens: { accessToken: at, refreshToken: rt, idToken: idt },
1313+
}) => {
1314+
const printable = {
1315+
lastServerIdPInfo: lastServerIdPInfo && {
1316+
issuer: lastServerIdPInfo?.issuer,
1317+
clientId: lastServerIdPInfo?.clientId,
1318+
requestScopes: lastServerIdPInfo?.requestScopes,
1319+
},
1320+
tokenType,
1321+
refreshToken,
1322+
expiresAt,
1323+
passIdTokenAsAccessToken,
1324+
tokens:
1325+
oidcDumpTokens === 'include-secrets'
1326+
? {
1327+
accessToken: tryParseJWT(at, 'include-secrets'),
1328+
refreshToken: tryParseJWT(rt, 'include-secrets'),
1329+
idToken: tryParseJWT(idt, 'include-secrets'),
1330+
}
1331+
: {
1332+
accessToken: tryParseJWT(at, 'redact'),
1333+
idToken: tryParseJWT(idt, 'redact'),
1334+
},
1335+
};
1336+
1337+
this.output.write(
1338+
'\n' +
1339+
this.clr(
1340+
'----- BEGIN OIDC TOKEN DUMP -----',
1341+
'mongosh:section-header'
1342+
) +
1343+
'\n' +
1344+
JSON.stringify(printable, null, 2) +
1345+
'\n' +
1346+
this.clr(
1347+
'----- END OIDC TOKEN DUMP -----',
1348+
'mongosh:section-header'
1349+
) +
1350+
'\n'
1351+
);
1352+
}
1353+
);
1354+
}
1355+
}
12571356
}

packages/cli-repl/src/constants.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,30 @@ export const USAGE = `
138138
'cli-repl.args.kmsURL'
139139
)}
140140
141+
oidcFlows?: string;
142+
oidcRedirectUri?: string;
143+
oidcTrustedEndpoint?: boolean;
144+
oidcIdTokenAsAccessToken?: boolean;
145+
oidcDumpTokens?: boolean | 'redacted' | 'include-secrets';
146+
147+
${clr(i18n.__('cli-repl.args.oidcOptions'), 'mongosh:section-header')}
148+
149+
--oidcFlows[=auth-code,device-auth] ${i18n.__(
150+
'cli-repl.args.oidcFlows'
151+
)}
152+
--oidcRedirectUri[=url] ${i18n.__(
153+
'cli-repl.args.oidcRedirectUri'
154+
)}
155+
--oidcTrustedEndpoint ${i18n.__(
156+
'cli-repl.args.oidcTrustedEndpoint'
157+
)}
158+
--oidcIdTokenAsAccessToken ${i18n.__(
159+
'cli-repl.args.oidcIdTokenAsAccessToken'
160+
)}
161+
--oidcDumpTokens[=mode] ${i18n.__(
162+
'cli-repl.args.oidcDumpTokens'
163+
)}
164+
141165
${clr(i18n.__('cli-repl.args.dbAddressOptions'), 'mongosh:section-header')}
142166
143167
foo ${i18n.__(

packages/e2e-tests/test/e2e-oidc.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,4 +478,46 @@ describe('OIDC auth e2e', function () {
478478
await verifyUser(shell, 'testuser-id', 'testuser-id-group');
479479
shell.assertNoErrors();
480480
});
481+
482+
it('can print tokens as debug information if requested', async function () {
483+
shell = TestShell.start({
484+
args: [
485+
await testServer.connectionString(),
486+
'--authenticationMechanism=MONGODB-OIDC',
487+
'--oidcRedirectUri=http://localhost:0/',
488+
'--oidcDumpTokens',
489+
`--browser=${fetchBrowserFixture}`,
490+
'--eval=42',
491+
],
492+
});
493+
await shell.waitForExit();
494+
495+
shell.assertContainsOutput('BEGIN OIDC TOKEN DUMP');
496+
shell.assertContainsOutput('"tokenType": "Bearer"');
497+
shell.assertContainsOutput('"alg": "RS256"');
498+
shell.assertContainsOutput('"sub": "testuser"');
499+
shell.assertNotContainsOutput('"signature":');
500+
shell.assertContainsOutput('"lastServerIdPInfo":');
501+
shell.assertNotContainsOutput(/"refreshToken": "(?!debugid:)/);
502+
503+
shell = TestShell.start({
504+
args: [
505+
await testServer.connectionString(),
506+
'--authenticationMechanism=MONGODB-OIDC',
507+
'--oidcRedirectUri=http://localhost:0/',
508+
'--oidcDumpTokens=include-secrets',
509+
`--browser=${fetchBrowserFixture}`,
510+
'--eval=42',
511+
],
512+
});
513+
await shell.waitForExit();
514+
515+
shell.assertContainsOutput('BEGIN OIDC TOKEN DUMP');
516+
shell.assertContainsOutput('"tokenType": "Bearer"');
517+
shell.assertContainsOutput('"alg": "RS256"');
518+
shell.assertContainsOutput('"sub": "testuser"');
519+
shell.assertContainsOutput('"signature":');
520+
shell.assertContainsOutput('"lastServerIdPInfo":');
521+
shell.assertContainsOutput(/"refreshToken": "(?!debugid:)/);
522+
});
481523
});

packages/i18n/src/locales/en_US.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable filename-rules/match */
12
import type Catalog from '../catalog';
23

34
/**
@@ -82,6 +83,16 @@ const translations: Catalog = {
8283
connectionExampleWithDatabase:
8384
"Start mongosh using 'ships' database on specified connection string:",
8485
moreInformation: 'For more information on usage:',
86+
oidcOptions: 'OIDC auth options:',
87+
oidcFlows: 'Supported OIDC auth flows',
88+
oidcRedirectUri:
89+
'Local auth code flow redirect URL [http://localhost:27097/redirect]',
90+
oidcTrustedEndpoint:
91+
'Treat the cluster/database mongosh as a trusted endpoint',
92+
oidcIdTokenAsAccessToken:
93+
'Use ID tokens in place of access tokens for auth',
94+
oidcDumpTokens:
95+
"Debug OIDC by printing tokens to mongosh's output [full|include-secrets]",
8596
},
8697
'arg-parser': {
8798
'unknown-option': 'Error parsing command line: unrecognized option:',

packages/service-provider-server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
},
4949
"dependencies": {
5050
"@mongodb-js/devtools-connect": "^3.0.5",
51-
"@mongodb-js/oidc-plugin": "^1.1.0",
51+
"@mongodb-js/oidc-plugin": "^1.1.1",
5252
"@mongosh/errors": "0.0.0-dev.0",
5353
"@mongosh/service-provider-core": "0.0.0-dev.0",
5454
"@mongosh/types": "0.0.0-dev.0",

packages/service-provider-server/src/cli-service-provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ const DEFAULT_BASE_OPTIONS: OperationOptions = Object.freeze({
147147

148148
/**
149149
* Pick properties of `uri` and `opts` that as a tuple that can be matched
150-
* against the correspondiung tuple for another `uri` and `opts` configuration,
150+
* against the corresponding tuple for another `uri` and `opts` configuration,
151151
* and when they do, it is meaningful to share connection state between them.
152152
*
153153
* Currently, this is only used for OIDC. We don't need to make sure that the

0 commit comments

Comments
 (0)