Skip to content

Commit a6826fb

Browse files
authored
fix: always send a nonce in the auth request MONGOSH-1905 (#195)
1 parent 3e77a74 commit a6826fb

File tree

6 files changed

+109
-46
lines changed

6 files changed

+109
-46
lines changed

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
"@mongodb-js/eslint-config-devtools": "^0.9.9",
5858
"@mongodb-js/mocha-config-devtools": "^1.0.0",
5959
"@mongodb-js/monorepo-tools": "^1.1.4",
60-
"@mongodb-js/oidc-mock-provider": "^0.8.0",
60+
"@mongodb-js/oidc-mock-provider": "^0.10.2",
6161
"@mongodb-js/prettier-config-devtools": "^1.0.1",
6262
"@mongodb-js/tsconfig-devtools": "^1.0.0",
6363
"@types/chai": "^4.2.21",

src/api.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,14 @@ export interface MongoDBOIDCPluginOptions {
197197
* broken identity providers.
198198
*/
199199
passIdTokenAsAccessToken?: boolean;
200+
201+
/**
202+
* Skip the nonce parameter in the Authorization Code request. This could
203+
* be used to work with providers that don't support the nonce parameter.
204+
*
205+
* Default is `false`.
206+
*/
207+
skipNonceInAuthCodeRequest?: boolean;
200208
}
201209

202210
/** @public */

src/plugin.spec.ts

Lines changed: 79 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,19 @@ async function delay(ms: number) {
7777
return await new Promise((resolve) => setTimeout(resolve, ms));
7878
}
7979

80+
function testAuthCodeFlow(
81+
fn: (opts: Partial<MongoDBOIDCPluginOptions>) => Mocha.Func
82+
): void {
83+
for (const skipNonceInAuthCodeRequest of [true, false]) {
84+
describe(`with skipNonceInAuthCodeRequest: ${skipNonceInAuthCodeRequest.toString()}`, function () {
85+
it(
86+
'can successfully authenticate with auth code flow',
87+
fn({ skipNonceInAuthCodeRequest })
88+
);
89+
});
90+
}
91+
}
92+
8093
describe('OIDC plugin (local OIDC provider)', function () {
8194
this.timeout(90_000);
8295

@@ -138,18 +151,42 @@ describe('OIDC plugin (local OIDC provider)', function () {
138151
plugin = createMongoDBOIDCPlugin(pluginOptions);
139152
});
140153

141-
it('can request tokens through the browser', async function () {
142-
const result = await requestToken(
143-
plugin,
144-
provider.getMongodbOIDCDBInfo()
145-
);
146-
const accessTokenContents = getJWTContents(result.accessToken);
147-
expect(accessTokenContents.sub).to.equal('testuser');
148-
expect(accessTokenContents.client_id).to.equal(
149-
provider.getMongodbOIDCDBInfo().clientId
150-
);
151-
verifySuccessfulAuthCodeFlowLog(await readLog());
152-
});
154+
testAuthCodeFlow(
155+
(opts) =>
156+
async function () {
157+
pluginOptions = {
158+
...pluginOptions,
159+
...opts,
160+
};
161+
plugin = createMongoDBOIDCPlugin(pluginOptions);
162+
let idToken: string | undefined;
163+
plugin.logger.once('mongodb-oidc-plugin:auth-succeeded', (event) => {
164+
idToken = event.tokens.idToken;
165+
});
166+
167+
const result = await requestToken(
168+
plugin,
169+
provider.getMongodbOIDCDBInfo()
170+
);
171+
const accessTokenContents = getJWTContents(result.accessToken);
172+
expect(accessTokenContents.sub).to.equal('testuser');
173+
expect(accessTokenContents.client_id).to.equal(
174+
provider.getMongodbOIDCDBInfo().clientId
175+
);
176+
177+
verifySuccessfulAuthCodeFlowLog(await readLog());
178+
179+
expect(idToken).to.not.be.undefined;
180+
181+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know it's non-null from the above check
182+
const idTokenContents = getJWTContents(idToken!);
183+
if (opts.skipNonceInAuthCodeRequest) {
184+
expect(idTokenContents.nonce).to.be.undefined;
185+
} else {
186+
expect(idTokenContents.nonce).to.not.be.undefined;
187+
}
188+
}
189+
);
153190

154191
it('will re-use tokens while they are valid if no username was provided', async function () {
155192
const skipAuthAttemptEvent = once(
@@ -1017,18 +1054,22 @@ describe('OIDC plugin (local OIDC provider)', function () {
10171054
};
10181055
});
10191056

1020-
it('can successfully authenticate with Okta using auth code flow', async function () {
1021-
plugin = createMongoDBOIDCPlugin({
1022-
...defaultOpts,
1023-
allowedFlows: ['auth-code'],
1024-
openBrowser: (opts) =>
1025-
oktaBrowserAuthCodeFlow({ ...opts, username, password }),
1026-
});
1027-
const result = await requestToken(plugin, metadata);
1057+
testAuthCodeFlow(
1058+
(opts) =>
1059+
async function () {
1060+
plugin = createMongoDBOIDCPlugin({
1061+
...defaultOpts,
1062+
allowedFlows: ['auth-code'],
1063+
openBrowser: (opts) =>
1064+
oktaBrowserAuthCodeFlow({ ...opts, username, password }),
1065+
...opts,
1066+
});
1067+
const result = await requestToken(plugin, metadata);
10281068

1029-
validateToken(getJWTContents(result.accessToken));
1030-
verifySuccessfulAuthCodeFlowLog(await readLog());
1031-
});
1069+
validateToken(getJWTContents(result.accessToken));
1070+
verifySuccessfulAuthCodeFlowLog(await readLog());
1071+
}
1072+
);
10321073

10331074
it('can successfully authenticate with Okta using device auth flow', async function () {
10341075
plugin = createMongoDBOIDCPlugin({
@@ -1087,18 +1128,22 @@ describe('OIDC plugin (local OIDC provider)', function () {
10871128
};
10881129
});
10891130

1090-
it('can successfully authenticate with Azure using auth code flow', async function () {
1091-
plugin = createMongoDBOIDCPlugin({
1092-
...defaultOpts,
1093-
allowedFlows: ['auth-code'],
1094-
openBrowser: (opts) =>
1095-
azureBrowserAuthCodeFlow({ ...opts, username, password }),
1096-
});
1097-
const result = await requestToken(plugin, metadata);
1131+
testAuthCodeFlow(
1132+
(opts) =>
1133+
async function () {
1134+
plugin = createMongoDBOIDCPlugin({
1135+
...defaultOpts,
1136+
allowedFlows: ['auth-code'],
1137+
openBrowser: (opts) =>
1138+
azureBrowserAuthCodeFlow({ ...opts, username, password }),
1139+
...opts,
1140+
});
1141+
const result = await requestToken(plugin, metadata);
10981142

1099-
validateToken(getJWTContents(result.accessToken));
1100-
verifySuccessfulAuthCodeFlowLog(await readLog());
1101-
});
1143+
validateToken(getJWTContents(result.accessToken));
1144+
verifySuccessfulAuthCodeFlowLog(await readLog());
1145+
}
1146+
);
11021147

11031148
it('can successfully authenticate with Azure using device auth flow', async function () {
11041149
plugin = createMongoDBOIDCPlugin({

src/plugin.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,10 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
657657
let client!: BaseClient;
658658
let actualRedirectURI!: string;
659659

660+
const nonce = this.options.skipNonceInAuthCodeRequest
661+
? undefined
662+
: generators.nonce();
663+
660664
try {
661665
await withAbortCheck(signal, async ({ signalCheck, signalPromise }) => {
662666
// We mark the operations that we want to allow to result in a fallback
@@ -680,6 +684,7 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
680684
code_challenge: codeChallenge,
681685
code_challenge_method: 'S256',
682686
state: oidcStateParam,
687+
nonce,
683688
});
684689
validateSecureHTTPUrl(authCodeFlowUrl, 'authCodeFlowUrl');
685690
const { localUrl, onAccessed: onLocalUrlAccessed } =
@@ -760,6 +765,7 @@ export class MongoDBOIDCPluginImpl implements MongoDBOIDCPlugin {
760765
const tokenSet = await client.callback(actualRedirectURI, params, {
761766
code_verifier: codeVerifier,
762767
state: oidcStateParam,
768+
nonce,
763769
});
764770
this.updateStateWithTokenSet(state, tokenSet);
765771
}

test/oidc-test-provider.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -282,10 +282,13 @@ async function waitForTitle(
282282
): Promise<void> {
283283
await browser.waitUntil(async () => {
284284
const actual = (await browser.$(selector).getText()).trim();
285-
let matches;
286-
if (typeof expected === 'string')
285+
let matches: boolean;
286+
if (typeof expected === 'string') {
287287
matches = actual.toLowerCase() === expected.toLowerCase();
288-
else matches = expected.test(actual);
288+
} else {
289+
matches = expected.test(actual);
290+
}
291+
289292
if (!matches) {
290293
throw new Error(`Wanted title "${String(expected)}", saw "${actual}"`);
291294
}
@@ -481,7 +484,7 @@ export async function azureBrowserDeviceAuthFlow({
481484
try {
482485
const normalizeUserCode = (str: string) => str.replace(/-/g, '');
483486
browser = await spawnBrowser(verificationUrl, true);
484-
await waitForTitle(browser, 'Enter code', 'div[role="heading"]');
487+
await waitForTitle(browser, /Enter code/, 'div[role="heading"]');
485488
await ensureValue(
486489
browser,
487490
'input[name="otc"]',

0 commit comments

Comments
 (0)