From 51532f26e5159e18a2dc7ac8b73f1094bcbd773f Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 12 Apr 2023 16:18:34 +0200 Subject: [PATCH 01/93] feat(NODE-5191): update oidc objects --- src/cmap/auth/mongo_credentials.ts | 2 ++ src/cmap/auth/mongodb_oidc.ts | 27 +++++++++++-------- .../auth/mongodb_oidc/callback_workflow.ts | 15 +++++------ src/index.ts | 1 + 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/cmap/auth/mongo_credentials.ts b/src/cmap/auth/mongo_credentials.ts index b24c395fcc5..c378606d3b1 100644 --- a/src/cmap/auth/mongo_credentials.ts +++ b/src/cmap/auth/mongo_credentials.ts @@ -43,6 +43,8 @@ export interface AuthMechanismProperties extends Document { REFRESH_TOKEN_CALLBACK?: OIDCRefreshFunction; /** @experimental */ PROVIDER_NAME?: 'aws'; + /** @experimental */ + ALLOWED_HOSTS?: string[]; } /** @public */ diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index 69ae3e0d3b3..71ea06395c1 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -11,11 +11,8 @@ import type { Workflow } from './mongodb_oidc/workflow'; * @experimental */ export interface OIDCMechanismServerStep1 { - authorizationEndpoint?: string; - tokenEndpoint?: string; - deviceAuthorizationEndpoint?: string; + issuer?: string; clientId: string; - clientSecret?: string; requestScopes?: string[]; } @@ -29,14 +26,23 @@ export interface OIDCRequestTokenResult { refreshToken?: string; } +/** + * @public + * @experimental + */ +export interface OIDCClientInfo { + principalName: string; + timeoutSeconds?: number; + timeoutContext?: AbortSignal; +} + /** * @public * @experimental */ export type OIDCRequestFunction = ( - principalName: string, - serverResult: OIDCMechanismServerStep1, - timeout: AbortSignal | number + clientInfo: OIDCClientInfo, + serverInfo: OIDCMechanismServerStep1 ) => Promise; /** @@ -44,10 +50,9 @@ export type OIDCRequestFunction = ( * @experimental */ export type OIDCRefreshFunction = ( - principalName: string, - serverResult: OIDCMechanismServerStep1, - result: OIDCRequestTokenResult, - timeout: AbortSignal | number + clientInfo: OIDCClientInfo, + serverInfo: OIDCMechanismServerStep1, + tokenResult: OIDCRequestTokenResult ) => Promise; type ProviderName = 'aws' | 'callback'; diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index ffebe7e49bd..73de7041ab7 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -9,8 +9,8 @@ import { AuthMechanism } from '../providers'; import { TokenEntryCache } from './token_entry_cache'; import type { Workflow } from './workflow'; -/* 5 minutes in milliseconds */ -const TIMEOUT_MS = 300000; +/* 5 minutes in seconds */ +const TIMEOUT_S = 300; /** * OIDC implementation of a callback based workflow. @@ -134,12 +134,8 @@ export class CallbackWorkflow implements Workflow { const refresh = credentials.mechanismProperties.REFRESH_TOKEN_CALLBACK; // If a refresh callback exists, use it. Otherwise use the request callback. if (refresh) { - const result: OIDCRequestTokenResult = await refresh( - credentials.username, - stepOneResult, - tokenResult, - TIMEOUT_MS - ); + const clientInfo = { principalName: credentials.username, timeoutSeconds: TIMEOUT_S }; + const result: OIDCRequestTokenResult = await refresh(clientInfo, stepOneResult, tokenResult); // Validate the result. if (!result || !result.accessToken) { throw new MongoMissingCredentialsError( @@ -182,7 +178,8 @@ export class CallbackWorkflow implements Workflow { 'Auth mechanism property REQUEST_TOKEN_CALLBACK is required.' ); } - const tokenResult = await request(credentials.username, stepOneResult, TIMEOUT_MS); + const clientInfo = { principalName: credentials.username, timeoutSeconds: TIMEOUT_S }; + const tokenResult = await request(clientInfo, stepOneResult); // Validate the result. if (!tokenResult || !tokenResult.accessToken) { throw new MongoMissingCredentialsError( diff --git a/src/index.ts b/src/index.ts index a0d67a322c2..f17f832c1f4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -204,6 +204,7 @@ export type { MongoCredentialsOptions } from './cmap/auth/mongo_credentials'; export type { + OIDCClientInfo, OIDCMechanismServerStep1, OIDCRefreshFunction, OIDCRequestFunction, From 08823d137dc69494d4f9e68a0c0b42a87ae8cdd4 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 13 Apr 2023 14:11:08 +0200 Subject: [PATCH 02/93] feat(NODE-5191): add allowed hosts option --- src/cmap/auth/mongo_credentials.ts | 29 +++++++++++++ src/connection_string.ts | 11 +++++ src/utils.ts | 11 +++++ test/unit/connection_string.test.ts | 67 +++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+) diff --git a/src/cmap/auth/mongo_credentials.ts b/src/cmap/auth/mongo_credentials.ts index c378606d3b1..85f42f45a20 100644 --- a/src/cmap/auth/mongo_credentials.ts +++ b/src/cmap/auth/mongo_credentials.ts @@ -5,6 +5,7 @@ import { MongoInvalidArgumentError, MongoMissingCredentialsError } from '../../error'; +import { isString } from '../../utils'; import { GSSAPICanonicalizationValue } from './gssapi'; import type { OIDCRefreshFunction, OIDCRequestFunction } from './mongodb_oidc'; import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism } from './providers'; @@ -30,6 +31,18 @@ function getDefaultAuthMechanism(hello?: Document): AuthMechanism { return AuthMechanism.MONGODB_CR; } +const ALLOWED_HOSTS_ERROR = 'Auth mechanism property ALLOWED_HOSTS must be an array of strings.'; + +/** @internal */ +export const DEFAULT_ALLOWED_HOSTS = [ + '*.mongodb.net', + '*.mongodb-dev.net', + '*.mongodbgov.net', + 'localhost', + '127.0.0.1', + '::1' +]; + /** @public */ export interface AuthMechanismProperties extends Document { SERVICE_HOST?: string; @@ -103,6 +116,10 @@ export class MongoCredentials { } } + if (this.mechanism === AuthMechanism.MONGODB_OIDC && !this.mechanismProperties.ALLOWED_HOSTS) { + this.mechanismProperties.ALLOWED_HOSTS = DEFAULT_ALLOWED_HOSTS; + } + Object.freeze(this.mechanismProperties); Object.freeze(this); } @@ -183,6 +200,18 @@ export class MongoCredentials { `Either a PROVIDER_NAME or a REQUEST_TOKEN_CALLBACK must be specified for mechanism '${this.mechanism}'.` ); } + + if (this.mechanismProperties.ALLOWED_HOSTS) { + const hosts = this.mechanismProperties.ALLOWED_HOSTS; + if (!Array.isArray(hosts)) { + throw new MongoInvalidArgumentError(ALLOWED_HOSTS_ERROR); + } + for (const host of hosts) { + if (!isString(host)) { + throw new MongoInvalidArgumentError(ALLOWED_HOSTS_ERROR); + } + } + } } if (AUTH_MECHS_AUTH_SRC_EXTERNAL.has(this.mechanism)) { diff --git a/src/connection_string.ts b/src/connection_string.ts index 9c6348c7988..21ce5aaa22d 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -310,6 +310,17 @@ export function parseOptions( ); } + const uriMechanismProperties = urlOptions.get('authMechanismProperties'); + if (uriMechanismProperties) { + for (const property of uriMechanismProperties) { + if (property.includes('ALLOWED_HOSTS:')) { + throw new MongoParseError( + 'Auth mechanism property ALLOWED_HOSTS is not allowed in the connection string.' + ); + } + } + } + if (objectOptions.has('loadBalanced')) { throw new MongoParseError('loadBalanced is only a valid option in the URI'); } diff --git a/src/utils.ts b/src/utils.ts index 95bf757af2d..112c153cc06 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -30,6 +30,8 @@ import type { Topology } from './sdam/topology'; import type { ClientSession } from './sessions'; import { WriteConcern } from './write_concern'; +const OBJECT_STRING = '[object String]'; + /** * MongoDB Driver style callback * @public @@ -58,6 +60,15 @@ export const ByteUtils = { } }; +/** + * Use this to test if an object is a string. This is because + * typeof new String('test') is 'object' and not 'string'. + * @internal + */ +export function isString(value: any): boolean { + return Object.prototype.toString.call(value) === OBJECT_STRING; +} + /** * Throws if collectionName is not a valid mongodb collection namespace. * @internal diff --git a/test/unit/connection_string.test.ts b/test/unit/connection_string.test.ts index 0f947d89591..eb765a81866 100644 --- a/test/unit/connection_string.test.ts +++ b/test/unit/connection_string.test.ts @@ -5,6 +5,7 @@ import * as sinon from 'sinon'; import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism, + DEFAULT_ALLOWED_HOSTS, FEATURE_FLAGS, MongoAPIError, MongoClient, @@ -211,6 +212,72 @@ describe('Connection String', function () { expect(options.readConcern.level).to.equal('local'); }); + context('when auth mechanism is MONGODB-OIDC', function () { + context('when ALLOWED_HOSTS is in the URI', function () { + it('raises an error', function () { + expect(() => { + parseOptions( + 'mongodb://localhost/?authMechanismProperties=PROVIDER_NAME:aws,ALLOWED_HOSTS:[localhost]&authMechanism=MONGODB-OIDC' + ); + }).to.throw( + MongoParseError, + 'Auth mechanism property ALLOWED_HOSTS is not allowed in the connection string.' + ); + }); + }); + + context('when ALLOWED_HOSTS is in the options', function () { + context('when it is an array of strings', function () { + const hosts = ['*.example.com']; + + it('sets the allowed hosts property', function () { + const options = parseOptions( + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws', + { + authMechanismProperties: { + ALLOWED_HOSTS: hosts + } + } + ); + expect(options.credentials.mechanismProperties).to.deep.equal({ + PROVIDER_NAME: 'aws', + ALLOWED_HOSTS: hosts + }); + }); + }); + + context('when it is not an array of strings', function () { + it('raises an error', function () { + expect(() => { + parseOptions( + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws', + { + authMechanismProperties: { + ALLOWED_HOSTS: [1, 2, 3] + } + } + ); + }).to.throw( + MongoInvalidArgumentError, + 'Auth mechanism property ALLOWED_HOSTS must be an array of strings.' + ); + }); + }); + }); + + context('when ALLOWED_HOSTS is not in the options', function () { + it('sets the default value', function () { + const options = parseOptions( + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws' + ); + expect(options.credentials.mechanismProperties).to.deep.equal({ + PROVIDER_NAME: 'aws', + ALLOWED_HOSTS: DEFAULT_ALLOWED_HOSTS + }); + }); + }); + }); + it('should parse `authMechanismProperties`', function () { const options = parseOptions( 'mongodb://user%40EXAMPLE.COM:secret@localhost/?authMechanismProperties=SERVICE_NAME:other,SERVICE_REALM:blah,CANONICALIZE_HOST_NAME:true,SERVICE_HOST:example.com&authMechanism=GSSAPI' From a943d7bc1d6f13e0a7348ad3d0ab81a624a414a3 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 13 Apr 2023 15:41:25 +0200 Subject: [PATCH 03/93] test(NODE-5191): add prose spec template --- test/manual/mongodb_oidc.prose.test.ts | 826 ++++++++----------------- 1 file changed, 262 insertions(+), 564 deletions(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index ce5728e597b..d8ffc4806ad 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -12,604 +12,302 @@ describe('MONGODB-OIDC', function () { }); describe('OIDC Auth Spec Prose Tests', function () { - // Drivers MUST be able to authenticate using OIDC callback(s) when there - // is one principal configured. describe('1. Callback-Driven Auth', function () { - // - Create a request callback that reads in the generated ``test_user1`` token file. - const requestCallback = async () => { - const token = await readFile(`${process.env.OIDC_TOKEN_DIR}/test_user1`, { - encoding: 'utf8' - }); - return { accessToken: token }; - }; - - context('when no username is provided', function () { - let client; - let collection; - // - Create a client with a url of the form ``mongodb://localhost/?authMechanism=MONGODB-OIDC`` - // and the OIDC request callback. - before(function () { - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: requestCallback - } - }); - collection = client.db('test').collection('test'); - }); - - after(async () => { - await client?.close(); - }); - - // - Perform a ``find`` operation. - // - Clear the cache. - it('successfully authenticates', async function () { - const doc = await collection.findOne(); - expect(doc).to.equal(null); - }); - }); - - context('when a username is provided', function () { - let client; - let collection; - // - Create a client with a url of the form - // ``mongodb://test_user1@localhost/?authMechanism=MONGODB-OIDC`` and the OIDC request callback. - before(function () { - client = new MongoClient('mongodb://test_user1@localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: requestCallback - } - }); - collection = client.db('test').collection('test'); - }); - - after(async () => { - await client?.close(); - }); - - // - Perform a ``find`` operation. - // - Clear the cache. - it('successfully authenticates', async function () { - const doc = await collection.findOne(); - expect(doc).to.equal(null); - }); - }); - }); - - // Drivers MUST be able to authenticate using the "aws" device workflow simulating - // an EC2 instance with an enabled web identity token provider, generated by - // Drivers Evergreen Tools. - describe('2. AWS Device Auth', function () { - const testTokenFile = process.env.AWS_WEB_IDENTITY_TOKEN_FILE; - let client; - let collection; - after(() => { - process.env.AWS_WEB_IDENTITY_TOKEN_FILE = testTokenFile; - client?.close(); + describe('1.1 Single Principal Implicit Username', function () { + // Clear the cache. + // Create a request callback returns a valid token. + // Create a client that uses the default OIDC url and the request callback. + // Perform a find operation. that succeeds. + // Close the client. }); - // - Create a client with the url parameters - // ``?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME=aws``. - before(function () { - // Set the ``AWS_WEB_IDENTITY_TOKEN_FILE`` environment variable to the location - // of the ``test_user1`` generated token file. - process.env.AWS_WEB_IDENTITY_TOKEN_FILE = `${process.env.OIDC_TOKEN_DIR}/test_user1`; - client = new MongoClient( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws' - ); - collection = client.db('test').collection('test'); + describe('1.2 Single Principal Explicit Username', function () { + // Clear the cache. + // Create a request callback that returns a valid token. + // Create a client with a url of the form mongodb://test_user1@localhost/?authMechanism=MONGODB-OIDC and the OIDC request callback. + // Perform a find operation that succeeds. + // Close the client. }); - // - Perform a find operation on the client. - it('successfully authenticates', async function () { - const doc = await collection.findOne(); - expect(doc).to.equal(null); + describe('1.3 Multiple Principal User 1', function () { + // Clear the cache. + // Create a request callback that returns a valid token. + // Create a client with a url of the form mongodb://test_user1@localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred and a valid OIDC request callback. + // Perform a find operation that succeeds. + // Close the client. }); - }); - - // Drivers MUST be able to authenticate using either authentication or device - // type if there are multiple principals configured on the server. Note that - // ``directConnection=true`` and ``readPreference=secondaryPreferred`` are needed - // because the server is a secondary on a replica set, on port ``27018``. - describe('3. Multiple Principals', function () { - context('when authenticating with user 1', function () { - context('when using a callback', function () { - let client; - let collection; - // - Create a request callback that reads in the generated ``test_user1`` token file. - const requestCallback = async () => { - const token = await readFile(`${process.env.OIDC_TOKEN_DIR}/test_user1`, { - encoding: 'utf8' - }); - return { accessToken: token }; - }; - // - Create a client with a url of the form - // ``mongodb://test_user1@localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred`` - // and the OIDC request callback. - before(function () { - client = new MongoClient( - 'mongodb://test_user1@localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred', - { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: requestCallback - } - } - ); - collection = client.db('test').collection('test'); - }); - - after(async () => { - client?.close(); - }); - - // - Perform a ``find`` operation. - // - Clear the cache. - it('successfully authenticates', async function () { - const doc = await collection.findOne(); - expect(doc).to.equal(null); - }); - }); - - context('when using aws', function () { - const testTokenFile = process.env.AWS_WEB_IDENTITY_TOKEN_FILE; - let client; - let collection; - after(async () => { - process.env.AWS_WEB_IDENTITY_TOKEN_FILE = testTokenFile; - client?.close(); - }); - - before(async () => { - // - Set the ``AWS_WEB_IDENTITY_TOKEN_FILE`` environment variable to the location - // of the ``test_user1`` generated token file. - process.env.AWS_WEB_IDENTITY_TOKEN_FILE = `${process.env.OIDC_TOKEN_DIR}/test_user1`; - // - Create a client with a url of the form - // ``mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred``. - client = new MongoClient( - 'mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred' - ); - collection = client.db('test').collection('test'); - }); - - // - Perform a ``find`` operation. - it('successfully authenticates', async function () { - const doc = await collection.findOne(); - expect(doc).to.equal(null); - }); - }); + describe('1.4 Multiple Principal User 2', function () { + // Clear the cache. + // Create a request callback that reads in the generated test_user2 token file. + // Create a client with a url of the form mongodb://test_user2@localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred and a valid OIDC request callback. + // Perform a find operation that succeeds. + // Close the client. }); - context('when authenticating with user 2', function () { - context('when using a callback', function () { - let client; - let collection; - // - Create a request callback that reads in the generated ``test_user2`` token file. - const requestCallback = async () => { - const token = await readFile(`${process.env.OIDC_TOKEN_DIR}/test_user2`, { - encoding: 'utf8' - }); - return { accessToken: token }; - }; - // - Create a client with a url of the form - // ``mongodb://test_user2@localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred`` - // and the OIDC request callback. - before(function () { - client = new MongoClient( - 'mongodb://test_user2@localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred', - { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: requestCallback - } - } - ); - collection = client.db('test').collection('test'); - }); - - after(async () => { - client?.close(); - }); - - // - Perform a ``find`` operation. - // - Clear the cache. - it('successfully authenticates', async function () { - const doc = await collection.findOne(); - expect(doc).to.equal(null); - }); - }); - - context('when using aws', function () { - let client; - let collection; - const testTokenFile = process.env.AWS_WEB_IDENTITY_TOKEN_FILE; - - after(async () => { - process.env.AWS_WEB_IDENTITY_TOKEN_FILE = testTokenFile; - client?.close(); - }); - - before(async () => { - // - Set the ``AWS_WEB_IDENTITY_TOKEN_FILE`` environment variable to the location - // of the ``test_user2`` generated token file. - process.env.AWS_WEB_IDENTITY_TOKEN_FILE = `${process.env.OIDC_TOKEN_DIR}/test_user2`; - // - Create a client with a url of the form - // ``mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred``. - client = new MongoClient( - 'mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred' - ); - collection = client.db('test').collection('test'); - }); - - // - Perform a ``find`` operation. - it('successfully authenticates', async function () { - const doc = await collection.findOne(); - expect(doc).to.equal(null); - }); - }); + describe('1.5 Multiple Principal No User', function () { + // Clear the cache. + // Create a client with a url of the form mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred and a valid OIDC request callback. + // Assert that a find operation fails. + // Close the client. }); - context('when not providing a user', function () { - it('fails on option parsing', function () { - expect(() => { - new MongoClient( - 'mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred' - ); - }).to.throw(MongoInvalidArgumentError, /PROVIDER_NAME|REQUEST_TOKEN_CALLBACK/); - }); + describe('1.6 Allowed Hosts Blocked', function () { + // Clear the cache. + // Create a client that uses the OIDC url and a request callback, and an ALLOWED_HOSTS that is an empty list. + // Assert that a find operation fails with a client-side error. + // Close the client. + // Create a client that uses the OIDC url and a request callback, and an ALLOWED_HOSTS that contains ["localhost1"]. + // Assert that a find operation fails with a client-side error. + // Close the client. }); }); + }); - describe('4. Invalid Callbacks', function () { - // - Any callback returns null - context('when the callback returns null', function () { - let client; - const requestCallback = async () => { - return null; - }; - - before(function () { - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: requestCallback - } - }); - }); - - after(async () => { - client?.close(); - }); - - it('raises an error', async function () { - try { - await client.connect(); - fail('Invalid request callbacks must throw on connect'); - } catch (error) { - expect(error.message).to.include('REQUEST_TOKEN_CALLBACK'); - } - }); - }); - - // - Any callback returns unexpected result - context('then the callback returns an unexpected result', function () { - let client; - const requestCallback = async () => { - return { unexpected: 'test' }; - }; - - before(function () { - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: requestCallback - } - }); - }); - - after(async () => { - client?.close(); - }); - - it('raises an error', async function () { - try { - await client.connect(); - fail('Invalid request callbacks must throw on connect'); - } catch (error) { - expect(error.message).to.include('REQUEST_TOKEN_CALLBACK'); - } - }); - }); + describe('2. AWS Automatic Auth', function () { + describe('2.1 Single Principal', function () { + // Create a client with a url of the form mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws. + // Perform a find operation that succeeds. + // Close the client. }); - // Drivers MUST ensure that they are testing the ability to cache credentials. - // Drivers will need to be able to query and override the cached credentials to - // verify usage. Unless otherwise specified, the tests MUST be performed with - // the authorization code workflow with and without a provided refresh callback. - // If desired, the caching tests can be done using mock server responses. - describe('5. Caching', function () { - let requestInvokations = 0; - let refreshInvokations = 0; - const cache = OIDC_WORKFLOWS.get('callback').cache; - // - Give a callback response with a valid accessToken and an expiresInSeconds - // that is within one minute. - // - Validate the request callback inputs, including the timeout parameter if possible. - const requestCallback = async (principalName, serverResult, timeout) => { - const token = await readFile(`${process.env.OIDC_TOKEN_DIR}/test_user1`, { - encoding: 'utf8' - }); - - expect(principalName).to.equal('test_user1'); - expect(serverResult).to.have.property('clientId'); - expect(timeout).to.equal(300000); - requestInvokations++; - - return { accessToken: token, expiresInSeconds: 30 }; - }; - - const refreshCallback = async (principalName, serverResult, tokenResult, timeout) => { - const token = await readFile(`${process.env.OIDC_TOKEN_DIR}/test_user1`, { - encoding: 'utf8' - }); - - expect(principalName).to.equal('test_user1'); - expect(serverResult).to.have.property('clientId'); - expect(tokenResult.accessToken).to.equal(token); - expect(timeout).to.equal(300000); - refreshInvokations++; - - return { accessToken: token, expiresInSeconds: 30 }; - }; - - beforeEach(() => { - requestInvokations = 0; - refreshInvokations = 0; - }); - - context('when calling the request callback', function () { - let client; - let collection; - - // - Clear the cache. - before(function () { - cache.clear(); - - // - Create a new client with a request callback and a refresh callback. - // Both callbacks will read the contents of the AWS_WEB_IDENTITY_TOKEN_FILE - // location to obtain a valid access token. - client = new MongoClient('mongodb://test_user1@localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: requestCallback, - REFRESH_TOKEN_CALLBACK: refreshCallback - } - }); - collection = client.db('test').collection('test'); - }); - - after(async () => { - client?.close(); - }); - - // - Ensure that a find operation adds credentials to the cache. - it('adds credentials to the cache', async function () { - await collection.findOne(); - expect(cache.entries.size).to.equal(1); - }); - }); - - context('when calling the refresh callback', function () { - let client; - let collection; - - before(function () { - // - Create a new client with the same request callback and a refresh callback. - client = new MongoClient('mongodb://test_user1@localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: requestCallback, - REFRESH_TOKEN_CALLBACK: refreshCallback - } - }); - collection = client.db('test').collection('test'); - }); - - after(async () => { - client?.close(); - }); - - // - Ensure that a find operation results in a call to the refresh callback. - // - Validate the refresh callback inputs, including the timeout parameter if possible. - // - Ensure there is a cache with credentials that will expire in less than 5 minutes, - // using a client with an appropriate request callback. - it('adds credentials to the cache', async function () { - await collection.findOne(); - expect(requestInvokations).to.equal(0); - expect(refreshInvokations).to.equal(1); - expect(cache.entries.values().next().value.expiration).to.be.below(Date.now() + 300000); - }); - }); - - context('when providing no refresh callback', function () { - let client; - let collection; - - before(function () { - // - Create a new client with the a request callback but no refresh callback. - client = new MongoClient('mongodb://test_user1@localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: requestCallback - } - }); - collection = client.db('test').collection('test'); - }); - - after(async () => { - client?.close(); - cache.clear(); - }); - - // - Ensure that a find operation results in a call to the request callback. - it('adds credentials to the cache', async function () { - await collection.findOne(); - expect(requestInvokations).to.equal(1); - expect(refreshInvokations).to.equal(0); - }); - }); + describe('2.2 Multiple Principal User 1', function () { + // Create a client with a url of the form mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred. + // Perform a find operation that succeeds. + // Close the client. }); - // The driver MUST test reauthentication with MONGODB-OIDC for a read operation. - describe('6. Reauthentication', function () { - let refreshInvocations = 0; - let findStarted = 0; - let findSucceeded = 0; - let findFailed = 0; - let saslStarted = 0; - let saslSucceeded = 0; - let client; - let collection; - const cache = OIDC_WORKFLOWS.get('callback').cache; - - // - Create request and refresh callbacks that return valid credentials that - // will not expire soon. - const requestCallback = async () => { - const token = await readFile(`${process.env.OIDC_TOKEN_DIR}/test_user1`, { - encoding: 'utf8' - }); - return { accessToken: token, expiresInSeconds: 300 }; - }; + describe('2.3 Multiple Principal User 2', function () { + // Set the AWS_WEB_IDENTITY_TOKEN_FILE environment variable to the location of valid test_user2 credentials. + // Create a client with a url of the form mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred. + // Perform a find operation that succeeds. + // Close the client. + // Restore the AWS_WEB_IDENTITY_TOKEN_FILE environment variable to the location of valid test_user2 credentials. + }); - const refreshCallback = async () => { - const token = await readFile(`${process.env.OIDC_TOKEN_DIR}/test_user1`, { - encoding: 'utf8' - }); - refreshInvocations++; - return { accessToken: token, expiresInSeconds: 300 }; - }; + describe('2.4 Allowed Hosts Ignored', function () { + // Create a client with a url of the form mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws, and an ALLOWED_HOSTS that is an empty list. + // Assert that a find operation succeeds. + // Close the client. + }); + }); - const commandStarted = event => { - if (event.commandName === 'find') { - findStarted++; - } - if (event.commandName === 'saslStart') { - saslStarted++; - } - }; + describe('3. Callback Validation', function () { + describe('3.1 Valid Callbacks', function () { + // Clear the cache. + // Create request and refresh callback that validate their inputs and return a valid token. The request callback must return a token that expires in one minute. + // Create a client that uses the above callbacks. + // Perform a find operation that succeeds. Verify that the request callback was called with the appropriate inputs, including the timeout parameter if possible. Ensure that there are no unexpected fields. + // Perform another find operation that succeeds. Verify that the refresh callback was called with the appropriate inputs, including the timeout parameter if possible. + // Close the client. + }); - const commandSucceeded = event => { - if (event.commandName === 'find') { - findSucceeded++; - } - if (event.commandName === 'saslStart') { - saslSucceeded++; - } - }; + describe('3.2 Request Callback Returns Null', function () { + // Clear the cache. + // Create a client with a request callback that returns null. + // Perform a find operation that fails. + // Close the client. + }); - const commandFailed = event => { - if (event.commandName === 'find') { - findFailed++; - } - }; + describe('3.3 Refresh Callback Returns Null', function () { + // Clear the cache. + // Create request callback that returns a valid token that will expire in a minute, and a refresh callback that returns null. + // Perform a find operation that succeeds. + // Perform a find operation that fails. + // Close the client. + }); - before(function () { - // - Clear the cache - cache.clear(); - // - Create a client with the callbacks and an event listener capable of - // listening for SASL commands - client = new MongoClient('mongodb://test_user1@localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: requestCallback, - REFRESH_TOKEN_CALLBACK: refreshCallback - }, - monitorCommands: true - }); - client.on('commandStarted', commandStarted); - client.on('commandSucceeded', commandSucceeded); - client.on('commandFailed', commandFailed); - collection = client.db('test').collection('test'); - }); + describe('3.4 Request Callback Returns Invalid Data', function () { + // Clear the cache. + // Create a client with a request callback that returns data not conforming to the OIDCRequestTokenResult with missing field(s). + // Perform a find operation that fails. + // Close the client. + // Create a client with a request callback that returns data not conforming to the OIDCRequestTokenResult with extra field(s). + // Perform a find operation that fails. + // Close the client. + }); - after(async function () { - client.removeAllListeners('commandStarted'); - client.removeAllListeners('commandSucceeded'); - client.removeAllListeners('commandFailed'); - cache.clear(); - await client?.close(); - }); + describe('3.5 Refresh Callback Returns Missing Data', function () { + // Clear the cache. + // Create request callback that returns a valid token that will expire in a minute, and a refresh callback that returns data not conforming to the OIDCRequestTokenResult with missing field(s). + // Create a client with the callbacks. + // Perform a find operation that succeeds. + // Close the client. + // Create a new client with the same callbacks. + // Perform a find operation that fails. + // Close the client. + }); - context('on the first find invokation', function () { - before(function () { - findStarted = 0; - findSucceeded = 0; - findFailed = 0; - refreshInvocations = 0; - saslStarted = 0; - saslSucceeded = 0; - }); + describe('3.6 Refresh Callback Returns Extra Data', function () { + // Clear the cache. + // Create request callback that returns a valid token that will expire in a minute, and a refresh callback that returns data not conforming to the OIDCRequestTokenResult with extra field(s). + // Create a client with the callbacks. + // Perform a find operation that succeeds. + // Close the client. + // Create a new client with the same callbacks. + // Perform a find operation that fails. + // Close the client. + }); + }); - // - Perform a find operation. - // - Assert that the refresh callback has not been called. - it('does not call the refresh callback', async function () { - await collection.findOne(); - expect(refreshInvocations).to.equal(0); - }); - }); + describe('4. Cached Credentials', function () { + describe('4.1 Cache with refresh', function () { + // Clear the cache. + // Create a new client with a request callback that gives credentials that expire in on minute. + // Ensure that a find operation adds credentials to the cache. + // Close the client. + // Create a new client with the same request callback and a refresh callback. + // Ensure that a find operation results in a call to the refresh callback. + // Close the client. + }); - context('when a command errors and needs reauthentication', function () { - // Force a reauthenication using a failCommand of the form: - before(async function () { - findStarted = 0; - findSucceeded = 0; - findFailed = 0; - refreshInvocations = 0; - saslStarted = 0; - saslSucceeded = 0; - await client.db('admin').command({ - configureFailPoint: 'failCommand', - mode: { times: 1 }, - data: { - failCommands: ['find'], - errorCode: 391 - } - }); - // Perform another find operation. - await collection.findOne(); - }); + describe('4.2 Cache with no refresh', function () { + // Clear the cache. + // Create a new client with a request callback that gives credentials that expire in one minute. + // Ensure that a find operation adds credentials to the cache. + // Close the client. + // Create a new client with the a request callback but no refresh callback. + // Ensure that a find operation results in a call to the request callback. + // Close the client. + }); - after(async function () { - await client.db('admin').command({ - configureFailPoint: 'failCommand', - mode: 'off' - }); - cache.clear(); - await client?.close(); - }); + describe('4.3 Cache key includes callback', function () { + // Clear the cache. + // Create a new client with a request callback that does not give an `expiresInSeconds` value. + // Ensure that a find operation adds credentials to the cache. + // Close the client. + // Create a new client with a different request callback. + // Ensure that a find operation adds a new entry to the cache. + // Close the client. + }); - // - Assert that the refresh callback has been called, if possible. - it('calls the refresh callback', function () { - expect(refreshInvocations).to.equal(1); - }); + describe('4.4 Error clears cache', function () { + // Clear the cache. + // Create a new client with a valid request callback that gives credentials that expire within 5 minutes and a refresh callback that gives invalid credentials. + // Ensure that a find operation adds a new entry to the cache. + // Ensure that a subsequent find operation results in an error. + // Ensure that the cached token has been cleared. + // Close the client. + }); - // - Assert that a find operation was started twice and a saslStart operation - // was started once during the command execution. - it('starts the find operation twice', function () { - expect(findStarted).to.equal(2); - }); + describe('4.5 AWS Automatic workflow does not use cache', function () { + // Clear the cache. + // Create a new client that uses the AWS automatic workflow. + // Ensure that a find operation does not add credentials to the cache. + // Close the client. + }); + }); - it('starts saslStart once', function () { - expect(saslStarted).to.equal(1); - }); + describe('5. Speculative Authentication', function () { + // Clear the cache. + // Create a client with a request callback that returns a valid token that will not expire soon. + // Set a fail point for saslStart commands of the form: + // + // { + // "configureFailPoint": "failCommand", + // "mode": { + // "times": 2 + // }, + // "data": { + // "failCommands": [ + // "saslStart" + // ], + // "errorCode": 18 + // } + // } + // + // Note + // + // The driver MUST either use a unique appName or explicitly remove the failCommand after the test to prevent leakage. + // + // Perform a find operation that succeeds. + // Close the client. + // Create a new client with the same properties without clearing the cache. + // Set a fail point for saslStart commands. + // Perform a find operation that succeeds. + // Close the client. + }); - // - Assert that a find operation succeeeded once and the saslStart operation - // succeeded during the command execution. - it('succeeds on the find once', function () { - expect(findSucceeded).to.equal(1); - }); + describe('6. Reauthentication', function () { + describe('6.1 Succeeds', function () { + // Clear the cache. + // Create request and refresh callbacks that return valid credentials that will not expire soon. + // Create a client with the callbacks and an event listener. The following assumes that the driver does not emit saslStart or saslContinue events. If the driver does emit those events, ignore/filter them for the purposes of this test. + // Perform a find operation that succeeds. + // Assert that the refresh callback has not been called. + // Clear the listener state if possible. + // Force a reauthenication using a failCommand of the form: + // + // { + // "configureFailPoint": "failCommand", + // "mode": { + // "times": 1 + // }, + // "data": { + // "failCommands": [ + // "find" + // ], + // "errorCode": 391 + // } + // } + // + // Note + // + // the driver MUST either use a unique appName or explicitly remove the failCommand after the test to prevent leakage. + // + // Perform another find operation that succeeds. + // Assert that the refresh callback has been called once, if possible. + // Assert that the ordering of list started events is [find], , find. Note that if the listener stat could not be cleared then there will and be extra find command. + // Assert that the list of command succeeded events is [find]. + // Assert that a find operation failed once during the command execution. + // Close the client. + }); - it('succeeds on saslStart once', function () { - expect(saslSucceeded).to.equal(1); - }); + describe('6.2 Retries and Succeeds with Cache', function () { + // Clear the cache. + // Create request and refresh callbacks that return valid credentials that will not expire soon. + // Perform a find operation that succeeds. + // Force a reauthenication using a failCommand of the form: + // + // { + // "configureFailPoint": "failCommand", + // "mode": { + // "times": 2 + // }, + // "data": { + // "failCommands": [ + // "find", "saslStart" + // ], + // "errorCode": 391 + // } + // } + // + // Perform a find operation that succeeds. + // Close the client. + }); - // Assert that a find operation failed once during the command execution. - it('fails on the find once', function () { - expect(findFailed).to.equal(1); - }); - }); + describe('6.3 Retries and Fails with no Cache', function () { + // Clear the cache. + // Create request and refresh callbacks that return valid credentials that will not expire soon. + // Perform a find operation that succeeds (to force a speculative auth). + // Clear the cache. + // Force a reauthenication using a failCommand of the form: + // + // { + // "configureFailPoint": "failCommand", + // "mode": { + // "times": 2 + // }, + // "data": { + // "failCommands": [ + // "find", "saslStart" + // ], + // "errorCode": 391 + // } + // } + // + // Perform a find operation that fails. + // Close the client. }); }); }); From 3383e8744102a614aee187eae29a8f07b1f7e481 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 13 Apr 2023 16:16:22 +0200 Subject: [PATCH 04/93] test: adding first prose test --- test/manual/mongodb_oidc.prose.test.ts | 81 +++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index d8ffc4806ad..4a1fea7ee1f 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -1,8 +1,19 @@ import { readFile } from 'node:fs/promises'; +import path from 'node:path'; import { expect } from 'chai'; -import { MongoClient, MongoInvalidArgumentError, OIDC_WORKFLOWS } from '../mongodb'; +import { + MongoClient, + MongoInvalidArgumentError, + OIDC_WORKFLOWS, + OIDCClientInfo, + OIDCMechanismServerStep1, + OIDCRefreshFunction, + OIDCRequestFunction, + OIDCRequestTokenResult, + Collection +} from '../mongodb'; describe('MONGODB-OIDC', function () { context('when running in the environment', function () { @@ -12,13 +23,81 @@ describe('MONGODB-OIDC', function () { }); describe('OIDC Auth Spec Prose Tests', function () { + // Set up the cache variable. + const cache = OIDC_WORKFLOWS.get('callback').cache; + // Creates a request function for use in the test. + const createRequestCallback = (username = 'test_user1', expiresInSeconds?: number) => { + return async (clientInfo: OIDCClientInfo, serverInfo: OIDCMechanismServerStep1) => { + const token = await readFile(path.join(process.env.OIDC_TOKEN_DIR, username), { + encoding: 'utf8' + }); + // Do some basic property assertions. + expect(clientInfo).to.have.property('timeoutSeconds'); + expect(serverInfo).to.have.property('issuer'); + expect(serverInfo).to.have.property('clientId'); + const response: OIDCRequestTokenResult = { accessToken: token }; + if (expiresInSeconds) { + response.expiresInSeconds = expiresInSeconds; + } + return response; + }; + }; + + // Creates a refresh function for use in the test. + const createRefreshCallback = (username = 'test_user1', expiresInSeconds?: number) => { + return async ( + clientInfo: OIDCClientInfo, + serverInfo: OIDCMechanismServerStep1, + tokenResult: OIDCRequestTokenResult + ) => { + const token = await readFile(path.join(process.env.OIDC_TOKEN_DIR, username), { + encoding: 'utf8' + }); + // Do some basic property assertions. + expect(clientInfo).to.have.property('timeoutSeconds'); + expect(serverInfo).to.have.property('issuer'); + expect(serverInfo).to.have.property('clientId'); + expect(tokenResult).to.have.property('accessToken'); + const response: OIDCRequestTokenResult = { accessToken: token }; + if (expiresInSeconds) { + response.expiresInSeconds = expiresInSeconds; + } + return response; + }; + }; + describe('1. Callback-Driven Auth', function () { + let client: MongoClient; + let collection: Collection; + + beforeEach(function () { + cache.clear(); + }); + + afterEach(async function () { + await client?.close(); + }); + describe('1.1 Single Principal Implicit Username', function () { + before(function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback() + } + }); + collection = client.db('test').collection('test'); + }); + // Clear the cache. // Create a request callback returns a valid token. // Create a client that uses the default OIDC url and the request callback. // Perform a find operation. that succeeds. // Close the client. + it('successfully authenticates', function () { + expect(async () => { + await collection.findOne(); + }).to.not.throw; + }); }); describe('1.2 Single Principal Explicit Username', function () { From 3608ea0b22865f716bbe1263b2e484c3251672ba Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 14 Apr 2023 13:06:44 +0200 Subject: [PATCH 05/93] test: prose tests up to aws --- test/manual/mongodb_oidc.prose.test.ts | 123 ++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 5 deletions(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 4a1fea7ee1f..bdc233df869 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -4,15 +4,12 @@ import path from 'node:path'; import { expect } from 'chai'; import { + Collection, MongoClient, - MongoInvalidArgumentError, OIDC_WORKFLOWS, OIDCClientInfo, OIDCMechanismServerStep1, - OIDCRefreshFunction, - OIDCRequestFunction, - OIDCRequestTokenResult, - Collection + OIDCRequestTokenResult } from '../mongodb'; describe('MONGODB-OIDC', function () { @@ -101,34 +98,99 @@ describe('MONGODB-OIDC', function () { }); describe('1.2 Single Principal Explicit Username', function () { + before(function () { + client = new MongoClient('mongodb://test_user@localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback() + } + }); + collection = client.db('test').collection('test'); + }); + // Clear the cache. // Create a request callback that returns a valid token. // Create a client with a url of the form mongodb://test_user1@localhost/?authMechanism=MONGODB-OIDC and the OIDC request callback. // Perform a find operation that succeeds. // Close the client. + it('successfully authenticates', function () { + expect(async () => { + await collection.findOne(); + }).to.not.throw; + }); }); describe('1.3 Multiple Principal User 1', function () { + before(function () { + client = new MongoClient( + 'mongodb://test_user1@localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred', + { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback() + } + } + ); + collection = client.db('test').collection('test'); + }); + // Clear the cache. // Create a request callback that returns a valid token. // Create a client with a url of the form mongodb://test_user1@localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred and a valid OIDC request callback. // Perform a find operation that succeeds. // Close the client. + it('successfully authenticates', function () { + expect(async () => { + await collection.findOne(); + }).to.not.throw; + }); }); describe('1.4 Multiple Principal User 2', function () { + before(function () { + client = new MongoClient( + 'mongodb://test_user2@localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred', + { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback() + } + } + ); + collection = client.db('test').collection('test'); + }); + // Clear the cache. // Create a request callback that reads in the generated test_user2 token file. // Create a client with a url of the form mongodb://test_user2@localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred and a valid OIDC request callback. // Perform a find operation that succeeds. // Close the client. + it('successfully authenticates', function () { + expect(async () => { + await collection.findOne(); + }).to.not.throw; + }); }); describe('1.5 Multiple Principal No User', function () { + before(function () { + client = new MongoClient( + 'mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred', + { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback() + } + } + ); + collection = client.db('test').collection('test'); + }); + // Clear the cache. // Create a client with a url of the form mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred and a valid OIDC request callback. // Assert that a find operation fails. // Close the client. + it('fails authentication', function () { + expect(async () => { + await collection.findOne(); + }).to.throw; + }); }); describe('1.6 Allowed Hosts Blocked', function () { @@ -144,24 +206,75 @@ describe('MONGODB-OIDC', function () { }); describe('2. AWS Automatic Auth', function () { + let client: MongoClient; + let collection: Collection; + + afterEach(async function () { + await client?.close(); + }); + describe('2.1 Single Principal', function () { + before(function () { + client = new MongoClient( + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws' + ); + collection = client.db('test').collection('test'); + }); + // Create a client with a url of the form mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws. // Perform a find operation that succeeds. // Close the client. + it('successfully authenticates', function () { + expect(async () => { + await collection.findOne(); + }).to.not.throw; + }); }); describe('2.2 Multiple Principal User 1', function () { + before(function () { + client = new MongoClient( + 'mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred' + ); + collection = client.db('test').collection('test'); + }); + // Create a client with a url of the form mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred. // Perform a find operation that succeeds. // Close the client. + it('successfully authenticates', function () { + expect(async () => { + await collection.findOne(); + }).to.not.throw; + }); }); describe('2.3 Multiple Principal User 2', function () { + let tokenFile; + + before(function () { + tokenFile = process.env.AWS_WEB_IDENTITY_TOKEN_FILE; + process.env.AWS_WEB_IDENTITY_TOKEN_FILE = path.join(process.env.OIDC_TOKEN_DIR, 'test2'); + client = new MongoClient( + 'mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred' + ); + collection = client.db('test').collection('test'); + }); + + after(function () { + process.env.AWS_WEB_IDENTITY_TOKEN_FILE = tokenFile; + }); + // Set the AWS_WEB_IDENTITY_TOKEN_FILE environment variable to the location of valid test_user2 credentials. // Create a client with a url of the form mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred. // Perform a find operation that succeeds. // Close the client. // Restore the AWS_WEB_IDENTITY_TOKEN_FILE environment variable to the location of valid test_user2 credentials. + it('successfully authenticates', function () { + expect(async () => { + await collection.findOne(); + }).to.not.throw; + }); }); describe('2.4 Allowed Hosts Ignored', function () { From ac802a2d6449c9d8db69e594c36356394faecbcb Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 14 Apr 2023 13:11:06 +0200 Subject: [PATCH 06/93] fix: issuer is not optional --- src/cmap/auth/mongodb_oidc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index 71ea06395c1..21ca970eb13 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -11,7 +11,7 @@ import type { Workflow } from './mongodb_oidc/workflow'; * @experimental */ export interface OIDCMechanismServerStep1 { - issuer?: string; + issuer: string; clientId: string; requestScopes?: string[]; } From 58aa17ee197b2b9a19149feb3eff097b0beb4459 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 14 Apr 2023 13:37:48 +0200 Subject: [PATCH 07/93] test: more callback tests --- test/manual/mongodb_oidc.prose.test.ts | 538 ++++++++++++++----------- 1 file changed, 294 insertions(+), 244 deletions(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index bdc233df869..fe78f8aec8a 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { expect } from 'chai'; +import * as sinon from 'sinon'; import { Collection, @@ -11,6 +12,7 @@ import { OIDCMechanismServerStep1, OIDCRequestTokenResult } from '../mongodb'; +import { request } from 'node:http'; describe('MONGODB-OIDC', function () { context('when running in the environment', function () { @@ -203,303 +205,351 @@ describe('MONGODB-OIDC', function () { // Close the client. }); }); - }); - describe('2. AWS Automatic Auth', function () { - let client: MongoClient; - let collection: Collection; + describe('2. AWS Automatic Auth', function () { + let client: MongoClient; + let collection: Collection; - afterEach(async function () { - await client?.close(); - }); + afterEach(async function () { + await client?.close(); + }); - describe('2.1 Single Principal', function () { - before(function () { - client = new MongoClient( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws' - ); - collection = client.db('test').collection('test'); + describe('2.1 Single Principal', function () { + before(function () { + client = new MongoClient( + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws' + ); + collection = client.db('test').collection('test'); + }); + + // Create a client with a url of the form mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws. + // Perform a find operation that succeeds. + // Close the client. + it('successfully authenticates', function () { + expect(async () => { + await collection.findOne(); + }).to.not.throw; + }); }); - // Create a client with a url of the form mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws. - // Perform a find operation that succeeds. - // Close the client. - it('successfully authenticates', function () { - expect(async () => { - await collection.findOne(); - }).to.not.throw; + describe('2.2 Multiple Principal User 1', function () { + before(function () { + client = new MongoClient( + 'mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred' + ); + collection = client.db('test').collection('test'); + }); + + // Create a client with a url of the form mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred. + // Perform a find operation that succeeds. + // Close the client. + it('successfully authenticates', function () { + expect(async () => { + await collection.findOne(); + }).to.not.throw; + }); }); - }); - describe('2.2 Multiple Principal User 1', function () { - before(function () { - client = new MongoClient( - 'mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred' - ); - collection = client.db('test').collection('test'); + describe('2.3 Multiple Principal User 2', function () { + let tokenFile; + + before(function () { + tokenFile = process.env.AWS_WEB_IDENTITY_TOKEN_FILE; + process.env.AWS_WEB_IDENTITY_TOKEN_FILE = path.join(process.env.OIDC_TOKEN_DIR, 'test2'); + client = new MongoClient( + 'mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred' + ); + collection = client.db('test').collection('test'); + }); + + after(function () { + process.env.AWS_WEB_IDENTITY_TOKEN_FILE = tokenFile; + }); + + // Set the AWS_WEB_IDENTITY_TOKEN_FILE environment variable to the location of valid test_user2 credentials. + // Create a client with a url of the form mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred. + // Perform a find operation that succeeds. + // Close the client. + // Restore the AWS_WEB_IDENTITY_TOKEN_FILE environment variable to the location of valid test_user2 credentials. + it('successfully authenticates', function () { + expect(async () => { + await collection.findOne(); + }).to.not.throw; + }); }); - // Create a client with a url of the form mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred. - // Perform a find operation that succeeds. - // Close the client. - it('successfully authenticates', function () { - expect(async () => { - await collection.findOne(); - }).to.not.throw; + describe('2.4 Allowed Hosts Ignored', function () { + // Create a client with a url of the form mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws, and an ALLOWED_HOSTS that is an empty list. + // Assert that a find operation succeeds. + // Close the client. }); }); - describe('2.3 Multiple Principal User 2', function () { - let tokenFile; + describe('3. Callback Validation', function () { + let client: MongoClient; + let collection: Collection; - before(function () { - tokenFile = process.env.AWS_WEB_IDENTITY_TOKEN_FILE; - process.env.AWS_WEB_IDENTITY_TOKEN_FILE = path.join(process.env.OIDC_TOKEN_DIR, 'test2'); - client = new MongoClient( - 'mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred' - ); - collection = client.db('test').collection('test'); + beforeEach(function () { + cache.clear(); }); - after(function () { - process.env.AWS_WEB_IDENTITY_TOKEN_FILE = tokenFile; + afterEach(async function () { + await client?.close(); }); - // Set the AWS_WEB_IDENTITY_TOKEN_FILE environment variable to the location of valid test_user2 credentials. - // Create a client with a url of the form mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred. - // Perform a find operation that succeeds. - // Close the client. - // Restore the AWS_WEB_IDENTITY_TOKEN_FILE environment variable to the location of valid test_user2 credentials. - it('successfully authenticates', function () { - expect(async () => { + describe('3.1 Valid Callbacks', function () { + let requestSpy; + let refreshSpy; + + before(function () { + requestSpy = sinon.spy(createRequestCallback('test_user1', 60)); + refreshSpy = sinon.spy(createRefreshCallback()); + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: requestSpy, + REFRESH_TOKEN_CALLBACK: refreshSpy + } + }); + collection = client.db('test').collection('test'); + }); + + // Clear the cache. + // Create request and refresh callback that validate their inputs and return a valid token. The request callback must return a token that expires in one minute. + // Create a client that uses the above callbacks. + // Perform a find operation that succeeds. Verify that the request callback was called with the appropriate inputs, including the timeout parameter if possible. Ensure that there are no unexpected fields. + // Perform another find operation that succeeds. Verify that the refresh callback was called with the appropriate inputs, including the timeout parameter if possible. + // Close the client. + it('successfully authenticates with the request and refresh callbacks', async function () { await collection.findOne(); - }).to.not.throw; + expect(requestSpy.calledOnce).to.be.true; + await collection.findOne(); + expect(refreshSpy.calledOnce).to.be.true; + }); }); - }); - describe('2.4 Allowed Hosts Ignored', function () { - // Create a client with a url of the form mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws, and an ALLOWED_HOSTS that is an empty list. - // Assert that a find operation succeeds. - // Close the client. - }); - }); - - describe('3. Callback Validation', function () { - describe('3.1 Valid Callbacks', function () { - // Clear the cache. - // Create request and refresh callback that validate their inputs and return a valid token. The request callback must return a token that expires in one minute. - // Create a client that uses the above callbacks. - // Perform a find operation that succeeds. Verify that the request callback was called with the appropriate inputs, including the timeout parameter if possible. Ensure that there are no unexpected fields. - // Perform another find operation that succeeds. Verify that the refresh callback was called with the appropriate inputs, including the timeout parameter if possible. - // Close the client. - }); + describe('3.2 Request Callback Returns Null', function () { + before(function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: () => { + return Promise.resolve(null); + } + } + }); + collection = client.db('test').collection('test'); + }); - describe('3.2 Request Callback Returns Null', function () { - // Clear the cache. - // Create a client with a request callback that returns null. - // Perform a find operation that fails. - // Close the client. - }); + // Clear the cache. + // Create a client with a request callback that returns null. + // Perform a find operation that fails. + // Close the client. + it('fails authentication', function () { + expect(async () => { + await collection.findOne(); + }).to.throw; + }); + }); - describe('3.3 Refresh Callback Returns Null', function () { - // Clear the cache. - // Create request callback that returns a valid token that will expire in a minute, and a refresh callback that returns null. - // Perform a find operation that succeeds. - // Perform a find operation that fails. - // Close the client. - }); + describe('3.3 Refresh Callback Returns Null', function () { + // Clear the cache. + // Create request callback that returns a valid token that will expire in a minute, and a refresh callback that returns null. + // Perform a find operation that succeeds. + // Perform a find operation that fails. + // Close the client. + }); - describe('3.4 Request Callback Returns Invalid Data', function () { - // Clear the cache. - // Create a client with a request callback that returns data not conforming to the OIDCRequestTokenResult with missing field(s). - // Perform a find operation that fails. - // Close the client. - // Create a client with a request callback that returns data not conforming to the OIDCRequestTokenResult with extra field(s). - // Perform a find operation that fails. - // Close the client. - }); + describe('3.4 Request Callback Returns Invalid Data', function () { + // Clear the cache. + // Create a client with a request callback that returns data not conforming to the OIDCRequestTokenResult with missing field(s). + // Perform a find operation that fails. + // Close the client. + // Create a client with a request callback that returns data not conforming to the OIDCRequestTokenResult with extra field(s). + // Perform a find operation that fails. + // Close the client. + }); - describe('3.5 Refresh Callback Returns Missing Data', function () { - // Clear the cache. - // Create request callback that returns a valid token that will expire in a minute, and a refresh callback that returns data not conforming to the OIDCRequestTokenResult with missing field(s). - // Create a client with the callbacks. - // Perform a find operation that succeeds. - // Close the client. - // Create a new client with the same callbacks. - // Perform a find operation that fails. - // Close the client. - }); + describe('3.5 Refresh Callback Returns Missing Data', function () { + // Clear the cache. + // Create request callback that returns a valid token that will expire in a minute, and a refresh callback that returns data not conforming to the OIDCRequestTokenResult with missing field(s). + // Create a client with the callbacks. + // Perform a find operation that succeeds. + // Close the client. + // Create a new client with the same callbacks. + // Perform a find operation that fails. + // Close the client. + }); - describe('3.6 Refresh Callback Returns Extra Data', function () { - // Clear the cache. - // Create request callback that returns a valid token that will expire in a minute, and a refresh callback that returns data not conforming to the OIDCRequestTokenResult with extra field(s). - // Create a client with the callbacks. - // Perform a find operation that succeeds. - // Close the client. - // Create a new client with the same callbacks. - // Perform a find operation that fails. - // Close the client. + describe('3.6 Refresh Callback Returns Extra Data', function () { + // Clear the cache. + // Create request callback that returns a valid token that will expire in a minute, and a refresh callback that returns data not conforming to the OIDCRequestTokenResult with extra field(s). + // Create a client with the callbacks. + // Perform a find operation that succeeds. + // Close the client. + // Create a new client with the same callbacks. + // Perform a find operation that fails. + // Close the client. + }); }); - }); - describe('4. Cached Credentials', function () { - describe('4.1 Cache with refresh', function () { - // Clear the cache. - // Create a new client with a request callback that gives credentials that expire in on minute. - // Ensure that a find operation adds credentials to the cache. - // Close the client. - // Create a new client with the same request callback and a refresh callback. - // Ensure that a find operation results in a call to the refresh callback. - // Close the client. - }); + describe('4. Cached Credentials', function () { + describe('4.1 Cache with refresh', function () { + // Clear the cache. + // Create a new client with a request callback that gives credentials that expire in on minute. + // Ensure that a find operation adds credentials to the cache. + // Close the client. + // Create a new client with the same request callback and a refresh callback. + // Ensure that a find operation results in a call to the refresh callback. + // Close the client. + }); - describe('4.2 Cache with no refresh', function () { - // Clear the cache. - // Create a new client with a request callback that gives credentials that expire in one minute. - // Ensure that a find operation adds credentials to the cache. - // Close the client. - // Create a new client with the a request callback but no refresh callback. - // Ensure that a find operation results in a call to the request callback. - // Close the client. - }); + describe('4.2 Cache with no refresh', function () { + // Clear the cache. + // Create a new client with a request callback that gives credentials that expire in one minute. + // Ensure that a find operation adds credentials to the cache. + // Close the client. + // Create a new client with the a request callback but no refresh callback. + // Ensure that a find operation results in a call to the request callback. + // Close the client. + }); - describe('4.3 Cache key includes callback', function () { - // Clear the cache. - // Create a new client with a request callback that does not give an `expiresInSeconds` value. - // Ensure that a find operation adds credentials to the cache. - // Close the client. - // Create a new client with a different request callback. - // Ensure that a find operation adds a new entry to the cache. - // Close the client. - }); + describe('4.3 Cache key includes callback', function () { + // Clear the cache. + // Create a new client with a request callback that does not give an `expiresInSeconds` value. + // Ensure that a find operation adds credentials to the cache. + // Close the client. + // Create a new client with a different request callback. + // Ensure that a find operation adds a new entry to the cache. + // Close the client. + }); - describe('4.4 Error clears cache', function () { - // Clear the cache. - // Create a new client with a valid request callback that gives credentials that expire within 5 minutes and a refresh callback that gives invalid credentials. - // Ensure that a find operation adds a new entry to the cache. - // Ensure that a subsequent find operation results in an error. - // Ensure that the cached token has been cleared. - // Close the client. - }); + describe('4.4 Error clears cache', function () { + // Clear the cache. + // Create a new client with a valid request callback that gives credentials that expire within 5 minutes and a refresh callback that gives invalid credentials. + // Ensure that a find operation adds a new entry to the cache. + // Ensure that a subsequent find operation results in an error. + // Ensure that the cached token has been cleared. + // Close the client. + }); - describe('4.5 AWS Automatic workflow does not use cache', function () { - // Clear the cache. - // Create a new client that uses the AWS automatic workflow. - // Ensure that a find operation does not add credentials to the cache. - // Close the client. + describe('4.5 AWS Automatic workflow does not use cache', function () { + // Clear the cache. + // Create a new client that uses the AWS automatic workflow. + // Ensure that a find operation does not add credentials to the cache. + // Close the client. + }); }); - }); - - describe('5. Speculative Authentication', function () { - // Clear the cache. - // Create a client with a request callback that returns a valid token that will not expire soon. - // Set a fail point for saslStart commands of the form: - // - // { - // "configureFailPoint": "failCommand", - // "mode": { - // "times": 2 - // }, - // "data": { - // "failCommands": [ - // "saslStart" - // ], - // "errorCode": 18 - // } - // } - // - // Note - // - // The driver MUST either use a unique appName or explicitly remove the failCommand after the test to prevent leakage. - // - // Perform a find operation that succeeds. - // Close the client. - // Create a new client with the same properties without clearing the cache. - // Set a fail point for saslStart commands. - // Perform a find operation that succeeds. - // Close the client. - }); - describe('6. Reauthentication', function () { - describe('6.1 Succeeds', function () { + describe('5. Speculative Authentication', function () { // Clear the cache. - // Create request and refresh callbacks that return valid credentials that will not expire soon. - // Create a client with the callbacks and an event listener. The following assumes that the driver does not emit saslStart or saslContinue events. If the driver does emit those events, ignore/filter them for the purposes of this test. - // Perform a find operation that succeeds. - // Assert that the refresh callback has not been called. - // Clear the listener state if possible. - // Force a reauthenication using a failCommand of the form: + // Create a client with a request callback that returns a valid token that will not expire soon. + // Set a fail point for saslStart commands of the form: // // { // "configureFailPoint": "failCommand", // "mode": { - // "times": 1 + // "times": 2 // }, // "data": { // "failCommands": [ - // "find" + // "saslStart" // ], - // "errorCode": 391 + // "errorCode": 18 // } // } // // Note // - // the driver MUST either use a unique appName or explicitly remove the failCommand after the test to prevent leakage. + // The driver MUST either use a unique appName or explicitly remove the failCommand after the test to prevent leakage. // - // Perform another find operation that succeeds. - // Assert that the refresh callback has been called once, if possible. - // Assert that the ordering of list started events is [find], , find. Note that if the listener stat could not be cleared then there will and be extra find command. - // Assert that the list of command succeeded events is [find]. - // Assert that a find operation failed once during the command execution. - // Close the client. - }); - - describe('6.2 Retries and Succeeds with Cache', function () { - // Clear the cache. - // Create request and refresh callbacks that return valid credentials that will not expire soon. // Perform a find operation that succeeds. - // Force a reauthenication using a failCommand of the form: - // - // { - // "configureFailPoint": "failCommand", - // "mode": { - // "times": 2 - // }, - // "data": { - // "failCommands": [ - // "find", "saslStart" - // ], - // "errorCode": 391 - // } - // } - // + // Close the client. + // Create a new client with the same properties without clearing the cache. + // Set a fail point for saslStart commands. // Perform a find operation that succeeds. // Close the client. }); - describe('6.3 Retries and Fails with no Cache', function () { - // Clear the cache. - // Create request and refresh callbacks that return valid credentials that will not expire soon. - // Perform a find operation that succeeds (to force a speculative auth). - // Clear the cache. - // Force a reauthenication using a failCommand of the form: - // - // { - // "configureFailPoint": "failCommand", - // "mode": { - // "times": 2 - // }, - // "data": { - // "failCommands": [ - // "find", "saslStart" - // ], - // "errorCode": 391 - // } - // } - // - // Perform a find operation that fails. - // Close the client. + describe('6. Reauthentication', function () { + describe('6.1 Succeeds', function () { + // Clear the cache. + // Create request and refresh callbacks that return valid credentials that will not expire soon. + // Create a client with the callbacks and an event listener. The following assumes that the driver does not emit saslStart or saslContinue events. If the driver does emit those events, ignore/filter them for the purposes of this test. + // Perform a find operation that succeeds. + // Assert that the refresh callback has not been called. + // Clear the listener state if possible. + // Force a reauthenication using a failCommand of the form: + // + // { + // "configureFailPoint": "failCommand", + // "mode": { + // "times": 1 + // }, + // "data": { + // "failCommands": [ + // "find" + // ], + // "errorCode": 391 + // } + // } + // + // Note + // + // the driver MUST either use a unique appName or explicitly remove the failCommand after the test to prevent leakage. + // + // Perform another find operation that succeeds. + // Assert that the refresh callback has been called once, if possible. + // Assert that the ordering of list started events is [find], , find. Note that if the listener stat could not be cleared then there will and be extra find command. + // Assert that the list of command succeeded events is [find]. + // Assert that a find operation failed once during the command execution. + // Close the client. + }); + + describe('6.2 Retries and Succeeds with Cache', function () { + // Clear the cache. + // Create request and refresh callbacks that return valid credentials that will not expire soon. + // Perform a find operation that succeeds. + // Force a reauthenication using a failCommand of the form: + // + // { + // "configureFailPoint": "failCommand", + // "mode": { + // "times": 2 + // }, + // "data": { + // "failCommands": [ + // "find", "saslStart" + // ], + // "errorCode": 391 + // } + // } + // + // Perform a find operation that succeeds. + // Close the client. + }); + + describe('6.3 Retries and Fails with no Cache', function () { + // Clear the cache. + // Create request and refresh callbacks that return valid credentials that will not expire soon. + // Perform a find operation that succeeds (to force a speculative auth). + // Clear the cache. + // Force a reauthenication using a failCommand of the form: + // + // { + // "configureFailPoint": "failCommand", + // "mode": { + // "times": 2 + // }, + // "data": { + // "failCommands": [ + // "find", "saslStart" + // ], + // "errorCode": 391 + // } + // } + // + // Perform a find operation that fails. + // Close the client. + }); }); }); }); From 7bac7eada0855002385791ae8d46883f4cd67b38 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 14 Apr 2023 14:12:19 +0200 Subject: [PATCH 08/93] fix: mechanism properties --- .evergreen/run-oidc-tests.sh | 1 + src/cmap/auth/mongo_credentials.ts | 5 ++++- test/manual/mongodb_oidc.prose.test.ts | 19 ++++++++++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/.evergreen/run-oidc-tests.sh b/.evergreen/run-oidc-tests.sh index ee6bd74c26d..7254fc16c2d 100644 --- a/.evergreen/run-oidc-tests.sh +++ b/.evergreen/run-oidc-tests.sh @@ -10,5 +10,6 @@ MONGODB_URI_SINGLE="${MONGODB_URI}/?authMechanism=MONGODB-OIDC&authMechanismProp echo $MONGODB_URI_SINGLE export MONGODB_URI="$MONGODB_URI_SINGLE" +export OIDC_TOKEN_DIR=${OIDC_TOKEN_DIR} npm run check:oidc diff --git a/src/cmap/auth/mongo_credentials.ts b/src/cmap/auth/mongo_credentials.ts index 85f42f45a20..7a240cd53ef 100644 --- a/src/cmap/auth/mongo_credentials.ts +++ b/src/cmap/auth/mongo_credentials.ts @@ -117,7 +117,10 @@ export class MongoCredentials { } if (this.mechanism === AuthMechanism.MONGODB_OIDC && !this.mechanismProperties.ALLOWED_HOSTS) { - this.mechanismProperties.ALLOWED_HOSTS = DEFAULT_ALLOWED_HOSTS; + this.mechanismProperties = { + ...this.mechanismProperties, + ALLOWED_HOSTS: DEFAULT_ALLOWED_HOSTS + }; } Object.freeze(this.mechanismProperties); diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index fe78f8aec8a..1a26d15fd9d 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -12,7 +12,6 @@ import { OIDCMechanismServerStep1, OIDCRequestTokenResult } from '../mongodb'; -import { request } from 'node:http'; describe('MONGODB-OIDC', function () { context('when running in the environment', function () { @@ -351,11 +350,29 @@ describe('MONGODB-OIDC', function () { }); describe('3.3 Refresh Callback Returns Null', function () { + before(function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), + REFRESH_TOKEN_CALLBACK: () => { + return Promise.resolve(null); + } + } + }); + collection = client.db('test').collection('test'); + }); + // Clear the cache. // Create request callback that returns a valid token that will expire in a minute, and a refresh callback that returns null. // Perform a find operation that succeeds. // Perform a find operation that fails. // Close the client. + it('fails authentication on refresh', async function () { + await collection.findOne(); + expect(async () => { + await collection.findOne(); + }).to.throw; + }); }); describe('3.4 Request Callback Returns Invalid Data', function () { From 1ab333135fe1eae47c34a5ba46138df96120b14f Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 14 Apr 2023 15:48:08 +0200 Subject: [PATCH 09/93] test: fix path --- test/manual/mongodb_oidc.prose.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 1a26d15fd9d..ec022c59a10 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -1,5 +1,5 @@ import { readFile } from 'node:fs/promises'; -import path from 'node:path'; +import * as path from 'node:path'; import { expect } from 'chai'; import * as sinon from 'sinon'; From c45f2850fd8194de87bde62b31ae554b793df00a Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 17 Apr 2023 14:42:43 -0400 Subject: [PATCH 10/93] test: add tests to speculative auth --- test/manual/mongodb_oidc.prose.test.ts | 516 +++++++++++++++++-------- 1 file changed, 364 insertions(+), 152 deletions(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index ec022c59a10..e13b528e197 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -24,7 +24,11 @@ describe('MONGODB-OIDC', function () { // Set up the cache variable. const cache = OIDC_WORKFLOWS.get('callback').cache; // Creates a request function for use in the test. - const createRequestCallback = (username = 'test_user1', expiresInSeconds?: number) => { + const createRequestCallback = ( + username = 'test_user1', + expiresInSeconds?: number, + extraFields?: any + ) => { return async (clientInfo: OIDCClientInfo, serverInfo: OIDCMechanismServerStep1) => { const token = await readFile(path.join(process.env.OIDC_TOKEN_DIR, username), { encoding: 'utf8' @@ -33,16 +37,16 @@ describe('MONGODB-OIDC', function () { expect(clientInfo).to.have.property('timeoutSeconds'); expect(serverInfo).to.have.property('issuer'); expect(serverInfo).to.have.property('clientId'); - const response: OIDCRequestTokenResult = { accessToken: token }; - if (expiresInSeconds) { - response.expiresInSeconds = expiresInSeconds; - } - return response; + return generateResult(token, expiresInSeconds, extraFields); }; }; // Creates a refresh function for use in the test. - const createRefreshCallback = (username = 'test_user1', expiresInSeconds?: number) => { + const createRefreshCallback = ( + username = 'test_user1', + expiresInSeconds?: number, + extraFields?: any + ) => { return async ( clientInfo: OIDCClientInfo, serverInfo: OIDCMechanismServerStep1, @@ -56,14 +60,22 @@ describe('MONGODB-OIDC', function () { expect(serverInfo).to.have.property('issuer'); expect(serverInfo).to.have.property('clientId'); expect(tokenResult).to.have.property('accessToken'); - const response: OIDCRequestTokenResult = { accessToken: token }; - if (expiresInSeconds) { - response.expiresInSeconds = expiresInSeconds; - } - return response; + return generateResult(token, expiresInSeconds, extraFields); }; }; + // Generates the result the request or refresh callback returns. + const generateResult = (token: string, expiresInSeconds?: number, extraFields?: any) => { + const response: OIDCRequestTokenResult = { accessToken: token }; + if (expiresInSeconds) { + response.expiresInSeconds = expiresInSeconds; + } + if (extraFields) { + return { ...response, ...extraFields }; + } + return response; + }; + describe('1. Callback-Driven Auth', function () { let client: MongoClient; let collection: Collection; @@ -278,9 +290,26 @@ describe('MONGODB-OIDC', function () { }); describe('2.4 Allowed Hosts Ignored', function () { + before(function () { + client = new MongoClient( + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws', + { + authMechanismProperties: { + ALLOWED_HOSTS: [] + } + } + ); + collection = client.db('test').collection('test'); + }); + // Create a client with a url of the form mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws, and an ALLOWED_HOSTS that is an empty list. // Assert that a find operation succeeds. // Close the client. + it('successfully authenticates', function () { + expect(async () => { + await collection.findOne(); + }).to.not.throw; + }); }); }); @@ -376,16 +405,64 @@ describe('MONGODB-OIDC', function () { }); describe('3.4 Request Callback Returns Invalid Data', function () { - // Clear the cache. - // Create a client with a request callback that returns data not conforming to the OIDCRequestTokenResult with missing field(s). - // Perform a find operation that fails. - // Close the client. - // Create a client with a request callback that returns data not conforming to the OIDCRequestTokenResult with extra field(s). - // Perform a find operation that fails. - // Close the client. + context('when the request callback has missing fields', function () { + before(function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: () => { + return Promise.resolve({}); + } + } + }); + collection = client.db('test').collection('test'); + }); + + // Clear the cache. + // Create a client with a request callback that returns data not conforming to the OIDCRequestTokenResult with missing field(s). + // Perform a find operation that fails. + // Close the client. + it('fails authentication', function () { + expect(async () => { + await collection.findOne(); + }).to.throw; + }); + }); + + context('when the request callback has extra fields', function () { + before(function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60, { foo: 'bar' }) + } + }); + collection = client.db('test').collection('test'); + }); + + // Create a client with a request callback that returns data not conforming to the OIDCRequestTokenResult with extra field(s). + // Perform a find operation that fails. + // Close the client. + it('fails authentication', function () { + expect(async () => { + await collection.findOne(); + }).to.throw; + }); + }); }); describe('3.5 Refresh Callback Returns Missing Data', function () { + before(async function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), + REFRESH_TOKEN_CALLBACK: () => { + return Promise.resolve({}); + } + } + }); + await client.db('test').collection('test').findOne(); + await client.close(); + }); + // Clear the cache. // Create request callback that returns a valid token that will expire in a minute, and a refresh callback that returns data not conforming to the OIDCRequestTokenResult with missing field(s). // Create a client with the callbacks. @@ -394,9 +471,33 @@ describe('MONGODB-OIDC', function () { // Create a new client with the same callbacks. // Perform a find operation that fails. // Close the client. + it('fails authentication on the refresh', function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), + REFRESH_TOKEN_CALLBACK: () => { + return Promise.resolve({}); + } + } + }); + expect(async () => { + await client.db('test').collection('test').findOne(); + }).to.throw; + }); }); describe('3.6 Refresh Callback Returns Extra Data', function () { + before(async function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), + REFRESH_TOKEN_CALLBACK: createRefreshCallback('test_user1', 60, { foo: 'bar' }) + } + }); + await client.db('test').collection('test').findOne(); + await client.close(); + }); + // Clear the cache. // Create request callback that returns a valid token that will expire in a minute, and a refresh callback that returns data not conforming to the OIDCRequestTokenResult with extra field(s). // Create a client with the callbacks. @@ -405,167 +506,278 @@ describe('MONGODB-OIDC', function () { // Create a new client with the same callbacks. // Perform a find operation that fails. // Close the client. + it('fails authentication on the refresh', function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), + REFRESH_TOKEN_CALLBACK: createRefreshCallback('test_user1', 60, { foo: 'bar' }) + } + }); + expect(async () => { + await client.db('test').collection('test').findOne(); + }).to.throw; + }); }); - }); - describe('4. Cached Credentials', function () { - describe('4.1 Cache with refresh', function () { - // Clear the cache. - // Create a new client with a request callback that gives credentials that expire in on minute. - // Ensure that a find operation adds credentials to the cache. - // Close the client. - // Create a new client with the same request callback and a refresh callback. - // Ensure that a find operation results in a call to the refresh callback. - // Close the client. - }); + describe('4. Cached Credentials', function () { + describe('4.1 Cache with refresh', function () { + let refreshSpy; - describe('4.2 Cache with no refresh', function () { - // Clear the cache. - // Create a new client with a request callback that gives credentials that expire in one minute. - // Ensure that a find operation adds credentials to the cache. - // Close the client. - // Create a new client with the a request callback but no refresh callback. - // Ensure that a find operation results in a call to the request callback. - // Close the client. - }); + before(async function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60) + } + }); + await client.db('test').collection('test').findOne(); + await client.close(); + refreshSpy = sinon.spy(createRefreshCallback('test_user1', 60)); + }); + // Clear the cache. + // Create a new client with a request callback that gives credentials that expire in on minute. + // Ensure that a find operation adds credentials to the cache. + // Close the client. + // Create a new client with the same request callback and a refresh callback. + // Ensure that a find operation results in a call to the refresh callback. + // Close the client. + it('successfully authenticates and calls the refresh callback', async function () { + // Ensure credentials added to the cache. + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), + REFRESH_TOKEN_CALLBACK: refreshSpy + } + }); + await client.db('test').collection('test').findOne(); + expect(refreshSpy.calledOnce).to.be.true; + }); + }); - describe('4.3 Cache key includes callback', function () { - // Clear the cache. - // Create a new client with a request callback that does not give an `expiresInSeconds` value. - // Ensure that a find operation adds credentials to the cache. - // Close the client. - // Create a new client with a different request callback. - // Ensure that a find operation adds a new entry to the cache. - // Close the client. - }); + describe('4.2 Cache with no refresh', function () { + let requestSpy; - describe('4.4 Error clears cache', function () { - // Clear the cache. - // Create a new client with a valid request callback that gives credentials that expire within 5 minutes and a refresh callback that gives invalid credentials. - // Ensure that a find operation adds a new entry to the cache. - // Ensure that a subsequent find operation results in an error. - // Ensure that the cached token has been cleared. - // Close the client. - }); + before(async function () { + requestSpy = sinon.spy(createRefreshCallback('test_user1', 60)); + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: requestSpy + } + }); + await client.db('test').collection('test').findOne(); + await client.close(); + }); - describe('4.5 AWS Automatic workflow does not use cache', function () { - // Clear the cache. - // Create a new client that uses the AWS automatic workflow. - // Ensure that a find operation does not add credentials to the cache. - // Close the client. - }); - }); + // Clear the cache. + // Create a new client with a request callback that gives credentials that expire in one minute. + // Ensure that a find operation adds credentials to the cache. + // Close the client. + // Create a new client with the a request callback but no refresh callback. + // Ensure that a find operation results in a call to the request callback. + // Close the client. + it('successfully authenticates and calls only the request callback', async function () { + expect(cache.entries.size).to.equal(1); + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: requestSpy + } + }); + await client.db('test').collection('test').findOne(); + expect(requestSpy.calledTwice).to.be.true; + }); + }); - describe('5. Speculative Authentication', function () { - // Clear the cache. - // Create a client with a request callback that returns a valid token that will not expire soon. - // Set a fail point for saslStart commands of the form: - // - // { - // "configureFailPoint": "failCommand", - // "mode": { - // "times": 2 - // }, - // "data": { - // "failCommands": [ - // "saslStart" - // ], - // "errorCode": 18 - // } - // } - // - // Note - // - // The driver MUST either use a unique appName or explicitly remove the failCommand after the test to prevent leakage. - // - // Perform a find operation that succeeds. - // Close the client. - // Create a new client with the same properties without clearing the cache. - // Set a fail point for saslStart commands. - // Perform a find operation that succeeds. - // Close the client. - }); + describe('4.3 Cache key includes callback', function () { + const firstRequestCallback = createRequestCallback('test_user1'); + const secondRequestCallback = createRequestCallback('test_user'); + + before(async function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: firstRequestCallback + } + }); + await client.db('test').collection('test').findOne(); + await client.close(); + }); + + // Clear the cache. + // Create a new client with a request callback that does not give an `expiresInSeconds` value. + // Ensure that a find operation adds credentials to the cache. + // Close the client. + // Create a new client with a different request callback. + // Ensure that a find operation adds a new entry to the cache. + // Close the client. + it('includes the callback functions in the cache', async function () { + expect(cache.entries.size).to.equal(1); + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: secondRequestCallback + } + }); + await client.db('test').collection('test').findOne(); + expect(cache.entries.size).to.equal(2); + }); + }); + + describe('4.4 Error clears cache', function () { + before(function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 300), + REFRESH_TOKEN_CALLBACK: () => { + return Promise.resolve({}); + } + } + }); + collection = client.db('test').collection('test'); + }); + + // Clear the cache. + // Create a new client with a valid request callback that gives credentials that expire within 5 minutes and a refresh callback that gives invalid credentials. + // Ensure that a find operation adds a new entry to the cache. + // Ensure that a subsequent find operation results in an error. + // Ensure that the cached token has been cleared. + // Close the client. + it('clears the cache on authentication error', async function () { + await collection.findOne(); + expect(cache.entries.size).to.equal(1); + expect(async () => { + await collection.findOne(); + }).to.throw; + expect(cache.entries).to.be.empty; + }); + }); + + describe('4.5 AWS Automatic workflow does not use cache', function () { + before(function () { + client = new MongoClient( + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws' + ); + collection = client.db('test').collection('test'); + }); + + // Clear the cache. + // Create a new client that uses the AWS automatic workflow. + // Ensure that a find operation does not add credentials to the cache. + // Close the client. + it('authenticates with no cache usage', async function () { + await collection.findOne(); + expect(cache.entries).to.be.empty; + }); + }); + }); - describe('6. Reauthentication', function () { - describe('6.1 Succeeds', function () { + describe('5. Speculative Authentication', function () { // Clear the cache. - // Create request and refresh callbacks that return valid credentials that will not expire soon. - // Create a client with the callbacks and an event listener. The following assumes that the driver does not emit saslStart or saslContinue events. If the driver does emit those events, ignore/filter them for the purposes of this test. - // Perform a find operation that succeeds. - // Assert that the refresh callback has not been called. - // Clear the listener state if possible. - // Force a reauthenication using a failCommand of the form: + // Create a client with a request callback that returns a valid token that will not expire soon. + // Set a fail point for saslStart commands of the form: // // { // "configureFailPoint": "failCommand", // "mode": { - // "times": 1 + // "times": 2 // }, // "data": { // "failCommands": [ - // "find" + // "saslStart" // ], - // "errorCode": 391 + // "errorCode": 18 // } // } // // Note // - // the driver MUST either use a unique appName or explicitly remove the failCommand after the test to prevent leakage. + // The driver MUST either use a unique appName or explicitly remove the failCommand after the test to prevent leakage. // - // Perform another find operation that succeeds. - // Assert that the refresh callback has been called once, if possible. - // Assert that the ordering of list started events is [find], , find. Note that if the listener stat could not be cleared then there will and be extra find command. - // Assert that the list of command succeeded events is [find]. - // Assert that a find operation failed once during the command execution. - // Close the client. - }); - - describe('6.2 Retries and Succeeds with Cache', function () { - // Clear the cache. - // Create request and refresh callbacks that return valid credentials that will not expire soon. // Perform a find operation that succeeds. - // Force a reauthenication using a failCommand of the form: - // - // { - // "configureFailPoint": "failCommand", - // "mode": { - // "times": 2 - // }, - // "data": { - // "failCommands": [ - // "find", "saslStart" - // ], - // "errorCode": 391 - // } - // } - // + // Close the client. + // Create a new client with the same properties without clearing the cache. + // Set a fail point for saslStart commands. // Perform a find operation that succeeds. // Close the client. }); - describe('6.3 Retries and Fails with no Cache', function () { - // Clear the cache. - // Create request and refresh callbacks that return valid credentials that will not expire soon. - // Perform a find operation that succeeds (to force a speculative auth). - // Clear the cache. - // Force a reauthenication using a failCommand of the form: - // - // { - // "configureFailPoint": "failCommand", - // "mode": { - // "times": 2 - // }, - // "data": { - // "failCommands": [ - // "find", "saslStart" - // ], - // "errorCode": 391 - // } - // } - // - // Perform a find operation that fails. - // Close the client. + describe('6. Reauthentication', function () { + describe('6.1 Succeeds', function () { + // Clear the cache. + // Create request and refresh callbacks that return valid credentials that will not expire soon. + // Create a client with the callbacks and an event listener. The following assumes that the driver does not emit saslStart or saslContinue events. If the driver does emit those events, ignore/filter them for the purposes of this test. + // Perform a find operation that succeeds. + // Assert that the refresh callback has not been called. + // Clear the listener state if possible. + // Force a reauthenication using a failCommand of the form: + // + // { + // "configureFailPoint": "failCommand", + // "mode": { + // "times": 1 + // }, + // "data": { + // "failCommands": [ + // "find" + // ], + // "errorCode": 391 + // } + // } + // + // Note + // + // the driver MUST either use a unique appName or explicitly remove the failCommand after the test to prevent leakage. + // + // Perform another find operation that succeeds. + // Assert that the refresh callback has been called once, if possible. + // Assert that the ordering of list started events is [find], , find. Note that if the listener stat could not be cleared then there will and be extra find command. + // Assert that the list of command succeeded events is [find]. + // Assert that a find operation failed once during the command execution. + // Close the client. + }); + + describe('6.2 Retries and Succeeds with Cache', function () { + // Clear the cache. + // Create request and refresh callbacks that return valid credentials that will not expire soon. + // Perform a find operation that succeeds. + // Force a reauthenication using a failCommand of the form: + // + // { + // "configureFailPoint": "failCommand", + // "mode": { + // "times": 2 + // }, + // "data": { + // "failCommands": [ + // "find", "saslStart" + // ], + // "errorCode": 391 + // } + // } + // + // Perform a find operation that succeeds. + // Close the client. + }); + + describe('6.3 Retries and Fails with no Cache', function () { + // Clear the cache. + // Create request and refresh callbacks that return valid credentials that will not expire soon. + // Perform a find operation that succeeds (to force a speculative auth). + // Clear the cache. + // Force a reauthenication using a failCommand of the form: + // + // { + // "configureFailPoint": "failCommand", + // "mode": { + // "times": 2 + // }, + // "data": { + // "failCommands": [ + // "find", "saslStart" + // ], + // "errorCode": 391 + // } + // } + // + // Perform a find operation that fails. + // Close the client. + }); }); }); }); From 4fef6d422322597b11a04bd60157941299e0a7bf Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 17 Apr 2023 14:45:06 -0400 Subject: [PATCH 11/93] test: update connection string regex --- src/connection_string.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/connection_string.ts b/src/connection_string.ts index 21ce5aaa22d..b3bc3f7b47a 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -313,7 +313,7 @@ export function parseOptions( const uriMechanismProperties = urlOptions.get('authMechanismProperties'); if (uriMechanismProperties) { for (const property of uriMechanismProperties) { - if (property.includes('ALLOWED_HOSTS:')) { + if (/(^|,)ALLOWED_HOSTS:/.test(property)) { throw new MongoParseError( 'Auth mechanism property ALLOWED_HOSTS is not allowed in the connection string.' ); From 0a5453d8ae059ba9352e6d41545a9050196acbb7 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 17 Apr 2023 14:48:10 -0400 Subject: [PATCH 12/93] fix: normal string testing --- src/cmap/auth/mongo_credentials.ts | 3 +-- src/utils.ts | 11 ----------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/cmap/auth/mongo_credentials.ts b/src/cmap/auth/mongo_credentials.ts index 7a240cd53ef..8014a95f8de 100644 --- a/src/cmap/auth/mongo_credentials.ts +++ b/src/cmap/auth/mongo_credentials.ts @@ -5,7 +5,6 @@ import { MongoInvalidArgumentError, MongoMissingCredentialsError } from '../../error'; -import { isString } from '../../utils'; import { GSSAPICanonicalizationValue } from './gssapi'; import type { OIDCRefreshFunction, OIDCRequestFunction } from './mongodb_oidc'; import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism } from './providers'; @@ -210,7 +209,7 @@ export class MongoCredentials { throw new MongoInvalidArgumentError(ALLOWED_HOSTS_ERROR); } for (const host of hosts) { - if (!isString(host)) { + if (typeof host !== 'string') { throw new MongoInvalidArgumentError(ALLOWED_HOSTS_ERROR); } } diff --git a/src/utils.ts b/src/utils.ts index 112c153cc06..95bf757af2d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -30,8 +30,6 @@ import type { Topology } from './sdam/topology'; import type { ClientSession } from './sessions'; import { WriteConcern } from './write_concern'; -const OBJECT_STRING = '[object String]'; - /** * MongoDB Driver style callback * @public @@ -60,15 +58,6 @@ export const ByteUtils = { } }; -/** - * Use this to test if an object is a string. This is because - * typeof new String('test') is 'object' and not 'string'. - * @internal - */ -export function isString(value: any): boolean { - return Object.prototype.toString.call(value) === OBJECT_STRING; -} - /** * Throws if collectionName is not a valid mongodb collection namespace. * @internal From 23fd2c57d0b272138d0a0a01f3eea066da54dd7c Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 17 Apr 2023 15:18:57 -0400 Subject: [PATCH 13/93] test: add debug --- .../auth/mongodb_oidc/callback_workflow.ts | 1 + test/manual/mongodb_oidc.prose.test.ts | 55 +++++++++++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 73de7041ab7..41ed712f33d 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -74,6 +74,7 @@ export class CallbackWorkflow implements Workflow { ); if (entry) { // Check if the entry is not expired and if we are reauthenticating. + console.log('entry valid', entry.isValid()); if (!reauthenticate && entry.isValid()) { // Skip step one and execute the step two saslContinue. try { diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index e13b528e197..be52c268b3f 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -349,9 +349,9 @@ describe('MONGODB-OIDC', function () { // Close the client. it('successfully authenticates with the request and refresh callbacks', async function () { await collection.findOne(); - expect(requestSpy.calledOnce).to.be.true; + expect(requestSpy).to.be.calledOnce; await collection.findOne(); - expect(refreshSpy.calledOnce).to.be.true; + expect(refreshSpy).to.be.calledOnce; }); }); @@ -549,7 +549,7 @@ describe('MONGODB-OIDC', function () { } }); await client.db('test').collection('test').findOne(); - expect(refreshSpy.calledOnce).to.be.true; + expect(refreshSpy).to.be.calledOnce; }); }); @@ -582,7 +582,7 @@ describe('MONGODB-OIDC', function () { } }); await client.db('test').collection('test').findOne(); - expect(requestSpy.calledTwice).to.be.true; + expect(requestSpy).to.be.calledTwice; }); }); @@ -668,6 +668,41 @@ describe('MONGODB-OIDC', function () { }); describe('5. Speculative Authentication', function () { + const setupFailPoint = async () => { + return await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 2 + }, + data: { + failCommands: ['saslStart'], + errorCode: 18 + } + }); + }; + + const removeFailPoint = async () => { + return await client.db().admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); + }; + + before(async function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 300) + } + }); + await setupFailPoint(); + collection = client.db('test').collection('test'); + await collection.findOne(); + await removeFailPoint(); + }); + // Clear the cache. // Create a client with a request callback that returns a valid token that will not expire soon. // Set a fail point for saslStart commands of the form: @@ -695,6 +730,18 @@ describe('MONGODB-OIDC', function () { // Set a fail point for saslStart commands. // Perform a find operation that succeeds. // Close the client. + it('successfully speculative authenticates', async function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 300) + } + }); + await setupFailPoint(); + expect(async () => { + await client.db('test').collection('test').findOne(); + }).to.not.throw; + await removeFailPoint(); + }); }); describe('6. Reauthentication', function () { From 640f31e6d157cde38ea958b8f4e624377091287d Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 17 Apr 2023 15:24:36 -0400 Subject: [PATCH 14/93] fix: fix formatting --- test/manual/mongodb_oidc.prose.test.ts | 528 +++++++++++++------------ 1 file changed, 275 insertions(+), 253 deletions(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index be52c268b3f..1da59fb93a0 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -518,313 +518,335 @@ describe('MONGODB-OIDC', function () { }).to.throw; }); }); + }); - describe('4. Cached Credentials', function () { - describe('4.1 Cache with refresh', function () { - let refreshSpy; + describe('4. Cached Credentials', function () { + let client: MongoClient; + let collection: Collection; - before(async function () { - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60) - } - }); - await client.db('test').collection('test').findOne(); - await client.close(); - refreshSpy = sinon.spy(createRefreshCallback('test_user1', 60)); + beforeEach(function () { + cache.clear(); + }); + + afterEach(async function () { + await client?.close(); + }); + + describe('4.1 Cache with refresh', function () { + let refreshSpy; + + before(async function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60) + } }); - // Clear the cache. - // Create a new client with a request callback that gives credentials that expire in on minute. - // Ensure that a find operation adds credentials to the cache. - // Close the client. - // Create a new client with the same request callback and a refresh callback. - // Ensure that a find operation results in a call to the refresh callback. - // Close the client. - it('successfully authenticates and calls the refresh callback', async function () { - // Ensure credentials added to the cache. - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), - REFRESH_TOKEN_CALLBACK: refreshSpy - } - }); - await client.db('test').collection('test').findOne(); - expect(refreshSpy).to.be.calledOnce; + await client.db('test').collection('test').findOne(); + await client.close(); + refreshSpy = sinon.spy(createRefreshCallback('test_user1', 60)); + }); + // Clear the cache. + // Create a new client with a request callback that gives credentials that expire in on minute. + // Ensure that a find operation adds credentials to the cache. + // Close the client. + // Create a new client with the same request callback and a refresh callback. + // Ensure that a find operation results in a call to the refresh callback. + // Close the client. + it('successfully authenticates and calls the refresh callback', async function () { + // Ensure credentials added to the cache. + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), + REFRESH_TOKEN_CALLBACK: refreshSpy + } }); + await client.db('test').collection('test').findOne(); + expect(refreshSpy).to.be.calledOnce; }); + }); - describe('4.2 Cache with no refresh', function () { - let requestSpy; + describe('4.2 Cache with no refresh', function () { + let requestSpy; - before(async function () { - requestSpy = sinon.spy(createRefreshCallback('test_user1', 60)); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: requestSpy - } - }); - await client.db('test').collection('test').findOne(); - await client.close(); + before(async function () { + requestSpy = sinon.spy(createRefreshCallback('test_user1', 60)); + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: requestSpy + } }); + await client.db('test').collection('test').findOne(); + await client.close(); + }); - // Clear the cache. - // Create a new client with a request callback that gives credentials that expire in one minute. - // Ensure that a find operation adds credentials to the cache. - // Close the client. - // Create a new client with the a request callback but no refresh callback. - // Ensure that a find operation results in a call to the request callback. - // Close the client. - it('successfully authenticates and calls only the request callback', async function () { - expect(cache.entries.size).to.equal(1); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: requestSpy - } - }); - await client.db('test').collection('test').findOne(); - expect(requestSpy).to.be.calledTwice; + // Clear the cache. + // Create a new client with a request callback that gives credentials that expire in one minute. + // Ensure that a find operation adds credentials to the cache. + // Close the client. + // Create a new client with the a request callback but no refresh callback. + // Ensure that a find operation results in a call to the request callback. + // Close the client. + it('successfully authenticates and calls only the request callback', async function () { + expect(cache.entries.size).to.equal(1); + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: requestSpy + } }); + await client.db('test').collection('test').findOne(); + expect(requestSpy).to.be.calledTwice; }); + }); - describe('4.3 Cache key includes callback', function () { - const firstRequestCallback = createRequestCallback('test_user1'); - const secondRequestCallback = createRequestCallback('test_user'); + describe('4.3 Cache key includes callback', function () { + const firstRequestCallback = createRequestCallback('test_user1'); + const secondRequestCallback = createRequestCallback('test_user'); - before(async function () { - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: firstRequestCallback - } - }); - await client.db('test').collection('test').findOne(); - await client.close(); + before(async function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: firstRequestCallback + } }); + await client.db('test').collection('test').findOne(); + await client.close(); + }); - // Clear the cache. - // Create a new client with a request callback that does not give an `expiresInSeconds` value. - // Ensure that a find operation adds credentials to the cache. - // Close the client. - // Create a new client with a different request callback. - // Ensure that a find operation adds a new entry to the cache. - // Close the client. - it('includes the callback functions in the cache', async function () { - expect(cache.entries.size).to.equal(1); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: secondRequestCallback - } - }); - await client.db('test').collection('test').findOne(); - expect(cache.entries.size).to.equal(2); + // Clear the cache. + // Create a new client with a request callback that does not give an `expiresInSeconds` value. + // Ensure that a find operation adds credentials to the cache. + // Close the client. + // Create a new client with a different request callback. + // Ensure that a find operation adds a new entry to the cache. + // Close the client. + it('includes the callback functions in the cache', async function () { + expect(cache.entries.size).to.equal(1); + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: secondRequestCallback + } }); + await client.db('test').collection('test').findOne(); + expect(cache.entries.size).to.equal(2); }); + }); - describe('4.4 Error clears cache', function () { - before(function () { - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 300), - REFRESH_TOKEN_CALLBACK: () => { - return Promise.resolve({}); - } + describe('4.4 Error clears cache', function () { + before(function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 300), + REFRESH_TOKEN_CALLBACK: () => { + return Promise.resolve({}); } - }); - collection = client.db('test').collection('test'); + } }); + collection = client.db('test').collection('test'); + }); - // Clear the cache. - // Create a new client with a valid request callback that gives credentials that expire within 5 minutes and a refresh callback that gives invalid credentials. - // Ensure that a find operation adds a new entry to the cache. - // Ensure that a subsequent find operation results in an error. - // Ensure that the cached token has been cleared. - // Close the client. - it('clears the cache on authentication error', async function () { + // Clear the cache. + // Create a new client with a valid request callback that gives credentials that expire within 5 minutes and a refresh callback that gives invalid credentials. + // Ensure that a find operation adds a new entry to the cache. + // Ensure that a subsequent find operation results in an error. + // Ensure that the cached token has been cleared. + // Close the client. + it('clears the cache on authentication error', async function () { + await collection.findOne(); + expect(cache.entries.size).to.equal(1); + expect(async () => { await collection.findOne(); - expect(cache.entries.size).to.equal(1); - expect(async () => { - await collection.findOne(); - }).to.throw; - expect(cache.entries).to.be.empty; - }); + }).to.throw; + expect(cache.entries).to.be.empty; }); + }); - describe('4.5 AWS Automatic workflow does not use cache', function () { - before(function () { - client = new MongoClient( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws' - ); - collection = client.db('test').collection('test'); - }); + describe('4.5 AWS Automatic workflow does not use cache', function () { + before(function () { + client = new MongoClient( + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws' + ); + collection = client.db('test').collection('test'); + }); - // Clear the cache. - // Create a new client that uses the AWS automatic workflow. - // Ensure that a find operation does not add credentials to the cache. - // Close the client. - it('authenticates with no cache usage', async function () { - await collection.findOne(); - expect(cache.entries).to.be.empty; - }); + // Clear the cache. + // Create a new client that uses the AWS automatic workflow. + // Ensure that a find operation does not add credentials to the cache. + // Close the client. + it('authenticates with no cache usage', async function () { + await collection.findOne(); + expect(cache.entries).to.be.empty; }); }); + }); - describe('5. Speculative Authentication', function () { - const setupFailPoint = async () => { - return await client - .db() - .admin() - .command({ - configureFailPoint: 'failCommand', - mode: { - times: 2 - }, - data: { - failCommands: ['saslStart'], - errorCode: 18 - } - }); - }; + describe('5. Speculative Authentication', function () { + let client: MongoClient; + let collection: Collection; - const removeFailPoint = async () => { - return await client.db().admin().command({ - configureFailPoint: 'failCommand', - mode: 'off' - }); - }; + beforeEach(function () { + cache.clear(); + }); - before(async function () { - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 300) + afterEach(async function () { + await client?.close(); + }); + + const setupFailPoint = async () => { + return await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 2 + }, + data: { + failCommands: ['saslStart'], + errorCode: 18 } }); - await setupFailPoint(); - collection = client.db('test').collection('test'); - await collection.findOne(); - await removeFailPoint(); + }; + + const removeFailPoint = async () => { + return await client.db().admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' }); + }; + + before(async function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 300) + } + }); + await setupFailPoint(); + collection = client.db('test').collection('test'); + await collection.findOne(); + await removeFailPoint(); + }); + // Clear the cache. + // Create a client with a request callback that returns a valid token that will not expire soon. + // Set a fail point for saslStart commands of the form: + // + // { + // "configureFailPoint": "failCommand", + // "mode": { + // "times": 2 + // }, + // "data": { + // "failCommands": [ + // "saslStart" + // ], + // "errorCode": 18 + // } + // } + // + // Note + // + // The driver MUST either use a unique appName or explicitly remove the failCommand after the test to prevent leakage. + // + // Perform a find operation that succeeds. + // Close the client. + // Create a new client with the same properties without clearing the cache. + // Set a fail point for saslStart commands. + // Perform a find operation that succeeds. + // Close the client. + it('successfully speculative authenticates', async function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 300) + } + }); + await setupFailPoint(); + expect(async () => { + await client.db('test').collection('test').findOne(); + }).to.not.throw; + await removeFailPoint(); + }); + }); + + describe('6. Reauthentication', function () { + describe('6.1 Succeeds', function () { // Clear the cache. - // Create a client with a request callback that returns a valid token that will not expire soon. - // Set a fail point for saslStart commands of the form: + // Create request and refresh callbacks that return valid credentials that will not expire soon. + // Create a client with the callbacks and an event listener. The following assumes that the driver does not emit saslStart or saslContinue events. If the driver does emit those events, ignore/filter them for the purposes of this test. + // Perform a find operation that succeeds. + // Assert that the refresh callback has not been called. + // Clear the listener state if possible. + // Force a reauthenication using a failCommand of the form: // // { // "configureFailPoint": "failCommand", // "mode": { - // "times": 2 + // "times": 1 // }, // "data": { // "failCommands": [ - // "saslStart" + // "find" // ], - // "errorCode": 18 + // "errorCode": 391 // } // } // // Note // - // The driver MUST either use a unique appName or explicitly remove the failCommand after the test to prevent leakage. + // the driver MUST either use a unique appName or explicitly remove the failCommand after the test to prevent leakage. // - // Perform a find operation that succeeds. + // Perform another find operation that succeeds. + // Assert that the refresh callback has been called once, if possible. + // Assert that the ordering of list started events is [find], , find. Note that if the listener stat could not be cleared then there will and be extra find command. + // Assert that the list of command succeeded events is [find]. + // Assert that a find operation failed once during the command execution. // Close the client. - // Create a new client with the same properties without clearing the cache. - // Set a fail point for saslStart commands. + }); + + describe('6.2 Retries and Succeeds with Cache', function () { + // Clear the cache. + // Create request and refresh callbacks that return valid credentials that will not expire soon. + // Perform a find operation that succeeds. + // Force a reauthenication using a failCommand of the form: + // + // { + // "configureFailPoint": "failCommand", + // "mode": { + // "times": 2 + // }, + // "data": { + // "failCommands": [ + // "find", "saslStart" + // ], + // "errorCode": 391 + // } + // } + // // Perform a find operation that succeeds. // Close the client. - it('successfully speculative authenticates', async function () { - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 300) - } - }); - await setupFailPoint(); - expect(async () => { - await client.db('test').collection('test').findOne(); - }).to.not.throw; - await removeFailPoint(); - }); }); - describe('6. Reauthentication', function () { - describe('6.1 Succeeds', function () { - // Clear the cache. - // Create request and refresh callbacks that return valid credentials that will not expire soon. - // Create a client with the callbacks and an event listener. The following assumes that the driver does not emit saslStart or saslContinue events. If the driver does emit those events, ignore/filter them for the purposes of this test. - // Perform a find operation that succeeds. - // Assert that the refresh callback has not been called. - // Clear the listener state if possible. - // Force a reauthenication using a failCommand of the form: - // - // { - // "configureFailPoint": "failCommand", - // "mode": { - // "times": 1 - // }, - // "data": { - // "failCommands": [ - // "find" - // ], - // "errorCode": 391 - // } - // } - // - // Note - // - // the driver MUST either use a unique appName or explicitly remove the failCommand after the test to prevent leakage. - // - // Perform another find operation that succeeds. - // Assert that the refresh callback has been called once, if possible. - // Assert that the ordering of list started events is [find], , find. Note that if the listener stat could not be cleared then there will and be extra find command. - // Assert that the list of command succeeded events is [find]. - // Assert that a find operation failed once during the command execution. - // Close the client. - }); - - describe('6.2 Retries and Succeeds with Cache', function () { - // Clear the cache. - // Create request and refresh callbacks that return valid credentials that will not expire soon. - // Perform a find operation that succeeds. - // Force a reauthenication using a failCommand of the form: - // - // { - // "configureFailPoint": "failCommand", - // "mode": { - // "times": 2 - // }, - // "data": { - // "failCommands": [ - // "find", "saslStart" - // ], - // "errorCode": 391 - // } - // } - // - // Perform a find operation that succeeds. - // Close the client. - }); - - describe('6.3 Retries and Fails with no Cache', function () { - // Clear the cache. - // Create request and refresh callbacks that return valid credentials that will not expire soon. - // Perform a find operation that succeeds (to force a speculative auth). - // Clear the cache. - // Force a reauthenication using a failCommand of the form: - // - // { - // "configureFailPoint": "failCommand", - // "mode": { - // "times": 2 - // }, - // "data": { - // "failCommands": [ - // "find", "saslStart" - // ], - // "errorCode": 391 - // } - // } - // - // Perform a find operation that fails. - // Close the client. - }); + describe('6.3 Retries and Fails with no Cache', function () { + // Clear the cache. + // Create request and refresh callbacks that return valid credentials that will not expire soon. + // Perform a find operation that succeeds (to force a speculative auth). + // Clear the cache. + // Force a reauthenication using a failCommand of the form: + // + // { + // "configureFailPoint": "failCommand", + // "mode": { + // "times": 2 + // }, + // "data": { + // "failCommands": [ + // "find", "saslStart" + // ], + // "errorCode": 391 + // } + // } + // + // Perform a find operation that fails. + // Close the client. }); }); }); From b020c7da08212b75c216a161ab064596a363ccb8 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 17 Apr 2023 15:49:48 -0400 Subject: [PATCH 15/93] test: fix sinon chai --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 2 +- test/manual/mongodb_oidc.prose.test.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 41ed712f33d..08d0753b027 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -72,9 +72,9 @@ export class CallbackWorkflow implements Workflow { request || null, refresh || null ); + console.log('VIEW ENTRY', entry, entry?.isValid()); if (entry) { // Check if the entry is not expired and if we are reauthenticating. - console.log('entry valid', entry.isValid()); if (!reauthenticate && entry.isValid()) { // Skip step one and execute the step two saslContinue. try { diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 1da59fb93a0..b585daefab9 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -349,9 +349,9 @@ describe('MONGODB-OIDC', function () { // Close the client. it('successfully authenticates with the request and refresh callbacks', async function () { await collection.findOne(); - expect(requestSpy).to.be.calledOnce; + expect(requestSpy).to.have.been.calledOnce; await collection.findOne(); - expect(refreshSpy).to.be.calledOnce; + expect(refreshSpy).to.have.been.calledOnce; }); }); @@ -561,7 +561,7 @@ describe('MONGODB-OIDC', function () { } }); await client.db('test').collection('test').findOne(); - expect(refreshSpy).to.be.calledOnce; + expect(refreshSpy).to.have.been.calledOnce; }); }); @@ -569,7 +569,7 @@ describe('MONGODB-OIDC', function () { let requestSpy; before(async function () { - requestSpy = sinon.spy(createRefreshCallback('test_user1', 60)); + requestSpy = sinon.spy(createRequestCallback('test_user1', 60)); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { REQUEST_TOKEN_CALLBACK: requestSpy @@ -594,13 +594,13 @@ describe('MONGODB-OIDC', function () { } }); await client.db('test').collection('test').findOne(); - expect(requestSpy).to.be.calledTwice; + expect(requestSpy).to.have.been.calledTwice; }); }); describe('4.3 Cache key includes callback', function () { const firstRequestCallback = createRequestCallback('test_user1'); - const secondRequestCallback = createRequestCallback('test_user'); + const secondRequestCallback = createRequestCallback('test_user1'); before(async function () { client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { From 2402d3ae33eed67043a78344c37645a3ac5bd4cd Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 17 Apr 2023 16:08:28 -0400 Subject: [PATCH 16/93] test: fix test env --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7590e18dec0..91cb2e0c06b 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ "check:atlas": "mocha --config test/manual/mocharc.json test/manual/atlas_connectivity.test.js", "check:adl": "mocha --config test/mocha_mongodb.json test/manual/atlas-data-lake-testing", "check:aws": "nyc mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_aws.test.ts", - "check:oidc": "mocha --config test/manual/mocharc.json test/manual/mongodb_oidc.prose.test.ts", + "check:oidc": "mocha --config test/mocha_mongodb.json test/manual/mongodb_oidc.prose.test.ts", "check:ocsp": "mocha --config test/manual/mocharc.json test/manual/ocsp_support.test.js", "check:kerberos": "nyc mocha --config test/manual/mocharc.json test/manual/kerberos.test.ts", "check:tls": "mocha --config test/manual/mocharc.json test/manual/tls_support.test.js", From 1d9af2d3674c4c14a1bc07bc75301ce7452d270d Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 18 Apr 2023 09:27:24 -0400 Subject: [PATCH 17/93] test: adding reauth tests --- .../auth/mongodb_oidc/callback_workflow.ts | 1 - test/manual/mongodb_oidc.prose.test.ts | 92 +++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 08d0753b027..73de7041ab7 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -72,7 +72,6 @@ export class CallbackWorkflow implements Workflow { request || null, refresh || null ); - console.log('VIEW ENTRY', entry, entry?.isValid()); if (entry) { // Check if the entry is not expired and if we are reauthenticating. if (!reauthenticate && entry.isValid()) { diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index b585daefab9..d06dfc2edbf 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -6,6 +6,9 @@ import * as sinon from 'sinon'; import { Collection, + CommandFailedEvent, + CommandStartedEvent, + CommandSucceededEvent, MongoClient, OIDC_WORKFLOWS, OIDCClientInfo, @@ -691,6 +694,7 @@ describe('MONGODB-OIDC', function () { await client?.close(); }); + // Sets up the fail point for saslStart. const setupFailPoint = async () => { return await client .db() @@ -707,6 +711,7 @@ describe('MONGODB-OIDC', function () { }); }; + // Removes the fail point. const removeFailPoint = async () => { return await client.db().admin().command({ configureFailPoint: 'failCommand', @@ -768,7 +773,87 @@ describe('MONGODB-OIDC', function () { }); describe('6. Reauthentication', function () { + let client: MongoClient; + let collection: Collection; + + beforeEach(function () { + cache.clear(); + }); + + afterEach(async function () { + await client?.close(); + }); + + // Sets up the fail point for the find to reauthenticate. + const setupFailPoint = async () => { + return await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 1 + }, + data: { + failCommands: ['find'], + errorCode: 391 + } + }); + }; + + // Removes the fail point. + const removeFailPoint = async () => { + return await client.db().admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); + }; + describe('6.1 Succeeds', function () { + const refreshSpy = sinon.spy(createRefreshCallback('test_user1', 600)); + let commandStartedEvents: CommandStartedEvent[]; + let commandSucceededEvents: CommandSucceededEvent[]; + let commandFailedEvents: CommandFailedEvent[]; + + const commandStartedListener = event => { + commandStartedEvents.push(event); + }; + const commandSucceededListener = event => { + commandSucceededEvents.push(event); + }; + const commandFailedListener = event => { + commandFailedEvents.push(event); + }; + + const resetEvents = () => { + commandStartedEvents = []; + commandSucceededEvents = []; + commandFailedEvents = []; + }; + + before(async function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 600), + REFRESH_TOKEN_CALLBACK: refreshSpy + }, + monitorCommands: true + }); + collection = client.db('test').collection('test'); + await collection.findOne(); + expect(refreshSpy).to.not.be.called; + resetEvents(); + await setupFailPoint(); + client.on('commandStarted', commandStartedListener); + client.on('commandSucceeded', commandSucceededListener); + client.on('commandFailed', commandFailedListener); + }); + + after(async function () { + resetEvents(); + await removeFailPoint(); + }); + // Clear the cache. // Create request and refresh callbacks that return valid credentials that will not expire soon. // Create a client with the callbacks and an event listener. The following assumes that the driver does not emit saslStart or saslContinue events. If the driver does emit those events, ignore/filter them for the purposes of this test. @@ -800,6 +885,13 @@ describe('MONGODB-OIDC', function () { // Assert that the list of command succeeded events is [find]. // Assert that a find operation failed once during the command execution. // Close the client. + it('successfully reauthenticates', async function () { + await collection.findOne(); + expect(refreshSpy).to.have.been.calledOnce; + expect(commandStartedEvents.map(event => event.commandName)).to.equal(['find', 'find']); + expect(commandStartedEvents.map(event => event.commandName)).to.equal(['find']); + expect(commandStartedEvents.map(event => event.commandName)).to.equal(['find']); + }); }); describe('6.2 Retries and Succeeds with Cache', function () { From 5b9f9554331898b8457e237ab34d4b7630e206c7 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 18 Apr 2023 09:48:42 -0400 Subject: [PATCH 18/93] test: finish reauth tests --- test/manual/mongodb_oidc.prose.test.ts | 111 +++++++++++++++++++++---- 1 file changed, 94 insertions(+), 17 deletions(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index d06dfc2edbf..c88ae037e30 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -784,23 +784,6 @@ describe('MONGODB-OIDC', function () { await client?.close(); }); - // Sets up the fail point for the find to reauthenticate. - const setupFailPoint = async () => { - return await client - .db() - .admin() - .command({ - configureFailPoint: 'failCommand', - mode: { - times: 1 - }, - data: { - failCommands: ['find'], - errorCode: 391 - } - }); - }; - // Removes the fail point. const removeFailPoint = async () => { return await client.db().admin().command({ @@ -831,6 +814,23 @@ describe('MONGODB-OIDC', function () { commandFailedEvents = []; }; + // Sets up the fail point for the find to reauthenticate. + const setupFailPoint = async () => { + return await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 1 + }, + data: { + failCommands: ['find'], + errorCode: 391 + } + }); + }; + before(async function () { client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { @@ -895,6 +895,39 @@ describe('MONGODB-OIDC', function () { }); describe('6.2 Retries and Succeeds with Cache', function () { + // Sets up the fail point for the find and saslStart to reauthenticate. + const setupFailPoint = async () => { + return await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 2 + }, + data: { + failCommands: ['find', 'saslStart'], + errorCode: 391 + } + }); + }; + + before(async function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 600), + REFRESH_TOKEN_CALLBACK: createRefreshCallback('test_user1', 600) + } + }); + collection = client.db('test').collection('test'); + await collection.findOne(); + await setupFailPoint(); + }); + + after(async function () { + await removeFailPoint(); + }); + // Clear the cache. // Create request and refresh callbacks that return valid credentials that will not expire soon. // Perform a find operation that succeeds. @@ -915,9 +948,48 @@ describe('MONGODB-OIDC', function () { // // Perform a find operation that succeeds. // Close the client. + it('successfully reauthenticates with the cache', function () { + expect(async () => { + await collection.findOne(); + }).to.not.throw; + }); }); describe('6.3 Retries and Fails with no Cache', function () { + // Sets up the fail point for the find and saslStart to reauthenticate. + const setupFailPoint = async () => { + return await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 2 + }, + data: { + failCommands: ['find', 'saslStart'], + errorCode: 391 + } + }); + }; + + before(async function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 600), + REFRESH_TOKEN_CALLBACK: createRefreshCallback('test_user1', 600) + } + }); + collection = client.db('test').collection('test'); + await collection.findOne(); + cache.clear(); + await setupFailPoint(); + }); + + after(async function () { + await removeFailPoint(); + }); + // Clear the cache. // Create request and refresh callbacks that return valid credentials that will not expire soon. // Perform a find operation that succeeds (to force a speculative auth). @@ -939,6 +1011,11 @@ describe('MONGODB-OIDC', function () { // // Perform a find operation that fails. // Close the client. + it('fails reauthentication with no cache entries', function () { + expect(async () => { + await collection.findOne(); + }).to.throw; + }); }); }); }); From 3c74e55f85a91b0b4e7f7272d460e99ebf1f4382 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 18 Apr 2023 10:18:01 -0400 Subject: [PATCH 19/93] chore: adding console debug --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 73de7041ab7..10d689438e4 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -65,6 +65,8 @@ export class CallbackWorkflow implements Workflow { ): Promise { const request = credentials.mechanismProperties.REQUEST_TOKEN_CALLBACK; const refresh = credentials.mechanismProperties.REFRESH_TOKEN_CALLBACK; + console.log('REQUEST', request); + console.log('REFRESH', refresh); const entry = this.cache.getEntry( connection.address, @@ -72,6 +74,7 @@ export class CallbackWorkflow implements Workflow { request || null, refresh || null ); + console.log('CALLBACK_WORKFLOW', entry, entry?.isValid()); if (entry) { // Check if the entry is not expired and if we are reauthenticating. if (!reauthenticate && entry.isValid()) { From d504784a7e4c02b8627717a6e1731c043ed3ee91 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 18 Apr 2023 10:29:25 -0400 Subject: [PATCH 20/93] chore: more debug --- src/cmap/auth/mongodb_oidc.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index 21ca970eb13..d3d38aaa1bd 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -80,6 +80,7 @@ export class MongoDBOIDC extends AuthProvider { override async auth(authContext: AuthContext): Promise { const { connection, credentials, response, reauthenticating } = authContext; + console.log('RESPONSE', response); if (response?.speculativeAuthenticate) { return; } @@ -90,6 +91,7 @@ export class MongoDBOIDC extends AuthProvider { const workflow = getWorkflow(credentials); + console.log('AUTH', workflow); await workflow.execute(connection, credentials, reauthenticating); } @@ -109,6 +111,7 @@ export class MongoDBOIDC extends AuthProvider { const workflow = getWorkflow(credentials); const result = await workflow.speculativeAuth(); + console.log('PREPARE', workflow, result); return { ...handshakeDoc, ...result }; } } From 47d9b4d0dca62bfb6a138b1fb504a8abf0a9d50d Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 18 Apr 2023 10:54:12 -0400 Subject: [PATCH 21/93] fix: no assert, just run --- test/manual/mongodb_oidc.prose.test.ts | 94 ++++++++------------------ 1 file changed, 30 insertions(+), 64 deletions(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index c88ae037e30..e3542a9b4ce 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -107,9 +107,7 @@ describe('MONGODB-OIDC', function () { // Perform a find operation. that succeeds. // Close the client. it('successfully authenticates', function () { - expect(async () => { - await collection.findOne(); - }).to.not.throw; + await collection.findOne(); }); }); @@ -128,10 +126,8 @@ describe('MONGODB-OIDC', function () { // Create a client with a url of the form mongodb://test_user1@localhost/?authMechanism=MONGODB-OIDC and the OIDC request callback. // Perform a find operation that succeeds. // Close the client. - it('successfully authenticates', function () { - expect(async () => { - await collection.findOne(); - }).to.not.throw; + it('successfully authenticates', async function () { + await collection.findOne(); }); }); @@ -153,10 +149,8 @@ describe('MONGODB-OIDC', function () { // Create a client with a url of the form mongodb://test_user1@localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred and a valid OIDC request callback. // Perform a find operation that succeeds. // Close the client. - it('successfully authenticates', function () { - expect(async () => { - await collection.findOne(); - }).to.not.throw; + it('successfully authenticates', async function () { + await collection.findOne(); }); }); @@ -178,10 +172,8 @@ describe('MONGODB-OIDC', function () { // Create a client with a url of the form mongodb://test_user2@localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred and a valid OIDC request callback. // Perform a find operation that succeeds. // Close the client. - it('successfully authenticates', function () { - expect(async () => { - await collection.findOne(); - }).to.not.throw; + it('successfully authenticates', async function () { + await collection.findOne(); }); }); @@ -202,10 +194,8 @@ describe('MONGODB-OIDC', function () { // Create a client with a url of the form mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred and a valid OIDC request callback. // Assert that a find operation fails. // Close the client. - it('fails authentication', function () { - expect(async () => { - await collection.findOne(); - }).to.throw; + it('fails authentication', async function () { + await collection.findOne(); }); }); @@ -239,10 +229,8 @@ describe('MONGODB-OIDC', function () { // Create a client with a url of the form mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws. // Perform a find operation that succeeds. // Close the client. - it('successfully authenticates', function () { - expect(async () => { - await collection.findOne(); - }).to.not.throw; + it('successfully authenticates', async function () { + await collection.findOne(); }); }); @@ -257,10 +245,8 @@ describe('MONGODB-OIDC', function () { // Create a client with a url of the form mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred. // Perform a find operation that succeeds. // Close the client. - it('successfully authenticates', function () { - expect(async () => { - await collection.findOne(); - }).to.not.throw; + it('successfully authenticates', async function () { + await collection.findOne(); }); }); @@ -285,10 +271,8 @@ describe('MONGODB-OIDC', function () { // Perform a find operation that succeeds. // Close the client. // Restore the AWS_WEB_IDENTITY_TOKEN_FILE environment variable to the location of valid test_user2 credentials. - it('successfully authenticates', function () { - expect(async () => { - await collection.findOne(); - }).to.not.throw; + it('successfully authenticates', async function () { + await collection.findOne(); }); }); @@ -308,10 +292,8 @@ describe('MONGODB-OIDC', function () { // Create a client with a url of the form mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws, and an ALLOWED_HOSTS that is an empty list. // Assert that a find operation succeeds. // Close the client. - it('successfully authenticates', function () { - expect(async () => { - await collection.findOne(); - }).to.not.throw; + it('successfully authenticates', async function () { + await collection.findOne(); }); }); }); @@ -374,10 +356,8 @@ describe('MONGODB-OIDC', function () { // Create a client with a request callback that returns null. // Perform a find operation that fails. // Close the client. - it('fails authentication', function () { - expect(async () => { - await collection.findOne(); - }).to.throw; + it('fails authentication', async function () { + await collection.findOne(); }); }); @@ -401,9 +381,7 @@ describe('MONGODB-OIDC', function () { // Close the client. it('fails authentication on refresh', async function () { await collection.findOne(); - expect(async () => { - await collection.findOne(); - }).to.throw; + await collection.findOne(); }); }); @@ -444,10 +422,8 @@ describe('MONGODB-OIDC', function () { // Create a client with a request callback that returns data not conforming to the OIDCRequestTokenResult with extra field(s). // Perform a find operation that fails. // Close the client. - it('fails authentication', function () { - expect(async () => { - await collection.findOne(); - }).to.throw; + it('fails authentication', async function () { + await collection.findOne(); }); }); }); @@ -509,16 +485,14 @@ describe('MONGODB-OIDC', function () { // Create a new client with the same callbacks. // Perform a find operation that fails. // Close the client. - it('fails authentication on the refresh', function () { + it('fails authentication on the refresh', async function () { client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), REFRESH_TOKEN_CALLBACK: createRefreshCallback('test_user1', 60, { foo: 'bar' }) } }); - expect(async () => { - await client.db('test').collection('test').findOne(); - }).to.throw; + await client.db('test').collection('test').findOne(); }); }); }); @@ -656,9 +630,7 @@ describe('MONGODB-OIDC', function () { it('clears the cache on authentication error', async function () { await collection.findOne(); expect(cache.entries.size).to.equal(1); - expect(async () => { - await collection.findOne(); - }).to.throw; + await collection.findOne(); expect(cache.entries).to.be.empty; }); }); @@ -765,9 +737,7 @@ describe('MONGODB-OIDC', function () { } }); await setupFailPoint(); - expect(async () => { - await client.db('test').collection('test').findOne(); - }).to.not.throw; + await client.db('test').collection('test').findOne(); await removeFailPoint(); }); }); @@ -948,10 +918,8 @@ describe('MONGODB-OIDC', function () { // // Perform a find operation that succeeds. // Close the client. - it('successfully reauthenticates with the cache', function () { - expect(async () => { - await collection.findOne(); - }).to.not.throw; + it('successfully reauthenticates with the cache', async function () { + await collection.findOne(); }); }); @@ -1011,10 +979,8 @@ describe('MONGODB-OIDC', function () { // // Perform a find operation that fails. // Close the client. - it('fails reauthentication with no cache entries', function () { - expect(async () => { - await collection.findOne(); - }).to.throw; + it('fails reauthentication with no cache entries', async function () { + await collection.findOne(); }); }); }); From 3f65e9ecdbab8439d67446c21128a767fb13ae43 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 18 Apr 2023 11:02:40 -0400 Subject: [PATCH 22/93] fix: await --- test/manual/mongodb_oidc.prose.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index e3542a9b4ce..bb6b7aada1f 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -106,7 +106,7 @@ describe('MONGODB-OIDC', function () { // Create a client that uses the default OIDC url and the request callback. // Perform a find operation. that succeeds. // Close the client. - it('successfully authenticates', function () { + it('successfully authenticates', async function () { await collection.findOne(); }); }); From ec1b874268c1e0cd2aca5457c0a9ec4a3a628f2f Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 18 Apr 2023 13:21:19 -0400 Subject: [PATCH 23/93] test: fixing callback, aws --- .evergreen/config.in.yml | 2 +- .evergreen/config.yml | 2 +- src/cmap/auth/mongodb_oidc.ts | 1 - .../auth/mongodb_oidc/callback_workflow.ts | 1 + test/manual/mongodb_oidc.prose.test.ts | 18 ++++++++++++++---- 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.evergreen/config.in.yml b/.evergreen/config.in.yml index 1ad8a8824b4..cbe5edcc5e4 100644 --- a/.evergreen/config.in.yml +++ b/.evergreen/config.in.yml @@ -152,7 +152,7 @@ functions: ${PREPARE_SHELL} OIDC_TOKEN_DIR="/tmp/tokens" \ - AWS_WEB_IDENTITY_TOKEN_FILE="/tmp/tokens/test1" \ + AWS_WEB_IDENTITY_TOKEN_FILE="/tmp/tokens/test_user1" \ PROJECT_DIRECTORY="${PROJECT_DIRECTORY}" \ bash ${PROJECT_DIRECTORY}/.evergreen/run-oidc-tests.sh diff --git a/.evergreen/config.yml b/.evergreen/config.yml index d9fa9e999ed..a2d3316f411 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -123,7 +123,7 @@ functions: ${PREPARE_SHELL} OIDC_TOKEN_DIR="/tmp/tokens" \ - AWS_WEB_IDENTITY_TOKEN_FILE="/tmp/tokens/test1" \ + AWS_WEB_IDENTITY_TOKEN_FILE="/tmp/tokens/test_user1" \ PROJECT_DIRECTORY="${PROJECT_DIRECTORY}" \ bash ${PROJECT_DIRECTORY}/.evergreen/run-oidc-tests.sh run deployed aws lambda tests: diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index d3d38aaa1bd..4e1d6968ab9 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -111,7 +111,6 @@ export class MongoDBOIDC extends AuthProvider { const workflow = getWorkflow(credentials); const result = await workflow.speculativeAuth(); - console.log('PREPARE', workflow, result); return { ...handshakeDoc, ...result }; } } diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 10d689438e4..1242bc5034b 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -117,6 +117,7 @@ export class CallbackWorkflow implements Workflow { undefined ); const stepOne = BSON.deserialize(result.payload.buffer) as OIDCMechanismServerStep1; + console.log('STEP ONE', stepOne, result); // Call the request callback and finish auth. return this.requestAndFinish(connection, credentials, stepOne, result.conversationId); } diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index bb6b7aada1f..cafea4a7af1 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -10,6 +10,7 @@ import { CommandStartedEvent, CommandSucceededEvent, MongoClient, + MongoServerError, OIDC_WORKFLOWS, OIDCClientInfo, OIDCMechanismServerStep1, @@ -113,7 +114,7 @@ describe('MONGODB-OIDC', function () { describe('1.2 Single Principal Explicit Username', function () { before(function () { - client = new MongoClient('mongodb://test_user@localhost/?authMechanism=MONGODB-OIDC', { + client = new MongoClient('mongodb://test_user1@localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { REQUEST_TOKEN_CALLBACK: createRequestCallback() } @@ -160,7 +161,7 @@ describe('MONGODB-OIDC', function () { 'mongodb://test_user2@localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred', { authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback() + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user2') } } ); @@ -195,7 +196,13 @@ describe('MONGODB-OIDC', function () { // Assert that a find operation fails. // Close the client. it('fails authentication', async function () { - await collection.findOne(); + try { + await collection.findOne(); + expect.fail('Expected OIDC auth to fail with no user provided'); + } catch (e) { + expect(e).to.be.instanceOf(MongoServerError); + expect(e.message).to.include('Authentication failed'); + } }); }); @@ -255,7 +262,10 @@ describe('MONGODB-OIDC', function () { before(function () { tokenFile = process.env.AWS_WEB_IDENTITY_TOKEN_FILE; - process.env.AWS_WEB_IDENTITY_TOKEN_FILE = path.join(process.env.OIDC_TOKEN_DIR, 'test2'); + process.env.AWS_WEB_IDENTITY_TOKEN_FILE = path.join( + process.env.OIDC_TOKEN_DIR, + 'test_user2' + ); client = new MongoClient( 'mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred' ); From 45a6220866e04fd0108c69b4fa35b8b4752499d9 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 18 Apr 2023 14:48:13 -0400 Subject: [PATCH 24/93] test: more test fixes --- src/cmap/auth/mongodb_oidc.ts | 1 - test/manual/mongodb_oidc.prose.test.ts | 109 ++++++++++++++++++------- 2 files changed, 79 insertions(+), 31 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index 4e1d6968ab9..fd52fdb2f17 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -80,7 +80,6 @@ export class MongoDBOIDC extends AuthProvider { override async auth(authContext: AuthContext): Promise { const { connection, credentials, response, reauthenticating } = authContext; - console.log('RESPONSE', response); if (response?.speculativeAuthenticate) { return; } diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index cafea4a7af1..ff198b0de46 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -10,6 +10,7 @@ import { CommandStartedEvent, CommandSucceededEvent, MongoClient, + MongoMissingCredentialsError, MongoServerError, OIDC_WORKFLOWS, OIDCClientInfo, @@ -108,7 +109,8 @@ describe('MONGODB-OIDC', function () { // Perform a find operation. that succeeds. // Close the client. it('successfully authenticates', async function () { - await collection.findOne(); + const result = await collection.findOne(); + expect(result).to.be.null; }); }); @@ -128,7 +130,8 @@ describe('MONGODB-OIDC', function () { // Perform a find operation that succeeds. // Close the client. it('successfully authenticates', async function () { - await collection.findOne(); + const result = await collection.findOne(); + expect(result).to.be.null; }); }); @@ -151,7 +154,8 @@ describe('MONGODB-OIDC', function () { // Perform a find operation that succeeds. // Close the client. it('successfully authenticates', async function () { - await collection.findOne(); + const result = await collection.findOne(); + expect(result).to.be.null; }); }); @@ -174,7 +178,8 @@ describe('MONGODB-OIDC', function () { // Perform a find operation that succeeds. // Close the client. it('successfully authenticates', async function () { - await collection.findOne(); + const result = await collection.findOne(); + expect(result).to.be.null; }); }); @@ -237,7 +242,8 @@ describe('MONGODB-OIDC', function () { // Perform a find operation that succeeds. // Close the client. it('successfully authenticates', async function () { - await collection.findOne(); + const result = await collection.findOne(); + expect(result).to.be.null; }); }); @@ -253,7 +259,8 @@ describe('MONGODB-OIDC', function () { // Perform a find operation that succeeds. // Close the client. it('successfully authenticates', async function () { - await collection.findOne(); + const result = await collection.findOne(); + expect(result).to.be.null; }); }); @@ -282,7 +289,8 @@ describe('MONGODB-OIDC', function () { // Close the client. // Restore the AWS_WEB_IDENTITY_TOKEN_FILE environment variable to the location of valid test_user2 credentials. it('successfully authenticates', async function () { - await collection.findOne(); + const result = await collection.findOne(); + expect(result).to.be.null; }); }); @@ -303,7 +311,8 @@ describe('MONGODB-OIDC', function () { // Assert that a find operation succeeds. // Close the client. it('successfully authenticates', async function () { - await collection.findOne(); + const result = await collection.findOne(); + expect(result).to.be.null; }); }); }); @@ -321,12 +330,10 @@ describe('MONGODB-OIDC', function () { }); describe('3.1 Valid Callbacks', function () { - let requestSpy; - let refreshSpy; + const requestSpy = sinon.spy(createRequestCallback('test_user1', 60)); + const refreshSpy = sinon.spy(createRefreshCallback()); - before(function () { - requestSpy = sinon.spy(createRequestCallback('test_user1', 60)); - refreshSpy = sinon.spy(createRefreshCallback()); + before(async function () { client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { REQUEST_TOKEN_CALLBACK: requestSpy, @@ -334,6 +341,9 @@ describe('MONGODB-OIDC', function () { } }); collection = client.db('test').collection('test'); + await collection.findOne(); + expect(requestSpy).to.have.been.calledOnce; + await client.close(); }); // Clear the cache. @@ -343,8 +353,13 @@ describe('MONGODB-OIDC', function () { // Perform another find operation that succeeds. Verify that the refresh callback was called with the appropriate inputs, including the timeout parameter if possible. // Close the client. it('successfully authenticates with the request and refresh callbacks', async function () { - await collection.findOne(); - expect(requestSpy).to.have.been.calledOnce; + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: requestSpy, + REFRESH_TOKEN_CALLBACK: refreshSpy + } + }); + collection = client.db('test').collection('test'); await collection.findOne(); expect(refreshSpy).to.have.been.calledOnce; }); @@ -367,12 +382,18 @@ describe('MONGODB-OIDC', function () { // Perform a find operation that fails. // Close the client. it('fails authentication', async function () { - await collection.findOne(); + try { + await collection.findOne(); + expect.fail('Expected OIDC auth to fail with null return from request callback'); + } catch (e) { + expect(e).to.be.instanceOf(MongoMissingCredentialsError); + expect(e.message).to.include('REQUEST_TOKEN_CALLBACK must return a valid object'); + } }); }); describe('3.3 Refresh Callback Returns Null', function () { - before(function () { + before(async function () { client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), @@ -382,6 +403,8 @@ describe('MONGODB-OIDC', function () { } }); collection = client.db('test').collection('test'); + await collection.findOne(); + await client.close(); }); // Clear the cache. @@ -390,7 +413,15 @@ describe('MONGODB-OIDC', function () { // Perform a find operation that fails. // Close the client. it('fails authentication on refresh', async function () { - await collection.findOne(); + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), + REFRESH_TOKEN_CALLBACK: () => { + return Promise.resolve(null); + } + } + }); + collection = client.db('test').collection('test'); await collection.findOne(); }); }); @@ -412,10 +443,14 @@ describe('MONGODB-OIDC', function () { // Create a client with a request callback that returns data not conforming to the OIDCRequestTokenResult with missing field(s). // Perform a find operation that fails. // Close the client. - it('fails authentication', function () { - expect(async () => { + it('fails authentication', async function () { + try { await collection.findOne(); - }).to.throw; + expect.fail('Expected OIDC auth to fail with invlid return from request callback'); + } catch (e) { + expect(e).to.be.instanceOf(MongoMissingCredentialsError); + expect(e.message).to.include('REQUEST_TOKEN_CALLBACK must return a valid object'); + } }); }); @@ -433,7 +468,13 @@ describe('MONGODB-OIDC', function () { // Perform a find operation that fails. // Close the client. it('fails authentication', async function () { - await collection.findOne(); + try { + await collection.findOne(); + expect.fail('Expected OIDC auth to fail with extra fields from request callback'); + } catch (e) { + expect(e).to.be.instanceOf(MongoMissingCredentialsError); + expect(e.message).to.include('REQUEST_TOKEN_CALLBACK must return a valid object'); + } }); }); }); @@ -460,7 +501,7 @@ describe('MONGODB-OIDC', function () { // Create a new client with the same callbacks. // Perform a find operation that fails. // Close the client. - it('fails authentication on the refresh', function () { + it('fails authentication on the refresh', async function () { client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), @@ -469,9 +510,13 @@ describe('MONGODB-OIDC', function () { } } }); - expect(async () => { + try { await client.db('test').collection('test').findOne(); - }).to.throw; + expect.fail('Expected OIDC auth to fail with missing data from request callback'); + } catch (e) { + expect(e).to.be.instanceOf(MongoMissingCredentialsError); + expect(e.message).to.include('REQUEST_TOKEN_CALLBACK must return a valid object'); + } }); }); @@ -502,7 +547,13 @@ describe('MONGODB-OIDC', function () { REFRESH_TOKEN_CALLBACK: createRefreshCallback('test_user1', 60, { foo: 'bar' }) } }); - await client.db('test').collection('test').findOne(); + try { + await client.db('test').collection('test').findOne(); + expect.fail('Expected OIDC auth to fail with extra fields from request callback'); + } catch (e) { + expect(e).to.be.instanceOf(MongoMissingCredentialsError); + expect(e.message).to.include('REQUEST_TOKEN_CALLBACK must return a valid object'); + } }); }); }); @@ -520,7 +571,7 @@ describe('MONGODB-OIDC', function () { }); describe('4.1 Cache with refresh', function () { - let refreshSpy; + const refreshSpy = sinon.spy(createRefreshCallback('test_user1', 60)); before(async function () { client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { @@ -530,7 +581,6 @@ describe('MONGODB-OIDC', function () { }); await client.db('test').collection('test').findOne(); await client.close(); - refreshSpy = sinon.spy(createRefreshCallback('test_user1', 60)); }); // Clear the cache. // Create a new client with a request callback that gives credentials that expire in on minute. @@ -553,10 +603,9 @@ describe('MONGODB-OIDC', function () { }); describe('4.2 Cache with no refresh', function () { - let requestSpy; + const requestSpy = sinon.spy(createRequestCallback('test_user1', 60)); before(async function () { - requestSpy = sinon.spy(createRequestCallback('test_user1', 60)); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { REQUEST_TOKEN_CALLBACK: requestSpy From 61448a12a3e55de0ab406ca73660593e769bb3e0 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 18 Apr 2023 15:01:22 -0400 Subject: [PATCH 25/93] chore: more debug to callback workflow --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 1242bc5034b..17feacd9a16 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -199,6 +199,7 @@ export class CallbackWorkflow implements Workflow { tokenResult, stepOneResult ); + console.log('REQUEST CALLBACK ADDED ENTRY TO CACHE', this.cache); return finishAuth(tokenResult, conversationId, connection, credentials); } } From 99009e19b033ed44ee0e4aed8e3df1d4a14ac658 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 18 Apr 2023 15:12:24 -0400 Subject: [PATCH 26/93] test: move cache clearing --- test/manual/mongodb_oidc.prose.test.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index ff198b0de46..6008faee980 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -321,10 +321,6 @@ describe('MONGODB-OIDC', function () { let client: MongoClient; let collection: Collection; - beforeEach(function () { - cache.clear(); - }); - afterEach(async function () { await client?.close(); }); @@ -334,6 +330,7 @@ describe('MONGODB-OIDC', function () { const refreshSpy = sinon.spy(createRefreshCallback()); before(async function () { + cache.clear(); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { REQUEST_TOKEN_CALLBACK: requestSpy, @@ -367,6 +364,7 @@ describe('MONGODB-OIDC', function () { describe('3.2 Request Callback Returns Null', function () { before(function () { + cache.clear(); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { REQUEST_TOKEN_CALLBACK: () => { @@ -394,6 +392,7 @@ describe('MONGODB-OIDC', function () { describe('3.3 Refresh Callback Returns Null', function () { before(async function () { + cache.clear(); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), @@ -429,6 +428,7 @@ describe('MONGODB-OIDC', function () { describe('3.4 Request Callback Returns Invalid Data', function () { context('when the request callback has missing fields', function () { before(function () { + cache.clear(); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { REQUEST_TOKEN_CALLBACK: () => { @@ -456,6 +456,7 @@ describe('MONGODB-OIDC', function () { context('when the request callback has extra fields', function () { before(function () { + cache.clear(); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60, { foo: 'bar' }) @@ -481,6 +482,7 @@ describe('MONGODB-OIDC', function () { describe('3.5 Refresh Callback Returns Missing Data', function () { before(async function () { + cache.clear(); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), @@ -522,6 +524,7 @@ describe('MONGODB-OIDC', function () { describe('3.6 Refresh Callback Returns Extra Data', function () { before(async function () { + cache.clear(); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), From e658e7a8337bdc7ee25af8820d4af52f39180053 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 18 Apr 2023 15:34:37 -0400 Subject: [PATCH 27/93] test: use same auth mech props --- test/manual/mongodb_oidc.prose.test.ts | 57 +++++++++++--------------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 6008faee980..4d50c65a480 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -391,15 +391,17 @@ describe('MONGODB-OIDC', function () { }); describe('3.3 Refresh Callback Returns Null', function () { + const authMechanismProperties = { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), + REFRESH_TOKEN_CALLBACK: () => { + return Promise.resolve(null); + } + }; + before(async function () { cache.clear(); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), - REFRESH_TOKEN_CALLBACK: () => { - return Promise.resolve(null); - } - } + authMechanismProperties: authMechanismProperties }); collection = client.db('test').collection('test'); await collection.findOne(); @@ -413,12 +415,7 @@ describe('MONGODB-OIDC', function () { // Close the client. it('fails authentication on refresh', async function () { client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), - REFRESH_TOKEN_CALLBACK: () => { - return Promise.resolve(null); - } - } + authMechanismProperties: authMechanismProperties }); collection = client.db('test').collection('test'); await collection.findOne(); @@ -481,15 +478,17 @@ describe('MONGODB-OIDC', function () { }); describe('3.5 Refresh Callback Returns Missing Data', function () { + const authMechanismProperties = { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), + REFRESH_TOKEN_CALLBACK: () => { + return Promise.resolve({}); + } + }; + before(async function () { cache.clear(); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), - REFRESH_TOKEN_CALLBACK: () => { - return Promise.resolve({}); - } - } + authMechanismProperties: authMechanismProperties }); await client.db('test').collection('test').findOne(); await client.close(); @@ -505,12 +504,7 @@ describe('MONGODB-OIDC', function () { // Close the client. it('fails authentication on the refresh', async function () { client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), - REFRESH_TOKEN_CALLBACK: () => { - return Promise.resolve({}); - } - } + authMechanismProperties: authMechanismProperties }); try { await client.db('test').collection('test').findOne(); @@ -523,13 +517,15 @@ describe('MONGODB-OIDC', function () { }); describe('3.6 Refresh Callback Returns Extra Data', function () { + const authMechanismProperties = { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), + REFRESH_TOKEN_CALLBACK: createRefreshCallback('test_user1', 60, { foo: 'bar' }) + }; + before(async function () { cache.clear(); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), - REFRESH_TOKEN_CALLBACK: createRefreshCallback('test_user1', 60, { foo: 'bar' }) - } + authMechanismProperties: authMechanismProperties }); await client.db('test').collection('test').findOne(); await client.close(); @@ -545,10 +541,7 @@ describe('MONGODB-OIDC', function () { // Close the client. it('fails authentication on the refresh', async function () { client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), - REFRESH_TOKEN_CALLBACK: createRefreshCallback('test_user1', 60, { foo: 'bar' }) - } + authMechanismProperties: authMechanismProperties }); try { await client.db('test').collection('test').findOne(); From 185c8952eaf0c0c0a59c6ca7f1e4fbbbe01c1d7d Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 18 Apr 2023 16:27:43 -0400 Subject: [PATCH 28/93] test: more test fixes: --- .../auth/mongodb_oidc/callback_workflow.ts | 17 ++++++++++++-- test/manual/mongodb_oidc.prose.test.ts | 23 +++++++++++-------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 17feacd9a16..1185cc1ef3a 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -141,7 +141,7 @@ export class CallbackWorkflow implements Workflow { const clientInfo = { principalName: credentials.username, timeoutSeconds: TIMEOUT_S }; const result: OIDCRequestTokenResult = await refresh(clientInfo, stepOneResult, tokenResult); // Validate the result. - if (!result || !result.accessToken) { + if (isCallbackResultInvalid(result)) { throw new MongoMissingCredentialsError( 'REFRESH_TOKEN_CALLBACK must return a valid object with an accessToken' ); @@ -185,7 +185,7 @@ export class CallbackWorkflow implements Workflow { const clientInfo = { principalName: credentials.username, timeoutSeconds: TIMEOUT_S }; const tokenResult = await request(clientInfo, stepOneResult); // Validate the result. - if (!tokenResult || !tokenResult.accessToken) { + if (isCallbackResultInvalid(tokenResult)) { throw new MongoMissingCredentialsError( 'REQUEST_TOKEN_CALLBACK must return a valid object with an accessToken' ); @@ -204,6 +204,19 @@ export class CallbackWorkflow implements Workflow { } } +// Properties allowed on results of callbacks. +const RESULT_PROPERTIES = ['accessToken', 'expiresInSeconds', 'refreshToken']; + +/** + * Determines if a result returned from a request or refresh callback + * function is invalid. + */ +function isCallbackResultInvalid(tokenResult: any): boolean { + if (!tokenResult) return true; + if (!tokenResult.accessToken) return true; + return Object.getOwnPropertyNames(tokenResult).every(prop => RESULT_PROPERTIES.includes(prop)); +} + /** * Cache the result of the user supplied callback and execute the * step two saslContinue. diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 4d50c65a480..3e8d78793bd 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -328,14 +328,15 @@ describe('MONGODB-OIDC', function () { describe('3.1 Valid Callbacks', function () { const requestSpy = sinon.spy(createRequestCallback('test_user1', 60)); const refreshSpy = sinon.spy(createRefreshCallback()); + const authMechanismProperties = { + REQUEST_TOKEN_CALLBACK: requestSpy, + REFRESH_TOKEN_CALLBACK: refreshSpy + }; before(async function () { cache.clear(); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: requestSpy, - REFRESH_TOKEN_CALLBACK: refreshSpy - } + authMechanismProperties: authMechanismProperties }); collection = client.db('test').collection('test'); await collection.findOne(); @@ -351,10 +352,7 @@ describe('MONGODB-OIDC', function () { // Close the client. it('successfully authenticates with the request and refresh callbacks', async function () { client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: requestSpy, - REFRESH_TOKEN_CALLBACK: refreshSpy - } + authMechanismProperties: authMechanismProperties }); collection = client.db('test').collection('test'); await collection.findOne(); @@ -417,8 +415,13 @@ describe('MONGODB-OIDC', function () { client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: authMechanismProperties }); - collection = client.db('test').collection('test'); - await collection.findOne(); + try { + await client.db('test').collection('test').findOne(); + expect.fail('Expected OIDC auth to fail with invlid return from request callback'); + } catch (e) { + expect(e).to.be.instanceOf(MongoMissingCredentialsError); + expect(e.message).to.include('REQUEST_TOKEN_CALLBACK must return a valid object'); + } }); }); From 8a8e3c591f9556612f860149c956bf60d74df756 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 18 Apr 2023 16:38:53 -0400 Subject: [PATCH 29/93] chore: more debug --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 1185cc1ef3a..7e693213331 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -214,6 +214,7 @@ const RESULT_PROPERTIES = ['accessToken', 'expiresInSeconds', 'refreshToken']; function isCallbackResultInvalid(tokenResult: any): boolean { if (!tokenResult) return true; if (!tokenResult.accessToken) return true; + console.log(Object.getOwnPropertyNames(tokenResult), RESULT_PROPERTIES); return Object.getOwnPropertyNames(tokenResult).every(prop => RESULT_PROPERTIES.includes(prop)); } From cb19e9cf513e73dc3b57841505bd68659786e879 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 19 Apr 2023 09:32:51 -0400 Subject: [PATCH 30/93] test: more debug --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 7e693213331..dc3fb2f27bc 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -212,6 +212,7 @@ const RESULT_PROPERTIES = ['accessToken', 'expiresInSeconds', 'refreshToken']; * function is invalid. */ function isCallbackResultInvalid(tokenResult: any): boolean { + console.log('TOKEN RESULT', tokenResult); if (!tokenResult) return true; if (!tokenResult.accessToken) return true; console.log(Object.getOwnPropertyNames(tokenResult), RESULT_PROPERTIES); From 76e3e2d68d2f62f3aa149d4911f00aaacedb911b Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 19 Apr 2023 09:43:24 -0400 Subject: [PATCH 31/93] fix: result check --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index dc3fb2f27bc..074b288803a 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -212,11 +212,9 @@ const RESULT_PROPERTIES = ['accessToken', 'expiresInSeconds', 'refreshToken']; * function is invalid. */ function isCallbackResultInvalid(tokenResult: any): boolean { - console.log('TOKEN RESULT', tokenResult); if (!tokenResult) return true; if (!tokenResult.accessToken) return true; - console.log(Object.getOwnPropertyNames(tokenResult), RESULT_PROPERTIES); - return Object.getOwnPropertyNames(tokenResult).every(prop => RESULT_PROPERTIES.includes(prop)); + return !Object.getOwnPropertyNames(tokenResult).every(prop => RESULT_PROPERTIES.includes(prop)); } /** From 392b6f6bfc9a168e3f9e88b9646bb9b693051f02 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 19 Apr 2023 09:57:18 -0400 Subject: [PATCH 32/93] test: fix callback instances in tests --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 3 ++- test/manual/mongodb_oidc.prose.test.ts | 17 +++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 074b288803a..691f11bb809 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -209,7 +209,8 @@ const RESULT_PROPERTIES = ['accessToken', 'expiresInSeconds', 'refreshToken']; /** * Determines if a result returned from a request or refresh callback - * function is invalid. + * function is invalid. This means the result is nullish, doesn't contain + * the accessToken required field, and does not contain extra fields. */ function isCallbackResultInvalid(tokenResult: any): boolean { if (!tokenResult) return true; diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 3e8d78793bd..0e31571b590 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -417,10 +417,10 @@ describe('MONGODB-OIDC', function () { }); try { await client.db('test').collection('test').findOne(); - expect.fail('Expected OIDC auth to fail with invlid return from request callback'); + expect.fail('Expected OIDC auth to fail with invlid return from refresh callback'); } catch (e) { expect(e).to.be.instanceOf(MongoMissingCredentialsError); - expect(e.message).to.include('REQUEST_TOKEN_CALLBACK must return a valid object'); + expect(e.message).to.include('REFRESH_TOKEN_CALLBACK must return a valid object'); } }); }); @@ -511,10 +511,10 @@ describe('MONGODB-OIDC', function () { }); try { await client.db('test').collection('test').findOne(); - expect.fail('Expected OIDC auth to fail with missing data from request callback'); + expect.fail('Expected OIDC auth to fail with missing data from refresh callback'); } catch (e) { expect(e).to.be.instanceOf(MongoMissingCredentialsError); - expect(e.message).to.include('REQUEST_TOKEN_CALLBACK must return a valid object'); + expect(e.message).to.include('REFRESH_TOKEN_CALLBACK must return a valid object'); } }); }); @@ -548,10 +548,10 @@ describe('MONGODB-OIDC', function () { }); try { await client.db('test').collection('test').findOne(); - expect.fail('Expected OIDC auth to fail with extra fields from request callback'); + expect.fail('Expected OIDC auth to fail with extra fields from refresh callback'); } catch (e) { expect(e).to.be.instanceOf(MongoMissingCredentialsError); - expect(e.message).to.include('REQUEST_TOKEN_CALLBACK must return a valid object'); + expect(e.message).to.include('REFRESH_TOKEN_CALLBACK must return a valid object'); } }); }); @@ -570,12 +570,13 @@ describe('MONGODB-OIDC', function () { }); describe('4.1 Cache with refresh', function () { + const requestCallback = createRequestCallback('test_user1', 60); const refreshSpy = sinon.spy(createRefreshCallback('test_user1', 60)); before(async function () { client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60) + REQUEST_TOKEN_CALLBACK: requestCallback } }); await client.db('test').collection('test').findOne(); @@ -592,7 +593,7 @@ describe('MONGODB-OIDC', function () { // Ensure credentials added to the cache. client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), + REQUEST_TOKEN_CALLBACK: requestCallback, REFRESH_TOKEN_CALLBACK: refreshSpy } }); From e365c694ad1550ead22ab502fb1ec2e8f3b73eac Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 19 Apr 2023 10:30:06 -0400 Subject: [PATCH 33/93] test: more test updates --- test/manual/mongodb_oidc.prose.test.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 0e31571b590..f8c29ded2d8 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -561,10 +561,6 @@ describe('MONGODB-OIDC', function () { let client: MongoClient; let collection: Collection; - beforeEach(function () { - cache.clear(); - }); - afterEach(async function () { await client?.close(); }); @@ -574,6 +570,7 @@ describe('MONGODB-OIDC', function () { const refreshSpy = sinon.spy(createRefreshCallback('test_user1', 60)); before(async function () { + cache.clear(); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { REQUEST_TOKEN_CALLBACK: requestCallback @@ -606,6 +603,7 @@ describe('MONGODB-OIDC', function () { const requestSpy = sinon.spy(createRequestCallback('test_user1', 60)); before(async function () { + cache.clear(); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { REQUEST_TOKEN_CALLBACK: requestSpy @@ -639,6 +637,7 @@ describe('MONGODB-OIDC', function () { const secondRequestCallback = createRequestCallback('test_user1'); before(async function () { + cache.clear(); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { REQUEST_TOKEN_CALLBACK: firstRequestCallback @@ -669,6 +668,7 @@ describe('MONGODB-OIDC', function () { describe('4.4 Error clears cache', function () { before(function () { + cache.clear(); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 300), @@ -689,8 +689,12 @@ describe('MONGODB-OIDC', function () { it('clears the cache on authentication error', async function () { await collection.findOne(); expect(cache.entries.size).to.equal(1); - await collection.findOne(); - expect(cache.entries).to.be.empty; + try { + await collection.findOne(); + } catch (e) { + console.log(e); + expect(cache.entries.size).to.equal(0); + } }); }); @@ -708,7 +712,7 @@ describe('MONGODB-OIDC', function () { // Close the client. it('authenticates with no cache usage', async function () { await collection.findOne(); - expect(cache.entries).to.be.empty; + expect(cache.entries.size).to.equal(0); }); }); }); @@ -809,10 +813,6 @@ describe('MONGODB-OIDC', function () { cache.clear(); }); - afterEach(async function () { - await client?.close(); - }); - // Removes the fail point. const removeFailPoint = async () => { return await client.db().admin().command({ @@ -881,6 +881,7 @@ describe('MONGODB-OIDC', function () { after(async function () { resetEvents(); await removeFailPoint(); + await client.close(); }); // Clear the cache. @@ -955,6 +956,7 @@ describe('MONGODB-OIDC', function () { after(async function () { await removeFailPoint(); + await client.close(); }); // Clear the cache. @@ -1015,6 +1017,7 @@ describe('MONGODB-OIDC', function () { after(async function () { await removeFailPoint(); + await client.close(); }); // Clear the cache. From 541e803a5345a5d28cc3ca8ae73526b62a004ea2 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 19 Apr 2023 10:48:14 -0400 Subject: [PATCH 34/93] test: more updates --- test/manual/mongodb_oidc.prose.test.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index f8c29ded2d8..965d9417f26 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -568,13 +568,15 @@ describe('MONGODB-OIDC', function () { describe('4.1 Cache with refresh', function () { const requestCallback = createRequestCallback('test_user1', 60); const refreshSpy = sinon.spy(createRefreshCallback('test_user1', 60)); + const authMechanismProperties = { + REQUEST_TOKEN_CALLBACK: requestCallback, + REFRESH_TOKEN_CALLBACK: refreshSpy + }; before(async function () { cache.clear(); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: requestCallback - } + authMechanismProperties: authMechanismProperties }); await client.db('test').collection('test').findOne(); await client.close(); @@ -589,10 +591,7 @@ describe('MONGODB-OIDC', function () { it('successfully authenticates and calls the refresh callback', async function () { // Ensure credentials added to the cache. client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: requestCallback, - REFRESH_TOKEN_CALLBACK: refreshSpy - } + authMechanismProperties: authMechanismProperties }); await client.db('test').collection('test').findOne(); expect(refreshSpy).to.have.been.calledOnce; @@ -700,6 +699,7 @@ describe('MONGODB-OIDC', function () { describe('4.5 AWS Automatic workflow does not use cache', function () { before(function () { + cache.clear(); client = new MongoClient( 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws' ); @@ -822,7 +822,12 @@ describe('MONGODB-OIDC', function () { }; describe('6.1 Succeeds', function () { + const requestCallback = createRequestCallback('test_user1', 600); const refreshSpy = sinon.spy(createRefreshCallback('test_user1', 600)); + const authMechanismProperties = { + REQUEST_TOKEN_CALLBACK: requestCallback, + REFRESH_TOKEN_CALLBACK: refreshSpy + }; let commandStartedEvents: CommandStartedEvent[]; let commandSucceededEvents: CommandSucceededEvent[]; let commandFailedEvents: CommandFailedEvent[]; @@ -862,10 +867,7 @@ describe('MONGODB-OIDC', function () { before(async function () { client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 600), - REFRESH_TOKEN_CALLBACK: refreshSpy - }, + authMechanismProperties: authMechanismProperties, monitorCommands: true }); collection = client.db('test').collection('test'); From 3ffa95e0824c6abaae1078a8a4d123e9938a7d7d Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 19 Apr 2023 11:30:56 -0400 Subject: [PATCH 35/93] test: more test fixes --- test/manual/mongodb_oidc.prose.test.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 965d9417f26..ba3ba925e39 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -651,17 +651,20 @@ describe('MONGODB-OIDC', function () { // Ensure that a find operation adds credentials to the cache. // Close the client. // Create a new client with a different request callback. - // Ensure that a find operation adds a new entry to the cache. + // Ensure that a find operation replaces the one-time entry with a new entry to the cache. // Close the client. - it('includes the callback functions in the cache', async function () { + it('replaces expired entries in the cache', async function () { expect(cache.entries.size).to.equal(1); + const initialKey = cache.entries.keys().next().value; client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { REQUEST_TOKEN_CALLBACK: secondRequestCallback } }); await client.db('test').collection('test').findOne(); - expect(cache.entries.size).to.equal(2); + expect(cache.entries.size).to.equal(1); + const newKey = cache.entries.keys().next().value; + expect(newKey).to.not.equal(initialKey); }); }); @@ -918,7 +921,11 @@ describe('MONGODB-OIDC', function () { // Assert that a find operation failed once during the command execution. // Close the client. it('successfully reauthenticates', async function () { - await collection.findOne(); + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: authMechanismProperties, + monitorCommands: true + }); + await client.db('test').collection('test').findOne(); expect(refreshSpy).to.have.been.calledOnce; expect(commandStartedEvents.map(event => event.commandName)).to.equal(['find', 'find']); expect(commandStartedEvents.map(event => event.commandName)).to.equal(['find']); From 5a7207a6b7a41052b106bec5728a8c8deb976e0f Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 19 Apr 2023 13:34:08 -0400 Subject: [PATCH 36/93] test: update prose test debug --- test/manual/mongodb_oidc.prose.test.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index ba3ba925e39..9dd88404d65 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -836,12 +836,15 @@ describe('MONGODB-OIDC', function () { let commandFailedEvents: CommandFailedEvent[]; const commandStartedListener = event => { + console.log('commandStarted', event); commandStartedEvents.push(event); }; const commandSucceededListener = event => { + console.log('commandSuceeded', event); commandSucceededEvents.push(event); }; const commandFailedListener = event => { + console.log('commandFailed', event); commandFailedEvents.push(event); }; @@ -851,6 +854,12 @@ describe('MONGODB-OIDC', function () { commandFailedEvents = []; }; + const addListeners = () => { + client.on('commandStarted', commandStartedListener); + client.on('commandSucceeded', commandSucceededListener); + client.on('commandFailed', commandFailedListener); + }; + // Sets up the fail point for the find to reauthenticate. const setupFailPoint = async () => { return await client @@ -877,10 +886,7 @@ describe('MONGODB-OIDC', function () { await collection.findOne(); expect(refreshSpy).to.not.be.called; resetEvents(); - await setupFailPoint(); - client.on('commandStarted', commandStartedListener); - client.on('commandSucceeded', commandSucceededListener); - client.on('commandFailed', commandFailedListener); + client.close(); }); after(async function () { @@ -925,6 +931,8 @@ describe('MONGODB-OIDC', function () { authMechanismProperties: authMechanismProperties, monitorCommands: true }); + addListeners(); + await setupFailPoint(); await client.db('test').collection('test').findOne(); expect(refreshSpy).to.have.been.calledOnce; expect(commandStartedEvents.map(event => event.commandName)).to.equal(['find', 'find']); From 0d14b648d6b8b4307e3a8b48656d1fe57c1138fb Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 19 Apr 2023 13:41:42 -0400 Subject: [PATCH 37/93] test: only track find events --- src/cmap/connection.ts | 1 + test/manual/mongodb_oidc.prose.test.ts | 51 ++++++++++++++------------ 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/cmap/connection.ts b/src/cmap/connection.ts index f86e4dc3056..c546e4c9f7f 100644 --- a/src/cmap/connection.ts +++ b/src/cmap/connection.ts @@ -410,6 +410,7 @@ export class Connection extends TypedEventEmitter { } if (document.ok === 0 || document.$err || document.errmsg || document.code) { + console.log('DOCUMENT', document); callback(new MongoServerError(document)); return; } diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 9dd88404d65..d02c1da5440 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -831,27 +831,27 @@ describe('MONGODB-OIDC', function () { REQUEST_TOKEN_CALLBACK: requestCallback, REFRESH_TOKEN_CALLBACK: refreshSpy }; - let commandStartedEvents: CommandStartedEvent[]; - let commandSucceededEvents: CommandSucceededEvent[]; - let commandFailedEvents: CommandFailedEvent[]; + const commandStartedEvents: CommandStartedEvent[] = []; + const commandSucceededEvents: CommandSucceededEvent[] = []; + const commandFailedEvents: CommandFailedEvent[] = []; const commandStartedListener = event => { console.log('commandStarted', event); - commandStartedEvents.push(event); + if (event.commandName === 'find') { + commandStartedEvents.push(event); + } }; const commandSucceededListener = event => { console.log('commandSuceeded', event); - commandSucceededEvents.push(event); + if (event.commandName === 'find') { + commandSucceededEvents.push(event); + } }; const commandFailedListener = event => { console.log('commandFailed', event); - commandFailedEvents.push(event); - }; - - const resetEvents = () => { - commandStartedEvents = []; - commandSucceededEvents = []; - commandFailedEvents = []; + if (event.commandName === 'find') { + commandFailedEvents.push(event); + } }; const addListeners = () => { @@ -879,18 +879,14 @@ describe('MONGODB-OIDC', function () { before(async function () { client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: authMechanismProperties, - monitorCommands: true + authMechanismProperties: authMechanismProperties }); - collection = client.db('test').collection('test'); - await collection.findOne(); + await client.db('test').collection('test').findOne(); expect(refreshSpy).to.not.be.called; - resetEvents(); client.close(); }); - after(async function () { - resetEvents(); + afterEach(async function () { await removeFailPoint(); await client.close(); }); @@ -935,9 +931,12 @@ describe('MONGODB-OIDC', function () { await setupFailPoint(); await client.db('test').collection('test').findOne(); expect(refreshSpy).to.have.been.calledOnce; - expect(commandStartedEvents.map(event => event.commandName)).to.equal(['find', 'find']); - expect(commandStartedEvents.map(event => event.commandName)).to.equal(['find']); - expect(commandStartedEvents.map(event => event.commandName)).to.equal(['find']); + expect(commandStartedEvents.map(event => event.commandName)).to.deep.equal([ + 'find', + 'find' + ]); + expect(commandSucceededEvents.map(event => event.commandName)).to.deep.equal(['find']); + expect(commandFailedEvents.map(event => event.commandName)).to.deep.equal(['find']); }); }); @@ -967,11 +966,13 @@ describe('MONGODB-OIDC', function () { } }); collection = client.db('test').collection('test'); + console.log('FIRST FIND'); await collection.findOne(); + console.log('SETUP FAIL POINT'); await setupFailPoint(); }); - after(async function () { + afterEach(async function () { await removeFailPoint(); await client.close(); }); @@ -997,7 +998,9 @@ describe('MONGODB-OIDC', function () { // Perform a find operation that succeeds. // Close the client. it('successfully reauthenticates with the cache', async function () { - await collection.findOne(); + console.log('SECOND FIND'); + const result = await collection.findOne(); + expect(result).to.be.null; }); }); From 3617ce2c9aafa21045632d4ec9338bf4df1a40cd Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 20 Apr 2023 09:33:21 -0400 Subject: [PATCH 38/93] fix: speculative auth --- src/cmap/auth/mongodb_oidc.ts | 7 +++++-- .../auth/mongodb_oidc/callback_workflow.ts | 19 +++++++++++++++---- src/cmap/auth/mongodb_oidc/workflow.ts | 2 +- src/cmap/connection.ts | 1 - src/cmap/connection_pool.ts | 1 + test/manual/mongodb_oidc.prose.test.ts | 13 ++++++++----- 6 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index fd52fdb2f17..1b086172d20 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -80,7 +80,9 @@ export class MongoDBOIDC extends AuthProvider { override async auth(authContext: AuthContext): Promise { const { connection, credentials, response, reauthenticating } = authContext; + console.log('RESPONSE', response); if (response?.speculativeAuthenticate) { + console.log('SPECULATIVE RESPONSE', response.speculativeAuthenticate); return; } @@ -101,7 +103,7 @@ export class MongoDBOIDC extends AuthProvider { handshakeDoc: HandshakeDocument, authContext: AuthContext ): Promise { - const { credentials } = authContext; + const { connection, credentials } = authContext; if (!credentials) { throw new MongoMissingCredentialsError('AuthContext must provide credentials.'); @@ -109,7 +111,8 @@ export class MongoDBOIDC extends AuthProvider { const workflow = getWorkflow(credentials); - const result = await workflow.speculativeAuth(); + const result = await workflow.speculativeAuth(connection, credentials); + console.log('PREPARE', result); return { ...handshakeDoc, ...result }; } } diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 691f11bb809..ebd48216f5e 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -27,11 +27,22 @@ export class CallbackWorkflow implements Workflow { } /** - * Get the document to add for speculative authentication. Is empty when - * callbacks are in play. + * Get the document to add for speculative authentication. This will only return + * a valid document when there is a valid entry in the cache. */ - speculativeAuth(): Promise { - return Promise.resolve({}); + async speculativeAuth(connection: Connection, credentials: MongoCredentials): Promise { + const request = credentials.mechanismProperties.REQUEST_TOKEN_CALLBACK; + const refresh = credentials.mechanismProperties.REFRESH_TOKEN_CALLBACK; + const entry = this.cache.getEntry( + connection.address, + credentials.username, + request || null, + refresh || null + ); + if (entry?.isValid()) { + return { speculativeAuthenticate: continueCommandDocument(entry.tokenResult.accessToken) }; + } + return { speculativeAuthenticate: startCommandDocument(credentials) }; } /** diff --git a/src/cmap/auth/mongodb_oidc/workflow.ts b/src/cmap/auth/mongodb_oidc/workflow.ts index 68d0b97688a..6d4f87a462e 100644 --- a/src/cmap/auth/mongodb_oidc/workflow.ts +++ b/src/cmap/auth/mongodb_oidc/workflow.ts @@ -17,5 +17,5 @@ export interface Workflow { /** * Get the document to add for speculative authentication. */ - speculativeAuth(): Promise; + speculativeAuth(connection: Connection, credentials: MongoCredentials): Promise; } diff --git a/src/cmap/connection.ts b/src/cmap/connection.ts index c546e4c9f7f..f86e4dc3056 100644 --- a/src/cmap/connection.ts +++ b/src/cmap/connection.ts @@ -410,7 +410,6 @@ export class Connection extends TypedEventEmitter { } if (document.ok === 0 || document.$err || document.errmsg || document.code) { - console.log('DOCUMENT', document); callback(new MongoServerError(document)); return; } diff --git a/src/cmap/connection_pool.ts b/src/cmap/connection_pool.ts index 01feb78fddf..91f354815dd 100644 --- a/src/cmap/connection_pool.ts +++ b/src/cmap/connection_pool.ts @@ -580,6 +580,7 @@ export class ConnectionPool extends TypedEventEmitter { callback: Callback ) { if (fnErr instanceof MongoError && fnErr.code === MONGODB_ERROR_CODES.Reauthenticate) { + console.log('REAUTHENTICATING'); this.reauthenticate(conn, fn, (error, res) => { if (error) { return callback(error); diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index d02c1da5440..c5cba60e64e 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -812,10 +812,6 @@ describe('MONGODB-OIDC', function () { let client: MongoClient; let collection: Collection; - beforeEach(function () { - cache.clear(); - }); - // Removes the fail point. const removeFailPoint = async () => { return await client.db().admin().command({ @@ -878,6 +874,7 @@ describe('MONGODB-OIDC', function () { }; before(async function () { + cache.clear(); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: authMechanismProperties }); @@ -959,6 +956,7 @@ describe('MONGODB-OIDC', function () { }; before(async function () { + cache.clear(); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 600), @@ -1023,6 +1021,7 @@ describe('MONGODB-OIDC', function () { }; before(async function () { + cache.clear(); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 600), @@ -1062,7 +1061,11 @@ describe('MONGODB-OIDC', function () { // Perform a find operation that fails. // Close the client. it('fails reauthentication with no cache entries', async function () { - await collection.findOne(); + try { + await collection.findOne(); + } catch (e) { + + } }); }); }); From 0f139dc8eaec07820e0556da8b1e391e5055b920 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 21 Apr 2023 10:25:27 -0400 Subject: [PATCH 39/93] refactor: refactoring callback workflow --- src/cmap/auth/mongodb_oidc.ts | 41 +-- .../auth/mongodb_oidc/callback_workflow.ts | 329 ++++++++---------- src/cmap/auth/mongodb_oidc/workflow.ts | 5 +- src/connection_string.ts | 2 +- test/manual/mongodb_oidc.prose.test.ts | 144 -------- 5 files changed, 171 insertions(+), 350 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index 1b086172d20..d88918033fa 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -78,22 +78,10 @@ export class MongoDBOIDC extends AuthProvider { * Authenticate using OIDC */ override async auth(authContext: AuthContext): Promise { - const { connection, credentials, response, reauthenticating } = authContext; - - console.log('RESPONSE', response); - if (response?.speculativeAuthenticate) { - console.log('SPECULATIVE RESPONSE', response.speculativeAuthenticate); - return; - } - - if (!credentials) { - throw new MongoMissingCredentialsError('AuthContext must provide credentials.'); - } - + const { connection, reauthenticating, response } = authContext; + const credentials = getCredentials(authContext); const workflow = getWorkflow(credentials); - - console.log('AUTH', workflow); - await workflow.execute(connection, credentials, reauthenticating); + await workflow.execute(connection, credentials, reauthenticating, response); } /** @@ -103,20 +91,23 @@ export class MongoDBOIDC extends AuthProvider { handshakeDoc: HandshakeDocument, authContext: AuthContext ): Promise { - const { connection, credentials } = authContext; - - if (!credentials) { - throw new MongoMissingCredentialsError('AuthContext must provide credentials.'); - } - - const workflow = getWorkflow(credentials); - - const result = await workflow.speculativeAuth(connection, credentials); - console.log('PREPARE', result); + const workflow = getWorkflow(getCredentials(authContext)); + const result = await workflow.speculativeAuth(); return { ...handshakeDoc, ...result }; } } +/** + * Get credentials from the auth context, throwing if they do not exist. + */ +function getCredentials(authContext: AuthContext): MongoCredentials { + const { credentials } = authContext; + if (!credentials) { + throw new MongoMissingCredentialsError('AuthContext must provide credentials.'); + } + return credentials; +} + /** * Gets either a device workflow or callback workflow. */ diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index ebd48216f5e..96887788005 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -4,14 +4,22 @@ import { MongoInvalidArgumentError, MongoMissingCredentialsError } from '../../. import { ns } from '../../../utils'; import type { Connection } from '../../connection'; import type { MongoCredentials } from '../mongo_credentials'; -import type { OIDCMechanismServerStep1, OIDCRequestTokenResult } from '../mongodb_oidc'; +import type { + OIDCMechanismServerStep1, + OIDCRefreshFunction, + OIDCRequestFunction, + OIDCRequestTokenResult +} from '../mongodb_oidc'; import { AuthMechanism } from '../providers'; import { TokenEntryCache } from './token_entry_cache'; import type { Workflow } from './workflow'; -/* 5 minutes in seconds */ +/** 5 minutes in seconds */ const TIMEOUT_S = 300; +/** Properties allowed on results of callbacks. */ +const RESULT_PROPERTIES = ['accessToken', 'expiresInSeconds', 'refreshToken']; + /** * OIDC implementation of a callback based workflow. * @internal @@ -30,193 +38,198 @@ export class CallbackWorkflow implements Workflow { * Get the document to add for speculative authentication. This will only return * a valid document when there is a valid entry in the cache. */ - async speculativeAuth(connection: Connection, credentials: MongoCredentials): Promise { - const request = credentials.mechanismProperties.REQUEST_TOKEN_CALLBACK; - const refresh = credentials.mechanismProperties.REFRESH_TOKEN_CALLBACK; - const entry = this.cache.getEntry( - connection.address, - credentials.username, - request || null, - refresh || null - ); - if (entry?.isValid()) { - return { speculativeAuthenticate: continueCommandDocument(entry.tokenResult.accessToken) }; - } - return { speculativeAuthenticate: startCommandDocument(credentials) }; + async speculativeAuth(): Promise { + return {}; } /** - * Execute the workflow. - * - * Steps: - * - If an entry is in the cache - * - If it is not expired - * - Skip step one and use the entry to execute step two. - * - If it is expired - * - If the refresh callback exists - * - remove expired entry from cache - * - call the refresh callback. - * - put the new entry in the cache. - * - execute step two. - * - If the refresh callback does not exist. - * - remove expired entry from cache - * - call the request callback. - * - put the new entry in the cache. - * - execute step two. - * - If no entry is in the cache. - * - execute step one. - * - call the refresh callback. - * - put the new entry in the cache. - * - execute step two. + * Execute the OIDC callback workflow. */ async execute( connection: Connection, credentials: MongoCredentials, - reauthenticate = false + reauthenticating: boolean, + response?: Document ): Promise { - const request = credentials.mechanismProperties.REQUEST_TOKEN_CALLBACK; - const refresh = credentials.mechanismProperties.REFRESH_TOKEN_CALLBACK; - console.log('REQUEST', request); - console.log('REFRESH', refresh); - + const requestCallback = credentials.mechanismProperties.REQUEST_TOKEN_CALLBACK; + const refreshCallback = credentials.mechanismProperties.REFRESH_TOKEN_CALLBACK; + // At minimum a request callback must be provided by the user. + if (!requestCallback) { + throw new MongoInvalidArgumentError( + 'Auth mechanism property REQUEST_TOKEN_CALLBACK is required.' + ); + } + // Look for an existing entry in the cache. const entry = this.cache.getEntry( connection.address, credentials.username, - request || null, - refresh || null + requestCallback, + refreshCallback || null ); - console.log('CALLBACK_WORKFLOW', entry, entry?.isValid()); - if (entry) { - // Check if the entry is not expired and if we are reauthenticating. - if (!reauthenticate && entry.isValid()) { - // Skip step one and execute the step two saslContinue. - try { - const result = await finishAuth(entry.tokenResult, undefined, connection, credentials); - return result; - } catch (error) { - // If authentication errors when using a cached token we remove it from - // the cache. - this.cache.deleteEntry( - connection.address, - credentials.username || '', - request || null, - refresh || null - ); - throw error; - } + let result; + // Reauthentication must go through all the steps again regards of a cache entry + // being present. + if (entry && !reauthenticating) { + if (entry.isValid()) { + // Presence of a valid cache entry means we can skip to the finishing step. + result = await this.finishAuthentication(connection, credentials, entry.tokenResult); } else { - // Remove the expired entry from the cache. - this.cache.deleteEntry( - connection.address, - credentials.username || '', - request || null, - refresh || null - ); - // Execute a refresh of the token and finish auth. - return this.refreshAndFinish( + // Presence of an expired cache entry means we must fetch a new one and + // then execute the final step. + const tokenResult = await this.fetchAccessToken( connection, credentials, entry.serverResult, - entry.tokenResult + requestCallback, + refreshCallback ); + result = await this.finishAuthentication(connection, credentials, tokenResult); } } else { - // No entry means to start with the step one saslStart. - const result = await connection.commandAsync( + // No entry in the cache requires us to do all authentication steps + // from start to finish, including getting a fresh token for the cache. + const startDocument = await this.startAuthentication(connection, credentials, response); + const conversationId = startDocument.conversationId; + const serverResult = BSON.deserialize( + startDocument.payload.buffer + ) as OIDCMechanismServerStep1; + const tokenResult = await this.fetchAccessToken( + connection, + credentials, + serverResult, + requestCallback, + refreshCallback + ); + result = await this.finishAuthentication( + connection, + credentials, + tokenResult, + conversationId + ); + } + return result; + } + + /** + * Starts the callback authentication process. If there is a speculative + * authentication document from the initial handshake, then we will use that + * value to get the issuer, otherwise we will send the saslStart command. + */ + private async startAuthentication( + connection: Connection, + credentials: MongoCredentials, + response?: Document + ): Promise { + let result; + if (response?.speculativeAuthentication) { + result = response.speculativeAuthentication; + } else { + result = await connection.commandAsync( ns(credentials.source), startCommandDocument(credentials), undefined ); - const stepOne = BSON.deserialize(result.payload.buffer) as OIDCMechanismServerStep1; - console.log('STEP ONE', stepOne, result); - // Call the request callback and finish auth. - return this.requestAndFinish(connection, credentials, stepOne, result.conversationId); } + return result; } /** - * Execute the refresh callback if it exists, otherwise the request callback, then - * finish the authentication. + * Finishes the callback authentication process. */ - private async refreshAndFinish( + private async finishAuthentication( connection: Connection, credentials: MongoCredentials, - stepOneResult: OIDCMechanismServerStep1, tokenResult: OIDCRequestTokenResult, conversationId?: number ): Promise { - const request = credentials.mechanismProperties.REQUEST_TOKEN_CALLBACK; - const refresh = credentials.mechanismProperties.REFRESH_TOKEN_CALLBACK; - // If a refresh callback exists, use it. Otherwise use the request callback. - if (refresh) { - const clientInfo = { principalName: credentials.username, timeoutSeconds: TIMEOUT_S }; - const result: OIDCRequestTokenResult = await refresh(clientInfo, stepOneResult, tokenResult); - // Validate the result. - if (isCallbackResultInvalid(result)) { - throw new MongoMissingCredentialsError( - 'REFRESH_TOKEN_CALLBACK must return a valid object with an accessToken' - ); - } - // Cache a new entry and continue with the saslContinue. - this.cache.addEntry( - connection.address, - credentials.username || '', - request || null, - refresh, - result, - stepOneResult - ); - return finishAuth(result, conversationId, connection, credentials); - } else { - // Fallback to using the request callback. - return this.requestAndFinish(connection, credentials, stepOneResult, conversationId); - } + const result = await connection.commandAsync( + ns(credentials.source), + finishCommandDocument(tokenResult.accessToken, conversationId), + undefined + ); + return result; } /** - * Execute the request callback and finish authentication. + * Fetches an access token using either the request or refresh callbacks and + * puts it in the cache. */ - private async requestAndFinish( + private async fetchAccessToken( connection: Connection, credentials: MongoCredentials, - stepOneResult: OIDCMechanismServerStep1, - conversationId?: number - ): Promise { - // Call the request callback. - const request = credentials.mechanismProperties.REQUEST_TOKEN_CALLBACK; - const refresh = credentials.mechanismProperties.REFRESH_TOKEN_CALLBACK; - // Always clear expired entries from the cache on each finish as cleanup. + startResult: OIDCMechanismServerStep1, + requestCallback: OIDCRequestFunction, + refreshCallback?: OIDCRefreshFunction + ): Promise { + // Delete expired cache entries then get the token from the cache. this.cache.deleteExpiredEntries(); - if (!request) { - // Request callback must be present. - throw new MongoInvalidArgumentError( - 'Auth mechanism property REQUEST_TOKEN_CALLBACK is required.' - ); - } + const entry = this.cache.getEntry( + connection.address, + credentials.username, + requestCallback || null, + refreshCallback || null + ); + let result; const clientInfo = { principalName: credentials.username, timeoutSeconds: TIMEOUT_S }; - const tokenResult = await request(clientInfo, stepOneResult); - // Validate the result. - if (isCallbackResultInvalid(tokenResult)) { + // Check if there's a token in the cache. + if (entry) { + // If the cache entry is valid, return the token result. + if (entry.isValid()) { + return entry.tokenResult; + } + // If the cache entry is not valid, remove it from the cache and first attempt + // to use the refresh callback to get a new token. If not refresh callback + // exists, then fallback to the request callback. + if (refreshCallback) { + result = await refreshCallback(clientInfo, startResult, entry.tokenResult); + } else { + result = await requestCallback(clientInfo, startResult); + } + } else { + // With no token in the cache we use the request callback. + result = await requestCallback(clientInfo, startResult); + } + // Validate that the result returned by the callback is acceptable. + if (isCallbackResultInvalid(result)) { throw new MongoMissingCredentialsError( - 'REQUEST_TOKEN_CALLBACK must return a valid object with an accessToken' + 'User provided OIDC callbacks must return a valid object with an accessToken.' ); } - // Cache a new entry and continue with the saslContinue. + // Put an entry into the cache. this.cache.addEntry( connection.address, credentials.username || '', - request, - refresh || null, - tokenResult, - stepOneResult + requestCallback, + refreshCallback || null, + result, + startResult ); - console.log('REQUEST CALLBACK ADDED ENTRY TO CACHE', this.cache); - return finishAuth(tokenResult, conversationId, connection, credentials); + return result; } } -// Properties allowed on results of callbacks. -const RESULT_PROPERTIES = ['accessToken', 'expiresInSeconds', 'refreshToken']; +/** + * Generate the finishing command document for authentication. Will be a + * saslStart or saslContinue depending on the presence of a conversation id. + */ +function finishCommandDocument(token: string, conversationId?: number): Document { + if (conversationId) { + return { + saslContinue: 1, + conversationId: conversationId, + payload: new Binary(BSON.serialize({ jwt: token })) + }; + } + // saslContinue requires a conversationId in the command to be valid so in this + // case the server allows "step two" to actually be a saslStart with the token + // as the jwt since the use of the cached value has no correlating conversating + // on the particular connection. + return { + saslStart: 1, + mechanism: AuthMechanism.MONGODB_OIDC, + payload: new Binary(BSON.serialize({ jwt: token })) + }; +} /** * Determines if a result returned from a request or refresh callback @@ -229,24 +242,6 @@ function isCallbackResultInvalid(tokenResult: any): boolean { return !Object.getOwnPropertyNames(tokenResult).every(prop => RESULT_PROPERTIES.includes(prop)); } -/** - * Cache the result of the user supplied callback and execute the - * step two saslContinue. - */ -async function finishAuth( - result: OIDCRequestTokenResult, - conversationId: number | undefined, - connection: Connection, - credentials: MongoCredentials -): Promise { - // Execute the step two saslContinue. - return connection.commandAsync( - ns(credentials.source), - continueCommandDocument(result.accessToken, conversationId), - undefined - ); -} - /** * Generate the saslStart command document. */ @@ -262,25 +257,3 @@ function startCommandDocument(credentials: MongoCredentials): Document { payload: new Binary(BSON.serialize(payload)) }; } - -/** - * Generate the saslContinue command document. - */ -function continueCommandDocument(token: string, conversationId?: number): Document { - if (conversationId) { - return { - saslContinue: 1, - conversationId: conversationId, - payload: new Binary(BSON.serialize({ jwt: token })) - }; - } - // saslContinue requires a conversationId in the command to be valid so in this - // case the server allows "step two" to actually be a saslStart with the token - // as the jwt since the use of the cached value has no correlating conversating - // on the particular connection. - return { - saslStart: 1, - mechanism: AuthMechanism.MONGODB_OIDC, - payload: new Binary(BSON.serialize({ jwt: token })) - }; -} diff --git a/src/cmap/auth/mongodb_oidc/workflow.ts b/src/cmap/auth/mongodb_oidc/workflow.ts index 6d4f87a462e..ced4b861fb6 100644 --- a/src/cmap/auth/mongodb_oidc/workflow.ts +++ b/src/cmap/auth/mongodb_oidc/workflow.ts @@ -11,11 +11,12 @@ export interface Workflow { execute( connection: Connection, credentials: MongoCredentials, - reauthenticate?: boolean + reauthenticating: boolean, + response?: Document ): Promise; /** * Get the document to add for speculative authentication. */ - speculativeAuth(connection: Connection, credentials: MongoCredentials): Promise; + speculativeAuth(): Promise; } diff --git a/src/connection_string.ts b/src/connection_string.ts index b3bc3f7b47a..ac2f2d25f66 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -313,7 +313,7 @@ export function parseOptions( const uriMechanismProperties = urlOptions.get('authMechanismProperties'); if (uriMechanismProperties) { for (const property of uriMechanismProperties) { - if (/(^|,)ALLOWED_HOSTS:/.test(property)) { + if (/(^|,)ALLOWED_HOSTS:/.test(property as string)) { throw new MongoParseError( 'Auth mechanism property ALLOWED_HOSTS is not allowed in the connection string.' ); diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index c5cba60e64e..df70cb4676e 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -721,54 +721,6 @@ describe('MONGODB-OIDC', function () { }); describe('5. Speculative Authentication', function () { - let client: MongoClient; - let collection: Collection; - - beforeEach(function () { - cache.clear(); - }); - - afterEach(async function () { - await client?.close(); - }); - - // Sets up the fail point for saslStart. - const setupFailPoint = async () => { - return await client - .db() - .admin() - .command({ - configureFailPoint: 'failCommand', - mode: { - times: 2 - }, - data: { - failCommands: ['saslStart'], - errorCode: 18 - } - }); - }; - - // Removes the fail point. - const removeFailPoint = async () => { - return await client.db().admin().command({ - configureFailPoint: 'failCommand', - mode: 'off' - }); - }; - - before(async function () { - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 300) - } - }); - await setupFailPoint(); - collection = client.db('test').collection('test'); - await collection.findOne(); - await removeFailPoint(); - }); - // Clear the cache. // Create a client with a request callback that returns a valid token that will not expire soon. // Set a fail point for saslStart commands of the form: @@ -796,21 +748,10 @@ describe('MONGODB-OIDC', function () { // Set a fail point for saslStart commands. // Perform a find operation that succeeds. // Close the client. - it('successfully speculative authenticates', async function () { - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 300) - } - }); - await setupFailPoint(); - await client.db('test').collection('test').findOne(); - await removeFailPoint(); - }); }); describe('6. Reauthentication', function () { let client: MongoClient; - let collection: Collection; // Removes the fail point. const removeFailPoint = async () => { @@ -938,43 +879,6 @@ describe('MONGODB-OIDC', function () { }); describe('6.2 Retries and Succeeds with Cache', function () { - // Sets up the fail point for the find and saslStart to reauthenticate. - const setupFailPoint = async () => { - return await client - .db() - .admin() - .command({ - configureFailPoint: 'failCommand', - mode: { - times: 2 - }, - data: { - failCommands: ['find', 'saslStart'], - errorCode: 391 - } - }); - }; - - before(async function () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 600), - REFRESH_TOKEN_CALLBACK: createRefreshCallback('test_user1', 600) - } - }); - collection = client.db('test').collection('test'); - console.log('FIRST FIND'); - await collection.findOne(); - console.log('SETUP FAIL POINT'); - await setupFailPoint(); - }); - - afterEach(async function () { - await removeFailPoint(); - await client.close(); - }); - // Clear the cache. // Create request and refresh callbacks that return valid credentials that will not expire soon. // Perform a find operation that succeeds. @@ -995,50 +899,9 @@ describe('MONGODB-OIDC', function () { // // Perform a find operation that succeeds. // Close the client. - it('successfully reauthenticates with the cache', async function () { - console.log('SECOND FIND'); - const result = await collection.findOne(); - expect(result).to.be.null; - }); }); describe('6.3 Retries and Fails with no Cache', function () { - // Sets up the fail point for the find and saslStart to reauthenticate. - const setupFailPoint = async () => { - return await client - .db() - .admin() - .command({ - configureFailPoint: 'failCommand', - mode: { - times: 2 - }, - data: { - failCommands: ['find', 'saslStart'], - errorCode: 391 - } - }); - }; - - before(async function () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 600), - REFRESH_TOKEN_CALLBACK: createRefreshCallback('test_user1', 600) - } - }); - collection = client.db('test').collection('test'); - await collection.findOne(); - cache.clear(); - await setupFailPoint(); - }); - - after(async function () { - await removeFailPoint(); - await client.close(); - }); - // Clear the cache. // Create request and refresh callbacks that return valid credentials that will not expire soon. // Perform a find operation that succeeds (to force a speculative auth). @@ -1060,13 +923,6 @@ describe('MONGODB-OIDC', function () { // // Perform a find operation that fails. // Close the client. - it('fails reauthentication with no cache entries', async function () { - try { - await collection.findOne(); - } catch (e) { - - } - }); }); }); }); From c5e435b476b00f5aef5a879e6f01731eea6b1df5 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 21 Apr 2023 10:42:21 -0400 Subject: [PATCH 40/93] fix: speculative auth and messages --- .../auth/mongodb_oidc/callback_workflow.ts | 10 ++++---- src/cmap/auth/mongodb_oidc/workflow.ts | 2 +- test/manual/mongodb_oidc.prose.test.ts | 24 ++++++++++++++----- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 96887788005..3a64b449b8b 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -35,11 +35,13 @@ export class CallbackWorkflow implements Workflow { } /** - * Get the document to add for speculative authentication. This will only return - * a valid document when there is a valid entry in the cache. + * Get the document to add for speculative authentication. This also needs + * to add a db field from the credentials source. */ - async speculativeAuth(): Promise { - return {}; + async speculativeAuth(credentials: MongoCredentials): Promise { + const document = startCommandDocument(credentials); + document.db = credentials.source; + return document; } /** diff --git a/src/cmap/auth/mongodb_oidc/workflow.ts b/src/cmap/auth/mongodb_oidc/workflow.ts index ced4b861fb6..797f7a7a8d5 100644 --- a/src/cmap/auth/mongodb_oidc/workflow.ts +++ b/src/cmap/auth/mongodb_oidc/workflow.ts @@ -18,5 +18,5 @@ export interface Workflow { /** * Get the document to add for speculative authentication. */ - speculativeAuth(): Promise; + speculativeAuth(credentials: MongoCredentials): Promise; } diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index df70cb4676e..9ff2eca541d 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -383,7 +383,9 @@ describe('MONGODB-OIDC', function () { expect.fail('Expected OIDC auth to fail with null return from request callback'); } catch (e) { expect(e).to.be.instanceOf(MongoMissingCredentialsError); - expect(e.message).to.include('REQUEST_TOKEN_CALLBACK must return a valid object'); + expect(e.message).to.include( + 'User provided OIDC callbacks must return a valid object with an accessToken' + ); } }); }); @@ -420,7 +422,9 @@ describe('MONGODB-OIDC', function () { expect.fail('Expected OIDC auth to fail with invlid return from refresh callback'); } catch (e) { expect(e).to.be.instanceOf(MongoMissingCredentialsError); - expect(e.message).to.include('REFRESH_TOKEN_CALLBACK must return a valid object'); + expect(e.message).to.include( + 'User provided OIDC callbacks must return a valid object with an accessToken' + ); } }); }); @@ -449,7 +453,9 @@ describe('MONGODB-OIDC', function () { expect.fail('Expected OIDC auth to fail with invlid return from request callback'); } catch (e) { expect(e).to.be.instanceOf(MongoMissingCredentialsError); - expect(e.message).to.include('REQUEST_TOKEN_CALLBACK must return a valid object'); + expect(e.message).to.include( + 'User provided OIDC callbacks must return a valid object with an accessToken' + ); } }); }); @@ -474,7 +480,9 @@ describe('MONGODB-OIDC', function () { expect.fail('Expected OIDC auth to fail with extra fields from request callback'); } catch (e) { expect(e).to.be.instanceOf(MongoMissingCredentialsError); - expect(e.message).to.include('REQUEST_TOKEN_CALLBACK must return a valid object'); + expect(e.message).to.include( + 'User provided OIDC callbacks must return a valid object with an accessToken' + ); } }); }); @@ -514,7 +522,9 @@ describe('MONGODB-OIDC', function () { expect.fail('Expected OIDC auth to fail with missing data from refresh callback'); } catch (e) { expect(e).to.be.instanceOf(MongoMissingCredentialsError); - expect(e.message).to.include('REFRESH_TOKEN_CALLBACK must return a valid object'); + expect(e.message).to.include( + 'User provided OIDC callbacks must return a valid object with an accessToken' + ); } }); }); @@ -551,7 +561,9 @@ describe('MONGODB-OIDC', function () { expect.fail('Expected OIDC auth to fail with extra fields from refresh callback'); } catch (e) { expect(e).to.be.instanceOf(MongoMissingCredentialsError); - expect(e.message).to.include('REFRESH_TOKEN_CALLBACK must return a valid object'); + expect(e.message).to.include( + 'User provided OIDC callbacks must return a valid object with an accessToken' + ); } }); }); From c722b9f471f4d9ebe35b2bbcd46f93fd36df95e0 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 21 Apr 2023 10:48:03 -0400 Subject: [PATCH 41/93] fix: speculative auth --- src/cmap/auth/mongodb_oidc.ts | 5 +++-- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index d88918033fa..8cf145c3136 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -91,8 +91,9 @@ export class MongoDBOIDC extends AuthProvider { handshakeDoc: HandshakeDocument, authContext: AuthContext ): Promise { - const workflow = getWorkflow(getCredentials(authContext)); - const result = await workflow.speculativeAuth(); + const credentials = getCredentials(authContext); + const workflow = getWorkflow(credentials); + const result = await workflow.speculativeAuth(credentials); return { ...handshakeDoc, ...result }; } } diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 3a64b449b8b..7b6702c3a42 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -168,7 +168,7 @@ export class CallbackWorkflow implements Workflow { const entry = this.cache.getEntry( connection.address, credentials.username, - requestCallback || null, + requestCallback, refreshCallback || null ); let result; @@ -180,7 +180,7 @@ export class CallbackWorkflow implements Workflow { return entry.tokenResult; } // If the cache entry is not valid, remove it from the cache and first attempt - // to use the refresh callback to get a new token. If not refresh callback + // to use the refresh callback to get a new token. If no refresh callback // exists, then fallback to the request callback. if (refreshCallback) { result = await refreshCallback(clientInfo, startResult, entry.tokenResult); From e3e242204993d5f3799d4bcd3108d95b74198774 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 21 Apr 2023 10:54:36 -0400 Subject: [PATCH 42/93] fix: change cleanup location --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 7b6702c3a42..d061e84bead 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -163,8 +163,7 @@ export class CallbackWorkflow implements Workflow { requestCallback: OIDCRequestFunction, refreshCallback?: OIDCRefreshFunction ): Promise { - // Delete expired cache entries then get the token from the cache. - this.cache.deleteExpiredEntries(); + // Get the token from the cache. const entry = this.cache.getEntry( connection.address, credentials.username, @@ -206,6 +205,8 @@ export class CallbackWorkflow implements Workflow { result, startResult ); + // Cleanup the cache. + this.cache.deleteExpiredEntries(); return result; } } From fbccca5655bdbf1ee3c01a702392db2a08f6155e Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 21 Apr 2023 11:00:32 -0400 Subject: [PATCH 43/93] fix: change cleanup location --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index d061e84bead..b2923ae883e 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -196,7 +196,9 @@ export class CallbackWorkflow implements Workflow { 'User provided OIDC callbacks must return a valid object with an accessToken.' ); } - // Put an entry into the cache. + // Cleanup the cache. + this.cache.deleteExpiredEntries(); + // Put the new entry into the cache. this.cache.addEntry( connection.address, credentials.username || '', @@ -205,8 +207,6 @@ export class CallbackWorkflow implements Workflow { result, startResult ); - // Cleanup the cache. - this.cache.deleteExpiredEntries(); return result; } } From acde7266aacbbb4bdb36fdae37eb261dd4111abe Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 21 Apr 2023 11:22:11 -0400 Subject: [PATCH 44/93] chore: more debug --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index b2923ae883e..32bfb2e4312 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -163,6 +163,7 @@ export class CallbackWorkflow implements Workflow { requestCallback: OIDCRequestFunction, refreshCallback?: OIDCRefreshFunction ): Promise { + console.log('FETCH ACCESS TOKEN'); // Get the token from the cache. const entry = this.cache.getEntry( connection.address, @@ -170,12 +171,14 @@ export class CallbackWorkflow implements Workflow { requestCallback, refreshCallback || null ); + console.log('ENTRY', entry); let result; const clientInfo = { principalName: credentials.username, timeoutSeconds: TIMEOUT_S }; // Check if there's a token in the cache. if (entry) { // If the cache entry is valid, return the token result. if (entry.isValid()) { + console.log('ENTRY IS VALID'); return entry.tokenResult; } // If the cache entry is not valid, remove it from the cache and first attempt @@ -183,12 +186,15 @@ export class CallbackWorkflow implements Workflow { // exists, then fallback to the request callback. if (refreshCallback) { result = await refreshCallback(clientInfo, startResult, entry.tokenResult); + console.log('USING REFRESH CALLBACK', result); } else { result = await requestCallback(clientInfo, startResult); + console.log('USING REQUEST CALLBACK, NO REFRESH FOUND', result); } } else { // With no token in the cache we use the request callback. result = await requestCallback(clientInfo, startResult); + console.log('USING REQUEST CALLBACK, NO TOKEN IN CACHE', result); } // Validate that the result returned by the callback is acceptable. if (isCallbackResultInvalid(result)) { From aec6db2435140e3c28c4a2cccc35d9517fb8dba9 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 21 Apr 2023 11:36:45 -0400 Subject: [PATCH 45/93] fix: reauth cannot use cached token --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 32bfb2e4312..cf3069e3fdc 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -82,6 +82,7 @@ export class CallbackWorkflow implements Workflow { connection, credentials, entry.serverResult, + reauthenticating, requestCallback, refreshCallback ); @@ -99,6 +100,7 @@ export class CallbackWorkflow implements Workflow { connection, credentials, serverResult, + reauthenticating, requestCallback, refreshCallback ); @@ -160,6 +162,7 @@ export class CallbackWorkflow implements Workflow { connection: Connection, credentials: MongoCredentials, startResult: OIDCMechanismServerStep1, + reauthenticating: boolean, requestCallback: OIDCRequestFunction, refreshCallback?: OIDCRefreshFunction ): Promise { @@ -177,8 +180,8 @@ export class CallbackWorkflow implements Workflow { // Check if there's a token in the cache. if (entry) { // If the cache entry is valid, return the token result. - if (entry.isValid()) { - console.log('ENTRY IS VALID'); + if (entry.isValid() && !reauthenticating) { + console.log('ENTRY IS VALID AND NOT REAUTHENTICATING'); return entry.tokenResult; } // If the cache entry is not valid, remove it from the cache and first attempt From 22f85d24e71250cdf36357ffe4a57a8c4ebe31de Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 21 Apr 2023 13:06:35 -0400 Subject: [PATCH 46/93] test: add speculative auth tests --- .../auth/mongodb_oidc/callback_workflow.ts | 2 +- test/manual/mongodb_oidc.prose.test.ts | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index cf3069e3fdc..f8c10e830d4 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -41,7 +41,7 @@ export class CallbackWorkflow implements Workflow { async speculativeAuth(credentials: MongoCredentials): Promise { const document = startCommandDocument(credentials); document.db = credentials.source; - return document; + return { speculativeAuthenticate: document }; } /** diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 9ff2eca541d..a87bb5044ab 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -733,6 +733,50 @@ describe('MONGODB-OIDC', function () { }); describe('5. Speculative Authentication', function () { + let client: MongoClient; + let collection: Collection; + const requestCallback = createRequestCallback('test_user1', 600); + + // Removes the fail point. + const removeFailPoint = async () => { + return await client.db().admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); + }; + + // Sets up the fail point for the find to reauthenticate. + const setupFailPoint = async () => { + return await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 2 + }, + data: { + failCommands: ['saslStart'], + errorCode: 391 + } + }); + }; + + afterEach(async function () { + await client?.close(); + }); + + before(async function () { + cache.clear(); + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: requestCallback + }); + collection = client.db('test').collection('test'); + await setupFailPoint(); + await collection.findOne(); + await client.close(); + }); + // Clear the cache. // Create a client with a request callback that returns a valid token that will not expire soon. // Set a fail point for saslStart commands of the form: @@ -760,6 +804,14 @@ describe('MONGODB-OIDC', function () { // Set a fail point for saslStart commands. // Perform a find operation that succeeds. // Close the client. + it('successfully speculative authenticates', async function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: requestCallback + }); + await setupFailPoint(); + const result = await collection.findOne(); + expect(result).to.be.null; + }); }); describe('6. Reauthentication', function () { From 75d3aa380fb02044e72e9246014c2eabc638bedb Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 21 Apr 2023 13:11:51 -0400 Subject: [PATCH 47/93] test: fix spec auth test --- test/manual/mongodb_oidc.prose.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index a87bb5044ab..e95d23dd546 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -736,6 +736,9 @@ describe('MONGODB-OIDC', function () { let client: MongoClient; let collection: Collection; const requestCallback = createRequestCallback('test_user1', 600); + const authMechanismProperties = { + REQUEST_TOKEN_CALLBACK: requestCallback + } // Removes the fail point. const removeFailPoint = async () => { @@ -769,7 +772,7 @@ describe('MONGODB-OIDC', function () { before(async function () { cache.clear(); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: requestCallback + authMechanismProperties: authMechanismProperties }); collection = client.db('test').collection('test'); await setupFailPoint(); @@ -806,7 +809,7 @@ describe('MONGODB-OIDC', function () { // Close the client. it('successfully speculative authenticates', async function () { client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: requestCallback + authMechanismProperties: authMechanismProperties }); await setupFailPoint(); const result = await collection.findOne(); From ecab8d5f5707b89af46ddcc554e82e3d269e928a Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 21 Apr 2023 13:19:08 -0400 Subject: [PATCH 48/93] test: fix test cleanup --- src/cmap/connect.ts | 1 + test/manual/mongodb_oidc.prose.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index 6c89fdcd887..21a47a5d4f3 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -130,6 +130,7 @@ async function performInitialHandshake( const start = new Date().getTime(); const response = await conn.commandAsync(ns('admin.$cmd'), handshakeDoc, handshakeOptions); + console.log('RESPONSE', response); if (!('isWritablePrimary' in response)) { // Provide hello-style response document. diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index e95d23dd546..e519e0343cf 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -766,6 +766,7 @@ describe('MONGODB-OIDC', function () { }; afterEach(async function () { + await removeFailPoint(); await client?.close(); }); From 6a07a158676a988ebd9318196ec4f060ac92b7ad Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 21 Apr 2023 13:21:27 -0400 Subject: [PATCH 49/93] fix: add db to speculative auth doc --- src/cmap/auth/mongodb_oidc/service_workflow.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/service_workflow.ts b/src/cmap/auth/mongodb_oidc/service_workflow.ts index 85a43294b1e..b0fb2cc142a 100644 --- a/src/cmap/auth/mongodb_oidc/service_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/service_workflow.ts @@ -24,9 +24,11 @@ export abstract class ServiceWorkflow implements Workflow { /** * Get the document to add for speculative authentication. */ - async speculativeAuth(): Promise { + async speculativeAuth(credentials: MongoCredentials): Promise { const token = await this.getToken(); - return { speculativeAuthenticate: commandDocument(token) }; + const document = commandDocument(token); + document.db = credentials.source; + return { speculativeAuthenticate: document }; } /** From f5ebffe5ec5b76e472ec45a1a770b37856d02fa2 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 21 Apr 2023 13:26:57 -0400 Subject: [PATCH 50/93] test: use correct error code --- test/manual/mongodb_oidc.prose.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index e519e0343cf..555e32c6375 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -760,7 +760,7 @@ describe('MONGODB-OIDC', function () { }, data: { failCommands: ['saslStart'], - errorCode: 391 + errorCode: 18 } }); }; From 556b99f0fb5e5304c909d6b98a069fdf4cdcfb5e Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 21 Apr 2023 13:39:46 -0400 Subject: [PATCH 51/93] fix: speculative auth --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 5 +++-- src/cmap/connect.ts | 1 - test/manual/mongodb_oidc.prose.test.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index f8c10e830d4..d0e80f4508b 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -61,6 +61,7 @@ export class CallbackWorkflow implements Workflow { 'Auth mechanism property REQUEST_TOKEN_CALLBACK is required.' ); } + console.log('RESPONSE', response); // Look for an existing entry in the cache. const entry = this.cache.getEntry( connection.address, @@ -125,8 +126,8 @@ export class CallbackWorkflow implements Workflow { response?: Document ): Promise { let result; - if (response?.speculativeAuthentication) { - result = response.speculativeAuthentication; + if (response?.speculativeAuthenticate) { + result = response.speculativeAuthenticate; } else { result = await connection.commandAsync( ns(credentials.source), diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index 21a47a5d4f3..6c89fdcd887 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -130,7 +130,6 @@ async function performInitialHandshake( const start = new Date().getTime(); const response = await conn.commandAsync(ns('admin.$cmd'), handshakeDoc, handshakeOptions); - console.log('RESPONSE', response); if (!('isWritablePrimary' in response)) { // Provide hello-style response document. diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 555e32c6375..fa8f8a21682 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -738,7 +738,7 @@ describe('MONGODB-OIDC', function () { const requestCallback = createRequestCallback('test_user1', 600); const authMechanismProperties = { REQUEST_TOKEN_CALLBACK: requestCallback - } + }; // Removes the fail point. const removeFailPoint = async () => { @@ -748,7 +748,7 @@ describe('MONGODB-OIDC', function () { }); }; - // Sets up the fail point for the find to reauthenticate. + // Sets up the fail point for the saslStart const setupFailPoint = async () => { return await client .db() From 434bc010de87578f000f545f76aefc345d9c0308 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 21 Apr 2023 13:55:57 -0400 Subject: [PATCH 52/93] test: more debug --- .../auth/mongodb_oidc/callback_workflow.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index d0e80f4508b..02deac9aaee 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -69,14 +69,22 @@ export class CallbackWorkflow implements Workflow { requestCallback, refreshCallback || null ); + console.log('ENTRY', entry, entry?.isValid()); let result; // Reauthentication must go through all the steps again regards of a cache entry // being present. if (entry && !reauthenticating) { if (entry.isValid()) { + console.log('FINISHING'); // Presence of a valid cache entry means we can skip to the finishing step. - result = await this.finishAuthentication(connection, credentials, entry.tokenResult); + result = await this.finishAuthentication( + connection, + credentials, + entry.tokenResult, + response?.speculativeAuthenticate?.conversationId + ); } else { + console.log('FETCH AND FINISH'); // Presence of an expired cache entry means we must fetch a new one and // then execute the final step. const tokenResult = await this.fetchAccessToken( @@ -87,16 +95,24 @@ export class CallbackWorkflow implements Workflow { requestCallback, refreshCallback ); - result = await this.finishAuthentication(connection, credentials, tokenResult); + result = await this.finishAuthentication( + connection, + credentials, + tokenResult, + response?.speculativeAuthenticate?.conversationId + ); } } else { + console.log('NO ENTRY IN CACHE'); // No entry in the cache requires us to do all authentication steps // from start to finish, including getting a fresh token for the cache. const startDocument = await this.startAuthentication(connection, credentials, response); + console.log('START DOCUMENT', startDocument); const conversationId = startDocument.conversationId; const serverResult = BSON.deserialize( startDocument.payload.buffer ) as OIDCMechanismServerStep1; + console.log('SERVER_RESULT', serverResult); const tokenResult = await this.fetchAccessToken( connection, credentials, From bf15cc06ef5f76750dab5c694f38019c70e815cf Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 21 Apr 2023 14:16:00 -0400 Subject: [PATCH 53/93] fix: no spec auth when reauth --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 10 ++++++++-- test/manual/mongodb_oidc.prose.test.ts | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 02deac9aaee..b2e02b0dafd 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -106,7 +106,12 @@ export class CallbackWorkflow implements Workflow { console.log('NO ENTRY IN CACHE'); // No entry in the cache requires us to do all authentication steps // from start to finish, including getting a fresh token for the cache. - const startDocument = await this.startAuthentication(connection, credentials, response); + const startDocument = await this.startAuthentication( + connection, + credentials, + reauthenticating, + response + ); console.log('START DOCUMENT', startDocument); const conversationId = startDocument.conversationId; const serverResult = BSON.deserialize( @@ -139,10 +144,11 @@ export class CallbackWorkflow implements Workflow { private async startAuthentication( connection: Connection, credentials: MongoCredentials, + reauthenticating: boolean, response?: Document ): Promise { let result; - if (response?.speculativeAuthenticate) { + if (!reauthenticating && response?.speculativeAuthenticate) { result = response.speculativeAuthenticate; } else { result = await connection.commandAsync( diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index fa8f8a21682..00baed6703b 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -765,7 +765,7 @@ describe('MONGODB-OIDC', function () { }); }; - afterEach(async function () { + after(async function () { await removeFailPoint(); await client?.close(); }); From bec5d9e95ac0847a2636211312553c99f7685f37 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 21 Apr 2023 14:31:19 -0400 Subject: [PATCH 54/93] fix: client leak --- test/manual/mongodb_oidc.prose.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 00baed6703b..4c3f57cee37 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -734,7 +734,6 @@ describe('MONGODB-OIDC', function () { describe('5. Speculative Authentication', function () { let client: MongoClient; - let collection: Collection; const requestCallback = createRequestCallback('test_user1', 600); const authMechanismProperties = { REQUEST_TOKEN_CALLBACK: requestCallback @@ -775,9 +774,8 @@ describe('MONGODB-OIDC', function () { client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: authMechanismProperties }); - collection = client.db('test').collection('test'); await setupFailPoint(); - await collection.findOne(); + await client.db('test').collection('test').findOne(); await client.close(); }); @@ -813,7 +811,7 @@ describe('MONGODB-OIDC', function () { authMechanismProperties: authMechanismProperties }); await setupFailPoint(); - const result = await collection.findOne(); + const result = await client.db('test').collection('test').findOne(); expect(result).to.be.null; }); }); From d9ef4d11ce3a6299f895d7c7d89a20ba9e808dce Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 21 Apr 2023 14:37:49 -0400 Subject: [PATCH 55/93] fix: properly close, remove debug --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 13 ------------- src/cmap/connection_pool.ts | 1 - test/manual/mongodb_oidc.prose.test.ts | 6 +----- 3 files changed, 1 insertion(+), 19 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index b2e02b0dafd..f7be99ca4ab 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -61,7 +61,6 @@ export class CallbackWorkflow implements Workflow { 'Auth mechanism property REQUEST_TOKEN_CALLBACK is required.' ); } - console.log('RESPONSE', response); // Look for an existing entry in the cache. const entry = this.cache.getEntry( connection.address, @@ -69,13 +68,11 @@ export class CallbackWorkflow implements Workflow { requestCallback, refreshCallback || null ); - console.log('ENTRY', entry, entry?.isValid()); let result; // Reauthentication must go through all the steps again regards of a cache entry // being present. if (entry && !reauthenticating) { if (entry.isValid()) { - console.log('FINISHING'); // Presence of a valid cache entry means we can skip to the finishing step. result = await this.finishAuthentication( connection, @@ -84,7 +81,6 @@ export class CallbackWorkflow implements Workflow { response?.speculativeAuthenticate?.conversationId ); } else { - console.log('FETCH AND FINISH'); // Presence of an expired cache entry means we must fetch a new one and // then execute the final step. const tokenResult = await this.fetchAccessToken( @@ -103,7 +99,6 @@ export class CallbackWorkflow implements Workflow { ); } } else { - console.log('NO ENTRY IN CACHE'); // No entry in the cache requires us to do all authentication steps // from start to finish, including getting a fresh token for the cache. const startDocument = await this.startAuthentication( @@ -112,12 +107,10 @@ export class CallbackWorkflow implements Workflow { reauthenticating, response ); - console.log('START DOCUMENT', startDocument); const conversationId = startDocument.conversationId; const serverResult = BSON.deserialize( startDocument.payload.buffer ) as OIDCMechanismServerStep1; - console.log('SERVER_RESULT', serverResult); const tokenResult = await this.fetchAccessToken( connection, credentials, @@ -189,7 +182,6 @@ export class CallbackWorkflow implements Workflow { requestCallback: OIDCRequestFunction, refreshCallback?: OIDCRefreshFunction ): Promise { - console.log('FETCH ACCESS TOKEN'); // Get the token from the cache. const entry = this.cache.getEntry( connection.address, @@ -197,14 +189,12 @@ export class CallbackWorkflow implements Workflow { requestCallback, refreshCallback || null ); - console.log('ENTRY', entry); let result; const clientInfo = { principalName: credentials.username, timeoutSeconds: TIMEOUT_S }; // Check if there's a token in the cache. if (entry) { // If the cache entry is valid, return the token result. if (entry.isValid() && !reauthenticating) { - console.log('ENTRY IS VALID AND NOT REAUTHENTICATING'); return entry.tokenResult; } // If the cache entry is not valid, remove it from the cache and first attempt @@ -212,15 +202,12 @@ export class CallbackWorkflow implements Workflow { // exists, then fallback to the request callback. if (refreshCallback) { result = await refreshCallback(clientInfo, startResult, entry.tokenResult); - console.log('USING REFRESH CALLBACK', result); } else { result = await requestCallback(clientInfo, startResult); - console.log('USING REQUEST CALLBACK, NO REFRESH FOUND', result); } } else { // With no token in the cache we use the request callback. result = await requestCallback(clientInfo, startResult); - console.log('USING REQUEST CALLBACK, NO TOKEN IN CACHE', result); } // Validate that the result returned by the callback is acceptable. if (isCallbackResultInvalid(result)) { diff --git a/src/cmap/connection_pool.ts b/src/cmap/connection_pool.ts index 91f354815dd..01feb78fddf 100644 --- a/src/cmap/connection_pool.ts +++ b/src/cmap/connection_pool.ts @@ -580,7 +580,6 @@ export class ConnectionPool extends TypedEventEmitter { callback: Callback ) { if (fnErr instanceof MongoError && fnErr.code === MONGODB_ERROR_CODES.Reauthenticate) { - console.log('REAUTHENTICATING'); this.reauthenticate(conn, fn, (error, res) => { if (error) { return callback(error); diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 4c3f57cee37..b19a91b9ab0 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -706,7 +706,6 @@ describe('MONGODB-OIDC', function () { try { await collection.findOne(); } catch (e) { - console.log(e); expect(cache.entries.size).to.equal(0); } }); @@ -764,7 +763,7 @@ describe('MONGODB-OIDC', function () { }); }; - after(async function () { + afterEach(async function () { await removeFailPoint(); await client?.close(); }); @@ -839,19 +838,16 @@ describe('MONGODB-OIDC', function () { const commandFailedEvents: CommandFailedEvent[] = []; const commandStartedListener = event => { - console.log('commandStarted', event); if (event.commandName === 'find') { commandStartedEvents.push(event); } }; const commandSucceededListener = event => { - console.log('commandSuceeded', event); if (event.commandName === 'find') { commandSucceededEvents.push(event); } }; const commandFailedListener = event => { - console.log('commandFailed', event); if (event.commandName === 'find') { commandFailedEvents.push(event); } From 5ca0e6b11c3e91d8eb50106ba320bdc087b07411 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 27 Apr 2023 13:22:21 +0200 Subject: [PATCH 56/93] feat: oidc updates --- src/cmap/auth/mongodb_oidc.ts | 36 ++-- .../auth/mongodb_oidc/callback_workflow.ts | 75 +++++--- .../auth/mongodb_oidc/token_entry_cache.ts | 16 +- src/index.ts | 6 +- src/utils.ts | 17 ++ test/manual/mongodb_oidc.prose.test.ts | 107 ++++++++++-- test/unit/utils.test.ts | 165 ++++++++++++++++++ 7 files changed, 356 insertions(+), 66 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index 8cf145c3136..418fd5c5910 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -1,16 +1,23 @@ import { MongoInvalidArgumentError, MongoMissingCredentialsError } from '../../error'; +import { hostMatchesWildcards } from '../../utils'; import type { HandshakeDocument } from '../connect'; import { type AuthContext, AuthProvider } from './auth_provider'; -import type { MongoCredentials } from './mongo_credentials'; +import { DEFAULT_ALLOWED_HOSTS, MongoCredentials } from './mongo_credentials'; import { AwsServiceWorkflow } from './mongodb_oidc/aws_service_workflow'; import { CallbackWorkflow } from './mongodb_oidc/callback_workflow'; import type { Workflow } from './mongodb_oidc/workflow'; +/** + * @internal + * The current version of OIDC implementation. + */ +export const OIDC_VERSION = 0; + /** * @public * @experimental */ -export interface OIDCMechanismServerStep1 { +export interface IdPServerInfo { issuer: string; clientId: string; requestScopes?: string[]; @@ -20,7 +27,7 @@ export interface OIDCMechanismServerStep1 { * @public * @experimental */ -export interface OIDCRequestTokenResult { +export interface IdPServerResponse { accessToken: string; expiresInSeconds?: number; refreshToken?: string; @@ -30,10 +37,11 @@ export interface OIDCRequestTokenResult { * @public * @experimental */ -export interface OIDCClientInfo { - principalName: string; +export interface OIDCCallbackContext { + refreshToken?: string; timeoutSeconds?: number; timeoutContext?: AbortSignal; + version: number; } /** @@ -41,19 +49,18 @@ export interface OIDCClientInfo { * @experimental */ export type OIDCRequestFunction = ( - clientInfo: OIDCClientInfo, - serverInfo: OIDCMechanismServerStep1 -) => Promise; + info: IdPServerInfo, + context: OIDCCallbackContext +) => Promise; /** * @public * @experimental */ export type OIDCRefreshFunction = ( - clientInfo: OIDCClientInfo, - serverInfo: OIDCMechanismServerStep1, - tokenResult: OIDCRequestTokenResult -) => Promise; + info: IdPServerInfo, + context: OIDCCallbackContext +) => Promise; type ProviderName = 'aws' | 'callback'; @@ -92,6 +99,11 @@ export class MongoDBOIDC extends AuthProvider { authContext: AuthContext ): Promise { const credentials = getCredentials(authContext); + const { connection } = authContext; + const allowedHosts = credentials.mechanismProperties.ALLOWED_HOSTS || DEFAULT_ALLOWED_HOSTS; + if (!hostMatchesWildcards(connection.address, allowedHosts)) { + throw new MongoInvalidArgumentError('Host does not match provided ALLOWED_HOSTS values'); + } const workflow = getWorkflow(credentials); const result = await workflow.speculativeAuth(credentials); return { ...handshakeDoc, ...result }; diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index f7be99ca4ab..6dda3b36e44 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -1,14 +1,21 @@ import { Binary, BSON, type Document } from 'bson'; -import { MongoInvalidArgumentError, MongoMissingCredentialsError } from '../../../error'; +import { + MONGODB_ERROR_CODES, + MongoError, + MongoInvalidArgumentError, + MongoMissingCredentialsError +} from '../../../error'; import { ns } from '../../../utils'; import type { Connection } from '../../connection'; import type { MongoCredentials } from '../mongo_credentials'; -import type { - OIDCMechanismServerStep1, +import { + IdPServerInfo, + IdPServerResponse, + OIDC_VERSION, + OIDCCallbackContext, OIDCRefreshFunction, - OIDCRequestFunction, - OIDCRequestTokenResult + OIDCRequestFunction } from '../mongodb_oidc'; import { AuthMechanism } from '../providers'; import { TokenEntryCache } from './token_entry_cache'; @@ -71,8 +78,8 @@ export class CallbackWorkflow implements Workflow { let result; // Reauthentication must go through all the steps again regards of a cache entry // being present. - if (entry && !reauthenticating) { - if (entry.isValid()) { + if (entry) { + if (entry.isValid() && !reauthenticating) { // Presence of a valid cache entry means we can skip to the finishing step. result = await this.finishAuthentication( connection, @@ -91,12 +98,33 @@ export class CallbackWorkflow implements Workflow { requestCallback, refreshCallback ); - result = await this.finishAuthentication( - connection, - credentials, - tokenResult, - response?.speculativeAuthenticate?.conversationId - ); + try { + result = await this.finishAuthentication( + connection, + credentials, + tokenResult, + response?.speculativeAuthenticate?.conversationId + ); + } catch (error) { + // If we are reauthenticating and this errors with reauthentication + // required, we need to do the entire process over again and clear + // the cache entry. + if ( + reauthenticating && + error instanceof MongoError && + error.code === MONGODB_ERROR_CODES.Reauthenticate + ) { + this.cache.deleteEntry( + connection.address, + credentials.username || '', + requestCallback, + refreshCallback || null + ); + result = await this.execute(connection, credentials, reauthenticating); + } else { + throw error; + } + } } } else { // No entry in the cache requires us to do all authentication steps @@ -108,9 +136,7 @@ export class CallbackWorkflow implements Workflow { response ); const conversationId = startDocument.conversationId; - const serverResult = BSON.deserialize( - startDocument.payload.buffer - ) as OIDCMechanismServerStep1; + const serverResult = BSON.deserialize(startDocument.payload.buffer) as IdPServerInfo; const tokenResult = await this.fetchAccessToken( connection, credentials, @@ -159,7 +185,7 @@ export class CallbackWorkflow implements Workflow { private async finishAuthentication( connection: Connection, credentials: MongoCredentials, - tokenResult: OIDCRequestTokenResult, + tokenResult: IdPServerResponse, conversationId?: number ): Promise { const result = await connection.commandAsync( @@ -177,11 +203,11 @@ export class CallbackWorkflow implements Workflow { private async fetchAccessToken( connection: Connection, credentials: MongoCredentials, - startResult: OIDCMechanismServerStep1, + serverInfo: IdPServerInfo, reauthenticating: boolean, requestCallback: OIDCRequestFunction, refreshCallback?: OIDCRefreshFunction - ): Promise { + ): Promise { // Get the token from the cache. const entry = this.cache.getEntry( connection.address, @@ -190,7 +216,7 @@ export class CallbackWorkflow implements Workflow { refreshCallback || null ); let result; - const clientInfo = { principalName: credentials.username, timeoutSeconds: TIMEOUT_S }; + const context: OIDCCallbackContext = { timeoutSeconds: TIMEOUT_S, version: OIDC_VERSION }; // Check if there's a token in the cache. if (entry) { // If the cache entry is valid, return the token result. @@ -201,13 +227,14 @@ export class CallbackWorkflow implements Workflow { // to use the refresh callback to get a new token. If no refresh callback // exists, then fallback to the request callback. if (refreshCallback) { - result = await refreshCallback(clientInfo, startResult, entry.tokenResult); + context.refreshToken = entry.tokenResult.refreshToken; + result = await refreshCallback(serverInfo, context); } else { - result = await requestCallback(clientInfo, startResult); + result = await requestCallback(serverInfo, context); } } else { // With no token in the cache we use the request callback. - result = await requestCallback(clientInfo, startResult); + result = await requestCallback(serverInfo, context); } // Validate that the result returned by the callback is acceptable. if (isCallbackResultInvalid(result)) { @@ -224,7 +251,7 @@ export class CallbackWorkflow implements Workflow { requestCallback, refreshCallback || null, result, - startResult + serverInfo ); return result; } diff --git a/src/cmap/auth/mongodb_oidc/token_entry_cache.ts b/src/cmap/auth/mongodb_oidc/token_entry_cache.ts index ec6ed3eb3aa..1dd3f4d6225 100644 --- a/src/cmap/auth/mongodb_oidc/token_entry_cache.ts +++ b/src/cmap/auth/mongodb_oidc/token_entry_cache.ts @@ -1,8 +1,8 @@ import type { - OIDCMechanismServerStep1, + PrincipalStepRequest, OIDCRefreshFunction, OIDCRequestFunction, - OIDCRequestTokenResult + IdPServerResponse } from '../mongodb_oidc'; /* 5 minutes in milliseonds */ @@ -22,16 +22,16 @@ FN_HASHES.set(NO_FUNCTION, FN_HASH_COUNTER); /** @internal */ export class TokenEntry { - tokenResult: OIDCRequestTokenResult; - serverResult: OIDCMechanismServerStep1; + tokenResult: IdPServerResponse; + serverResult: PrincipalStepRequest; expiration: number; /** * Instantiate the entry. */ constructor( - tokenResult: OIDCRequestTokenResult, - serverResult: OIDCMechanismServerStep1, + tokenResult: IdPServerResponse, + serverResult: PrincipalStepRequest, expiration: number ) { this.tokenResult = tokenResult; @@ -67,8 +67,8 @@ export class TokenEntryCache { username: string, requestFn: OIDCRequestFunction | null, refreshFn: OIDCRefreshFunction | null, - tokenResult: OIDCRequestTokenResult, - serverResult: OIDCMechanismServerStep1 + tokenResult: IdPServerResponse, + serverResult: PrincipalStepRequest ): TokenEntry { const entry = new TokenEntry( tokenResult, diff --git a/src/index.ts b/src/index.ts index f17f832c1f4..eb23da3825e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -204,11 +204,11 @@ export type { MongoCredentialsOptions } from './cmap/auth/mongo_credentials'; export type { - OIDCClientInfo, - OIDCMechanismServerStep1, + OIDCCallbackContext as OIDCClientInfo, + PrincipalStepRequest as OIDCMechanismServerStep1, OIDCRefreshFunction, OIDCRequestFunction, - OIDCRequestTokenResult + IdPServerResponse as OIDCRequestTokenResult } from './cmap/auth/mongodb_oidc'; export type { BinMsg, diff --git a/src/utils.ts b/src/utils.ts index 95bf757af2d..c5792ae1d91 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -58,6 +58,23 @@ export const ByteUtils = { } }; +/** + * Determines if a connection's address matches a user provided list + * of domain wildcards. + */ +export function hostMatchesWildcards(address: string, wildcards: string[]): boolean { + const host = HostAddress.fromString(address).host; + for (const wildcard of wildcards) { + if ( + host === wildcard || + (wildcard.startsWith('*.') && host?.endsWith(wildcard.substring(2, wildcard.length))) + ) { + return true; + } + } + return false; +} + /** * Throws if collectionName is not a valid mongodb collection namespace. * @internal diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index b19a91b9ab0..2c9bc9a83bd 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -9,12 +9,13 @@ import { CommandFailedEvent, CommandStartedEvent, CommandSucceededEvent, + IdPServerInfo, MongoClient, + MongoInvalidArgumentError, MongoMissingCredentialsError, MongoServerError, OIDC_WORKFLOWS, - OIDCClientInfo, - OIDCMechanismServerStep1, + OIDCCallbackContext, OIDCRequestTokenResult } from '../mongodb'; @@ -34,14 +35,14 @@ describe('MONGODB-OIDC', function () { expiresInSeconds?: number, extraFields?: any ) => { - return async (clientInfo: OIDCClientInfo, serverInfo: OIDCMechanismServerStep1) => { + return async (info: IdPServerInfo, context: OIDCCallbackContext) => { const token = await readFile(path.join(process.env.OIDC_TOKEN_DIR, username), { encoding: 'utf8' }); // Do some basic property assertions. - expect(clientInfo).to.have.property('timeoutSeconds'); - expect(serverInfo).to.have.property('issuer'); - expect(serverInfo).to.have.property('clientId'); + expect(context).to.have.property('timeoutSeconds'); + expect(info).to.have.property('issuer'); + expect(info).to.have.property('clientId'); return generateResult(token, expiresInSeconds, extraFields); }; }; @@ -52,19 +53,15 @@ describe('MONGODB-OIDC', function () { expiresInSeconds?: number, extraFields?: any ) => { - return async ( - clientInfo: OIDCClientInfo, - serverInfo: OIDCMechanismServerStep1, - tokenResult: OIDCRequestTokenResult - ) => { + return async (info: IdPServerInfo, context: OIDCCallbackContext) => { const token = await readFile(path.join(process.env.OIDC_TOKEN_DIR, username), { encoding: 'utf8' }); // Do some basic property assertions. - expect(clientInfo).to.have.property('timeoutSeconds'); - expect(serverInfo).to.have.property('issuer'); - expect(serverInfo).to.have.property('clientId'); - expect(tokenResult).to.have.property('accessToken'); + expect(context).to.have.property('timeoutSeconds'); + expect(info).to.have.property('issuer'); + expect(info).to.have.property('clientId'); + expect(info).to.have.property('accessToken'); return generateResult(token, expiresInSeconds, extraFields); }; }; @@ -212,13 +209,85 @@ describe('MONGODB-OIDC', function () { }); describe('1.6 Allowed Hosts Blocked', function () { + before(function () { + cache.clear(); + }); + // Clear the cache. - // Create a client that uses the OIDC url and a request callback, and an ALLOWED_HOSTS that is an empty list. - // Assert that a find operation fails with a client-side error. + // Create a client that uses the OIDC url and a request callback, and an + // ``ALLOWED_HOSTS`` that is an empty list. + // Assert that a ``find`` operation fails with a client-side error. // Close the client. - // Create a client that uses the OIDC url and a request callback, and an ALLOWED_HOSTS that contains ["localhost1"]. - // Assert that a find operation fails with a client-side error. + context('when ALLOWED_HOSTS is empty', function () { + before(function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + ALLOWED_HOSTS: [] + } + }); + collection = client.db('test').collection('test'); + }); + + it('fails validation', async function () { + try { + await collection.findOne(); + } catch (error) { + expect(error).to.be.instanceOf(MongoInvalidArgumentError); + expect(error.message).to.include('Host does not match provided ALLOWED_HOSTS values'); + } + }); + }); + + // Create a client that uses the url ``mongodb://localhost/?authMechanism=MONGODB-OIDC&ignored=example.com`` a request callback, and an + // ``ALLOWED_HOSTS`` that contains ["example.com"]. + // Assert that a ``find`` operation fails with a client-side error. // Close the client. + context('when ALLOWED_HOSTS does not match', function () { + before(function () { + client = new MongoClient( + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&ignored=example.com', + { + authMechanismProperties: { + ALLOWED_HOSTS: ['examle.com'] + } + } + ); + collection = client.db('test').collection('test'); + }); + + it('fails validation', async function () { + try { + await collection.findOne(); + } catch (error) { + expect(error).to.be.instanceOf(MongoInvalidArgumentError); + expect(error.message).to.include('Host does not match provided ALLOWED_HOSTS values'); + } + }); + }); + + // Create a client that uses the url ``mongodb://evilmongodb.com`` a request + // callback, and an ``ALLOWED_HOSTS`` that contains ``*mongodb.com``. + // Assert that a ``find`` operation fails with a client-side error. + // Close the client. + context('when ALLOWED_HOSTS is invalid', function () { + before(function () { + client = new MongoClient('mongodb://evilmongodb.com/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + ALLOWED_HOSTS: ['*mongodb.com'] + } + }); + collection = client.db('test').collection('test'); + }); + + it('fails validation', async function () { + try { + await collection.findOne(); + } catch (error) { + expect(error).to.be.instanceOf(MongoInvalidArgumentError); + expect(error.message).to.include('Host does not match provided ALLOWED_HOSTS values'); + } + }); + }); }); }); diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts index df0efc599eb..59af7520934 100644 --- a/test/unit/utils.test.ts +++ b/test/unit/utils.test.ts @@ -6,6 +6,7 @@ import { compareObjectId, eachAsync, HostAddress, + hostMatchesWildcards, isHello, LEGACY_HELLO_COMMAND, List, @@ -18,6 +19,170 @@ import { } from '../mongodb'; describe('driver utils', function () { + describe('.hostMatchesWildcards', function () { + context('when using domains', function () { + context('when using exact match', function () { + context('when the address contains a port', function () { + context('when the host matches at least one', function () { + it('returns true', function () { + expect(hostMatchesWildcards('localhost:27017', ['localhost', 'other'])).to.be.true; + }); + }); + + context('when the host does not match any', function () { + it('returns false', function () { + expect(hostMatchesWildcards('localhost:27017', ['test1', 'test2'])).to.be.false; + }); + }); + + context('when the host matches a FQDN', function () { + it('returns true', function () { + expect( + hostMatchesWildcards('mongodb.net:27017', ['mongodb.net', 'other']) + ).to.be.true; + }); + }); + + context('when the host does not match a FQDN', function () { + it('returns false', function () { + expect( + hostMatchesWildcards('mongodb.net:27017', ['mongodb.com', 'other']) + ).to.be.false; + }); + }); + + context('when the host matches a FQDN with subdomain', function () { + it('returns true', function () { + expect( + hostMatchesWildcards('prod.mongodb.net:27017', ['prod.mongodb.net', 'other']) + ).to.be.true; + }); + }); + + context('when the host does not match a FQDN with subdomain', function () { + it('returns false', function () { + expect( + hostMatchesWildcards('prod.mongodb.net:27017', [ + 'dev.mongodb.net', + 'prod.mongodb.com' + ]) + ).to.be.false; + }); + }); + }); + + context('when the address does not contain a port', function () { + context('when the host matches at least one', function () { + it('returns true', function () { + expect(hostMatchesWildcards('localhost', ['localhost', 'other'])).to.be.true; + }); + }); + + context('when the host does not match any', function () { + it('returns false', function () { + expect(hostMatchesWildcards('localhost', ['test1', 'test2'])).to.be.false; + }); + }); + }); + }); + + context('when using a leading * with domains', function () { + context('when the address contains a port', function () { + context('when the host matches at least one', function () { + it('returns true', function () { + expect(hostMatchesWildcards('localhost:27017', ['*.localhost', 'other'])).to.be.true; + }); + }); + + context('when the host does not match any', function () { + it('returns false', function () { + expect(hostMatchesWildcards('localhost:27017', ['*.test1', 'test2'])).to.be.false; + }); + }); + + context('when the host matches a FQDN', function () { + it('returns true', function () { + expect( + hostMatchesWildcards('mongodb.net:27017', ['*.mongodb.net', 'other']) + ).to.be.true; + }); + }); + + context('when the host does not match a FQDN', function () { + it('returns false', function () { + expect( + hostMatchesWildcards('mongodb.net:27017', ['*.mongodb.com', 'other']) + ).to.be.false; + }); + }); + + context('when the host matches a FQDN with subdomain', function () { + it('returns true', function () { + expect( + hostMatchesWildcards('prod.mongodb.net:27017', ['*.prod.mongodb.net', 'other']) + ).to.be.true; + }); + }); + + context('when the host does not match a FQDN with subdomain', function () { + it('returns false', function () { + expect( + hostMatchesWildcards('prod.mongodb.net:27017', [ + '*.dev.mongodb.net', + '*.prod.mongodb.com' + ]) + ).to.be.false; + }); + }); + }); + + context('when the address does not contain a port', function () { + context('when the host matches at least one', function () { + it('returns true', function () { + expect(hostMatchesWildcards('localhost', ['*.localhost', 'other'])).to.be.true; + }); + }); + + context('when the host does not match any', function () { + it('returns false', function () { + expect(hostMatchesWildcards('localhost', ['*.test1', 'test2'])).to.be.false; + }); + }); + }); + }); + }); + + context('when using IP addresses', function () { + context('when using IPv4', function () { + context('when the host matches at least one', function () { + it('returns true', function () { + expect(hostMatchesWildcards('127.0.0.1:27017', ['127.0.0.1', 'other'])).to.be.true; + }); + }); + + context('when the host does not match any', function () { + it('returns false', function () { + expect(hostMatchesWildcards('127.0.0.1:27017', ['127.0.0.2', 'test2'])).to.be.false; + }); + }); + }); + + context('when using IPv6', function () { + context('when the host matches at least one', function () { + it('returns true', function () { + expect(hostMatchesWildcards('[::1]:27017', ['::1', 'other'])).to.be.true; + }); + }); + + context('when the host does not match any', function () { + it('returns false', function () { + expect(hostMatchesWildcards('[::1]:27017', ['::2', 'test2'])).to.be.false; + }); + }); + }); + }); + }); + context('eachAsync()', function () { it('should callback with an error', function (done) { eachAsync( From 46534f1518de8c6ae75f335659f30c0181c002ab Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 27 Apr 2023 13:58:54 +0200 Subject: [PATCH 57/93] test: fixing tests --- src/cmap/auth/mongodb_oidc.ts | 7 +- test/manual/mongodb_oidc.prose.test.ts | 92 ++++++++++++++++++++++++-- 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index 418fd5c5910..a37d387d206 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -100,8 +100,11 @@ export class MongoDBOIDC extends AuthProvider { ): Promise { const credentials = getCredentials(authContext); const { connection } = authContext; - const allowedHosts = credentials.mechanismProperties.ALLOWED_HOSTS || DEFAULT_ALLOWED_HOSTS; - if (!hostMatchesWildcards(connection.address, allowedHosts)) { + const mechanismProperties = credentials.mechanismProperties; + const allowedHosts = mechanismProperties.ALLOWED_HOSTS || DEFAULT_ALLOWED_HOSTS; + const providerName = mechanismProperties.PROVIDER_NAME; + // Cloud provider automatic auth does not validate allowed hosts. + if (!providerName && !hostMatchesWildcards(connection.address, allowedHosts)) { throw new MongoInvalidArgumentError('Host does not match provided ALLOWED_HOSTS values'); } const workflow = getWorkflow(credentials); diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 2c9bc9a83bd..74fe52d8f95 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -61,7 +61,6 @@ describe('MONGODB-OIDC', function () { expect(context).to.have.property('timeoutSeconds'); expect(info).to.have.property('issuer'); expect(info).to.have.property('clientId'); - expect(info).to.have.property('accessToken'); return generateResult(token, expiresInSeconds, extraFields); }; }; @@ -222,7 +221,8 @@ describe('MONGODB-OIDC', function () { before(function () { client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { - ALLOWED_HOSTS: [] + ALLOWED_HOSTS: [], + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 600) } }); collection = client.db('test').collection('test'); @@ -248,7 +248,8 @@ describe('MONGODB-OIDC', function () { 'mongodb://localhost/?authMechanism=MONGODB-OIDC&ignored=example.com', { authMechanismProperties: { - ALLOWED_HOSTS: ['examle.com'] + ALLOWED_HOSTS: ['examle.com'], + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 600) } } ); @@ -273,7 +274,8 @@ describe('MONGODB-OIDC', function () { before(function () { client = new MongoClient('mongodb://evilmongodb.com/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { - ALLOWED_HOSTS: ['*mongodb.com'] + ALLOWED_HOSTS: ['*mongodb.com'], + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 600) } }); collection = client.db('test').collection('test'); @@ -1010,6 +1012,43 @@ describe('MONGODB-OIDC', function () { }); describe('6.2 Retries and Succeeds with Cache', function () { + const requestCallback = createRequestCallback('test_user1', 600); + const refreshCallback = createRefreshCallback('test_user1', 600); + const authMechanismProperties = { + REQUEST_TOKEN_CALLBACK: requestCallback, + REFRESH_TOKEN_CALLBACK: refreshCallback + }; + // Sets up the fail point for the find to reauthenticate. + const setupFailPoint = async () => { + return await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 1 + }, + data: { + failCommands: ['find', 'saslStart'], + errorCode: 391 + } + }); + }; + + before(async function () { + cache.clear(); + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: authMechanismProperties + }); + await client.db('test').collection('test').findOne(); + await setupFailPoint(); + }); + + afterEach(async function () { + await removeFailPoint(); + await client.close(); + }); + // Clear the cache. // Create request and refresh callbacks that return valid credentials that will not expire soon. // Perform a find operation that succeeds. @@ -1030,9 +1069,51 @@ describe('MONGODB-OIDC', function () { // // Perform a find operation that succeeds. // Close the client. + it('successfully authenticates', async function () { + const result = await client.db('test').collection('test').findOne(); + expect(result).to.be.null; + }); }); describe('6.3 Retries and Fails with no Cache', function () { + const requestCallback = createRequestCallback('test_user1', 600); + const refreshCallback = createRefreshCallback('test_user1', 600); + const authMechanismProperties = { + REQUEST_TOKEN_CALLBACK: requestCallback, + REFRESH_TOKEN_CALLBACK: refreshCallback + }; + // Sets up the fail point for the find to reauthenticate. + const setupFailPoint = async () => { + return await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 1 + }, + data: { + failCommands: ['find', 'saslStart'], + errorCode: 391 + } + }); + }; + + before(async function () { + cache.clear(); + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: authMechanismProperties + }); + await client.db('test').collection('test').findOne(); + cache.clear(); + await setupFailPoint(); + }); + + afterEach(async function () { + await removeFailPoint(); + await client.close(); + }); + // Clear the cache. // Create request and refresh callbacks that return valid credentials that will not expire soon. // Perform a find operation that succeeds (to force a speculative auth). @@ -1054,6 +1135,9 @@ describe('MONGODB-OIDC', function () { // // Perform a find operation that fails. // Close the client. + it('fails authentication', async function () { + await client.db('test').collection('test').findOne(); + }); }); }); }); From b5b53e4cc0f9b7833c088c3557c7dbd216bc096b Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 27 Apr 2023 14:29:18 +0200 Subject: [PATCH 58/93] test: update tests --- test/manual/mongodb_oidc.prose.test.ts | 49 +++++++++++++------------- test/unit/utils.test.ts | 8 +++++ 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 74fe52d8f95..dd5abeacc14 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -1,4 +1,3 @@ -import { readFile } from 'node:fs/promises'; import * as path from 'node:path'; import { expect } from 'chai'; @@ -229,12 +228,12 @@ describe('MONGODB-OIDC', function () { }); it('fails validation', async function () { - try { - await collection.findOne(); - } catch (error) { - expect(error).to.be.instanceOf(MongoInvalidArgumentError); - expect(error.message).to.include('Host does not match provided ALLOWED_HOSTS values'); - } + //try { + await collection.findOne(); + //} catch (error) { + // expect(error).to.be.instanceOf(MongoInvalidArgumentError); + // expect(error.message).to.include('Host does not match provided ALLOWED_HOSTS values'); + //} }); }); @@ -243,26 +242,28 @@ describe('MONGODB-OIDC', function () { // Assert that a ``find`` operation fails with a client-side error. // Close the client. context('when ALLOWED_HOSTS does not match', function () { - before(function () { - client = new MongoClient( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&ignored=example.com', - { - authMechanismProperties: { - ALLOWED_HOSTS: ['examle.com'], - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 600) - } - } - ); - collection = client.db('test').collection('test'); + beforeEach(function () { + this.currentTest.skipReason = 'Will fail URI parsing as ignored is not a valid option'; + this.skip(); + // client = new MongoClient( + // 'mongodb://localhost/?authMechanism=MONGODB-OIDC&ignored=example.com', + // { + // authMechanismProperties: { + // ALLOWED_HOSTS: ['example.com'], + // REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 600) + // } + // } + // ); + // collection = client.db('test').collection('test'); }); it('fails validation', async function () { - try { - await collection.findOne(); - } catch (error) { - expect(error).to.be.instanceOf(MongoInvalidArgumentError); - expect(error.message).to.include('Host does not match provided ALLOWED_HOSTS values'); - } + // try { + // await collection.findOne(); + // } catch (error) { + // expect(error).to.be.instanceOf(MongoInvalidArgumentError); + // expect(error.message).to.include('Host does not match provided ALLOWED_HOSTS values'); + // } }); }); diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts index 59af7520934..18ca4271da6 100644 --- a/test/unit/utils.test.ts +++ b/test/unit/utils.test.ts @@ -100,6 +100,14 @@ describe('driver utils', function () { }); }); + context('when the wildcard does not start with *.', function () { + it('returns false', function () { + expect( + hostMatchesWildcards('evilmongodb.com:27017', ['*mongodb.com', 'test2']) + ).to.be.false; + }); + }); + context('when the host matches a FQDN', function () { it('returns true', function () { expect( From f35aa168b089754fe0eeeee55fc7150005153c4d Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 27 Apr 2023 15:34:34 +0200 Subject: [PATCH 59/93] test: fix readfile --- test/manual/mongodb_oidc.prose.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index dd5abeacc14..7e5c4802290 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -1,3 +1,4 @@ +import { readFile } from 'node:fs'; import * as path from 'node:path'; import { expect } from 'chai'; From 393799ff9a1e852fb89573660aff4821d2d52ed3 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 27 Apr 2023 16:08:22 +0200 Subject: [PATCH 60/93] chore: debug --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 5 +++++ test/manual/mongodb_oidc.prose.test.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 6dda3b36e44..ab2063875a3 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -79,7 +79,9 @@ export class CallbackWorkflow implements Workflow { // Reauthentication must go through all the steps again regards of a cache entry // being present. if (entry) { + console.log('FOUND ENTRY'); if (entry.isValid() && !reauthenticating) { + console.log('ENTRY VALID, NOT REAUTH'); // Presence of a valid cache entry means we can skip to the finishing step. result = await this.finishAuthentication( connection, @@ -88,6 +90,7 @@ export class CallbackWorkflow implements Workflow { response?.speculativeAuthenticate?.conversationId ); } else { + console.log('NEED FETCH TOKEN', reauthenticating, response); // Presence of an expired cache entry means we must fetch a new one and // then execute the final step. const tokenResult = await this.fetchAccessToken( @@ -106,6 +109,7 @@ export class CallbackWorkflow implements Workflow { response?.speculativeAuthenticate?.conversationId ); } catch (error) { + console.log('ERROR', error, reauthenticating); // If we are reauthenticating and this errors with reauthentication // required, we need to do the entire process over again and clear // the cache entry. @@ -127,6 +131,7 @@ export class CallbackWorkflow implements Workflow { } } } else { + console.log('NO TOKEN'); // No entry in the cache requires us to do all authentication steps // from start to finish, including getting a fresh token for the cache. const startDocument = await this.startAuthentication( diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 7e5c4802290..295bc5416fe 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -1,4 +1,4 @@ -import { readFile } from 'node:fs'; +import { readFile } from 'node:fs/promises'; import * as path from 'node:path'; import { expect } from 'chai'; From 0f305a2958759f6b2bb21b91d5f619dc50fd25b2 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 27 Apr 2023 16:43:11 +0200 Subject: [PATCH 61/93] chore: dont send conversation id during reauth --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 2 +- test/manual/mongodb_oidc.prose.test.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index ab2063875a3..53e524a7de3 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -106,7 +106,7 @@ export class CallbackWorkflow implements Workflow { connection, credentials, tokenResult, - response?.speculativeAuthenticate?.conversationId + reauthenticating ? undefined : response?.speculativeAuthenticate?.conversationId ); } catch (error) { console.log('ERROR', error, reauthenticating); diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 295bc5416fe..f3dcaf97106 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -229,12 +229,12 @@ describe('MONGODB-OIDC', function () { }); it('fails validation', async function () { - //try { - await collection.findOne(); - //} catch (error) { - // expect(error).to.be.instanceOf(MongoInvalidArgumentError); - // expect(error.message).to.include('Host does not match provided ALLOWED_HOSTS values'); - //} + try { + await collection.findOne(); + } catch (error) { + expect(error).to.be.instanceOf(MongoInvalidArgumentError); + expect(error.message).to.include('Host does not match provided ALLOWED_HOSTS values'); + } }); }); From fcfd9b6ba7d30ed060c7e5e6d2ba3c65b3c5c46a Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 27 Apr 2023 17:00:27 +0200 Subject: [PATCH 62/93] chore: lint --- .../auth/mongodb_oidc/callback_workflow.ts | 7 +------ .../auth/mongodb_oidc/token_entry_cache.ts | 20 ++++++++----------- src/index.ts | 8 ++++---- 3 files changed, 13 insertions(+), 22 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 53e524a7de3..db160567f54 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -79,9 +79,7 @@ export class CallbackWorkflow implements Workflow { // Reauthentication must go through all the steps again regards of a cache entry // being present. if (entry) { - console.log('FOUND ENTRY'); if (entry.isValid() && !reauthenticating) { - console.log('ENTRY VALID, NOT REAUTH'); // Presence of a valid cache entry means we can skip to the finishing step. result = await this.finishAuthentication( connection, @@ -90,13 +88,12 @@ export class CallbackWorkflow implements Workflow { response?.speculativeAuthenticate?.conversationId ); } else { - console.log('NEED FETCH TOKEN', reauthenticating, response); // Presence of an expired cache entry means we must fetch a new one and // then execute the final step. const tokenResult = await this.fetchAccessToken( connection, credentials, - entry.serverResult, + entry.serverInfo, reauthenticating, requestCallback, refreshCallback @@ -109,7 +106,6 @@ export class CallbackWorkflow implements Workflow { reauthenticating ? undefined : response?.speculativeAuthenticate?.conversationId ); } catch (error) { - console.log('ERROR', error, reauthenticating); // If we are reauthenticating and this errors with reauthentication // required, we need to do the entire process over again and clear // the cache entry. @@ -131,7 +127,6 @@ export class CallbackWorkflow implements Workflow { } } } else { - console.log('NO TOKEN'); // No entry in the cache requires us to do all authentication steps // from start to finish, including getting a fresh token for the cache. const startDocument = await this.startAuthentication( diff --git a/src/cmap/auth/mongodb_oidc/token_entry_cache.ts b/src/cmap/auth/mongodb_oidc/token_entry_cache.ts index 1dd3f4d6225..8e58ba1b8a5 100644 --- a/src/cmap/auth/mongodb_oidc/token_entry_cache.ts +++ b/src/cmap/auth/mongodb_oidc/token_entry_cache.ts @@ -1,8 +1,8 @@ import type { - PrincipalStepRequest, + IdPServerInfo, + IdPServerResponse, OIDCRefreshFunction, - OIDCRequestFunction, - IdPServerResponse + OIDCRequestFunction } from '../mongodb_oidc'; /* 5 minutes in milliseonds */ @@ -23,19 +23,15 @@ FN_HASHES.set(NO_FUNCTION, FN_HASH_COUNTER); /** @internal */ export class TokenEntry { tokenResult: IdPServerResponse; - serverResult: PrincipalStepRequest; + serverInfo: IdPServerInfo; expiration: number; /** * Instantiate the entry. */ - constructor( - tokenResult: IdPServerResponse, - serverResult: PrincipalStepRequest, - expiration: number - ) { + constructor(tokenResult: IdPServerResponse, serverInfo: IdPServerInfo, expiration: number) { this.tokenResult = tokenResult; - this.serverResult = serverResult; + this.serverInfo = serverInfo; this.expiration = expiration; } @@ -68,11 +64,11 @@ export class TokenEntryCache { requestFn: OIDCRequestFunction | null, refreshFn: OIDCRefreshFunction | null, tokenResult: IdPServerResponse, - serverResult: PrincipalStepRequest + serverInfo: IdPServerInfo ): TokenEntry { const entry = new TokenEntry( tokenResult, - serverResult, + serverInfo, expirationTime(tokenResult.expiresInSeconds) ); this.entries.set(cacheKey(address, username, requestFn, refreshFn), entry); diff --git a/src/index.ts b/src/index.ts index eb23da3825e..e3b1950cfb9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -204,11 +204,11 @@ export type { MongoCredentialsOptions } from './cmap/auth/mongo_credentials'; export type { - OIDCCallbackContext as OIDCClientInfo, - PrincipalStepRequest as OIDCMechanismServerStep1, + IdPServerInfo, + IdPServerResponse, + OIDCCallbackContext, OIDCRefreshFunction, - OIDCRequestFunction, - IdPServerResponse as OIDCRequestTokenResult + OIDCRequestFunction } from './cmap/auth/mongodb_oidc'; export type { BinMsg, From 1163de6ca6d7c4402bd92e48c87b4100f3213aaa Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 27 Apr 2023 18:04:25 +0200 Subject: [PATCH 63/93] chore: debug error --- test/manual/mongodb_oidc.prose.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index f3dcaf97106..2f7edbda455 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -287,6 +287,7 @@ describe('MONGODB-OIDC', function () { try { await collection.findOne(); } catch (error) { + console.log(error); expect(error).to.be.instanceOf(MongoInvalidArgumentError); expect(error.message).to.include('Host does not match provided ALLOWED_HOSTS values'); } From d1563ae2a54575b8882776c0b62a2576d49112d0 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 27 Apr 2023 20:42:01 +0200 Subject: [PATCH 64/93] refactor: moving validation --- src/cmap/auth/mongodb_oidc.ts | 8 -- src/connection_string.ts | 1 + src/mongo_client.ts | 8 ++ src/utils.ts | 3 +- test/unit/utils.test.ts | 168 ++++++++++++---------------------- 5 files changed, 70 insertions(+), 118 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index a37d387d206..702daaecf4e 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -99,14 +99,6 @@ export class MongoDBOIDC extends AuthProvider { authContext: AuthContext ): Promise { const credentials = getCredentials(authContext); - const { connection } = authContext; - const mechanismProperties = credentials.mechanismProperties; - const allowedHosts = mechanismProperties.ALLOWED_HOSTS || DEFAULT_ALLOWED_HOSTS; - const providerName = mechanismProperties.PROVIDER_NAME; - // Cloud provider automatic auth does not validate allowed hosts. - if (!providerName && !hostMatchesWildcards(connection.address, allowedHosts)) { - throw new MongoInvalidArgumentError('Host does not match provided ALLOWED_HOSTS values'); - } const workflow = getWorkflow(credentials); const result = await workflow.speculativeAuth(credentials); return { ...handshakeDoc, ...result }; diff --git a/src/connection_string.ts b/src/connection_string.ts index ac2f2d25f66..0e675de2233 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -32,6 +32,7 @@ import { emitWarning, emitWarningOnce, HostAddress, + hostMatchesWildcards, isRecord, matchesParentDomain, parseInteger, diff --git a/src/mongo_client.ts b/src/mongo_client.ts index d7779aeb474..4632fa653c1 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -447,6 +447,14 @@ export class MongoClient extends TypedEventEmitter { options.hosts[index] = host; } } + console.log('OPTIONS', options); + for (const host of options.hosts) { + // Validation of allowed hosts is required by the spec to happen immediately + // after SRV record resolution. + console.log('HOST', host.host); + //if (!hostMatchesWildcards(hostAddress.host, allowedHosts)) { + //} + } const topology = new Topology(options.hosts, options); // Events can be emitted before initialization is complete so we have to diff --git a/src/utils.ts b/src/utils.ts index c5792ae1d91..a989180cdfd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -62,8 +62,7 @@ export const ByteUtils = { * Determines if a connection's address matches a user provided list * of domain wildcards. */ -export function hostMatchesWildcards(address: string, wildcards: string[]): boolean { - const host = HostAddress.fromString(address).host; +export function hostMatchesWildcards(host: string, wildcards: string[]): boolean { for (const wildcard of wildcards) { if ( host === wildcard || diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts index 18ca4271da6..6dc632b2728 100644 --- a/test/unit/utils.test.ts +++ b/test/unit/utils.test.ts @@ -22,139 +22,91 @@ describe('driver utils', function () { describe('.hostMatchesWildcards', function () { context('when using domains', function () { context('when using exact match', function () { - context('when the address contains a port', function () { - context('when the host matches at least one', function () { - it('returns true', function () { - expect(hostMatchesWildcards('localhost:27017', ['localhost', 'other'])).to.be.true; - }); - }); - - context('when the host does not match any', function () { - it('returns false', function () { - expect(hostMatchesWildcards('localhost:27017', ['test1', 'test2'])).to.be.false; - }); - }); - - context('when the host matches a FQDN', function () { - it('returns true', function () { - expect( - hostMatchesWildcards('mongodb.net:27017', ['mongodb.net', 'other']) - ).to.be.true; - }); + context('when the host matches at least one', function () { + it('returns true', function () { + expect(hostMatchesWildcards('localhost', ['localhost', 'other'])).to.be.true; }); + }); - context('when the host does not match a FQDN', function () { - it('returns false', function () { - expect( - hostMatchesWildcards('mongodb.net:27017', ['mongodb.com', 'other']) - ).to.be.false; - }); + context('when the host does not match any', function () { + it('returns false', function () { + expect(hostMatchesWildcards('localhost', ['test1', 'test2'])).to.be.false; }); + }); - context('when the host matches a FQDN with subdomain', function () { - it('returns true', function () { - expect( - hostMatchesWildcards('prod.mongodb.net:27017', ['prod.mongodb.net', 'other']) - ).to.be.true; - }); + context('when the host matches a FQDN', function () { + it('returns true', function () { + expect(hostMatchesWildcards('mongodb.net', ['mongodb.net', 'other'])).to.be.true; }); + }); - context('when the host does not match a FQDN with subdomain', function () { - it('returns false', function () { - expect( - hostMatchesWildcards('prod.mongodb.net:27017', [ - 'dev.mongodb.net', - 'prod.mongodb.com' - ]) - ).to.be.false; - }); + context('when the host does not match a FQDN', function () { + it('returns false', function () { + expect(hostMatchesWildcards('mongodb.net', ['mongodb.com', 'other'])).to.be.false; }); }); - context('when the address does not contain a port', function () { - context('when the host matches at least one', function () { - it('returns true', function () { - expect(hostMatchesWildcards('localhost', ['localhost', 'other'])).to.be.true; - }); + context('when the host matches a FQDN with subdomain', function () { + it('returns true', function () { + expect( + hostMatchesWildcards('prod.mongodb.net', ['prod.mongodb.net', 'other']) + ).to.be.true; }); + }); - context('when the host does not match any', function () { - it('returns false', function () { - expect(hostMatchesWildcards('localhost', ['test1', 'test2'])).to.be.false; - }); + context('when the host does not match a FQDN with subdomain', function () { + it('returns false', function () { + expect( + hostMatchesWildcards('prod.mongodb.net', ['dev.mongodb.net', 'prod.mongodb.com']) + ).to.be.false; }); }); }); context('when using a leading * with domains', function () { - context('when the address contains a port', function () { - context('when the host matches at least one', function () { - it('returns true', function () { - expect(hostMatchesWildcards('localhost:27017', ['*.localhost', 'other'])).to.be.true; - }); - }); - - context('when the host does not match any', function () { - it('returns false', function () { - expect(hostMatchesWildcards('localhost:27017', ['*.test1', 'test2'])).to.be.false; - }); - }); - - context('when the wildcard does not start with *.', function () { - it('returns false', function () { - expect( - hostMatchesWildcards('evilmongodb.com:27017', ['*mongodb.com', 'test2']) - ).to.be.false; - }); + context('when the host matches at least one', function () { + it('returns true', function () { + expect(hostMatchesWildcards('localhost', ['*.localhost', 'other'])).to.be.true; }); + }); - context('when the host matches a FQDN', function () { - it('returns true', function () { - expect( - hostMatchesWildcards('mongodb.net:27017', ['*.mongodb.net', 'other']) - ).to.be.true; - }); + context('when the host does not match any', function () { + it('returns false', function () { + expect(hostMatchesWildcards('localhost', ['*.test1', 'test2'])).to.be.false; }); + }); - context('when the host does not match a FQDN', function () { - it('returns false', function () { - expect( - hostMatchesWildcards('mongodb.net:27017', ['*.mongodb.com', 'other']) - ).to.be.false; - }); + context('when the wildcard does not start with *.', function () { + it('returns false', function () { + expect(hostMatchesWildcards('evilmongodb.com', ['*mongodb.com', 'test2'])).to.be.false; }); + }); - context('when the host matches a FQDN with subdomain', function () { - it('returns true', function () { - expect( - hostMatchesWildcards('prod.mongodb.net:27017', ['*.prod.mongodb.net', 'other']) - ).to.be.true; - }); + context('when the host matches a FQDN', function () { + it('returns true', function () { + expect(hostMatchesWildcards('mongodb.net', ['*.mongodb.net', 'other'])).to.be.true; }); + }); - context('when the host does not match a FQDN with subdomain', function () { - it('returns false', function () { - expect( - hostMatchesWildcards('prod.mongodb.net:27017', [ - '*.dev.mongodb.net', - '*.prod.mongodb.com' - ]) - ).to.be.false; - }); + context('when the host does not match a FQDN', function () { + it('returns false', function () { + expect(hostMatchesWildcards('mongodb.net', ['*.mongodb.com', 'other'])).to.be.false; }); }); - context('when the address does not contain a port', function () { - context('when the host matches at least one', function () { - it('returns true', function () { - expect(hostMatchesWildcards('localhost', ['*.localhost', 'other'])).to.be.true; - }); + context('when the host matches a FQDN with subdomain', function () { + it('returns true', function () { + expect( + hostMatchesWildcards('prod.mongodb.net', ['*.prod.mongodb.net', 'other']) + ).to.be.true; }); + }); - context('when the host does not match any', function () { - it('returns false', function () { - expect(hostMatchesWildcards('localhost', ['*.test1', 'test2'])).to.be.false; - }); + context('when the host does not match a FQDN with subdomain', function () { + it('returns false', function () { + expect( + hostMatchesWildcards('prod.mongodb.net', ['*.dev.mongodb.net', '*.prod.mongodb.com']) + ).to.be.false; }); }); }); @@ -164,13 +116,13 @@ describe('driver utils', function () { context('when using IPv4', function () { context('when the host matches at least one', function () { it('returns true', function () { - expect(hostMatchesWildcards('127.0.0.1:27017', ['127.0.0.1', 'other'])).to.be.true; + expect(hostMatchesWildcards('127.0.0.1', ['127.0.0.1', 'other'])).to.be.true; }); }); context('when the host does not match any', function () { it('returns false', function () { - expect(hostMatchesWildcards('127.0.0.1:27017', ['127.0.0.2', 'test2'])).to.be.false; + expect(hostMatchesWildcards('127.0.0.1', ['127.0.0.2', 'test2'])).to.be.false; }); }); }); @@ -178,13 +130,13 @@ describe('driver utils', function () { context('when using IPv6', function () { context('when the host matches at least one', function () { it('returns true', function () { - expect(hostMatchesWildcards('[::1]:27017', ['::1', 'other'])).to.be.true; + expect(hostMatchesWildcards('[::1]', ['::1', 'other'])).to.be.true; }); }); context('when the host does not match any', function () { it('returns false', function () { - expect(hostMatchesWildcards('[::1]:27017', ['::2', 'test2'])).to.be.false; + expect(hostMatchesWildcards('[::1]', ['::2', 'test2'])).to.be.false; }); }); }); From 95d90648303a630b733ee0db4be28f14dd76a64f Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 27 Apr 2023 21:12:18 +0200 Subject: [PATCH 65/93] refactor: move validation into client --- src/mongo_client.ts | 32 ++++++++++++++++++-------- test/manual/mongodb_oidc.prose.test.ts | 8 +++++-- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 4632fa653c1..ded980285fd 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -1,11 +1,11 @@ import type { TcpNetConnectOpts } from 'net'; -import type { ConnectionOptions as TLSConnectionOptions, TLSSocketOptions } from 'tls'; +import { DEFAULT_ECDH_CURVE, ConnectionOptions as TLSConnectionOptions, TLSSocketOptions } from 'tls'; import { promisify } from 'util'; import { BSONSerializeOptions, Document, resolveBSONOptions } from './bson'; import { ChangeStream, ChangeStreamDocument, ChangeStreamOptions } from './change_stream'; -import type { AuthMechanismProperties, MongoCredentials } from './cmap/auth/mongo_credentials'; -import type { AuthMechanism } from './cmap/auth/providers'; +import { AuthMechanismProperties, DEFAULT_ALLOWED_HOSTS, MongoCredentials } from './cmap/auth/mongo_credentials'; +import { AuthMechanism } from './cmap/auth/providers'; import type { LEGAL_TCP_SOCKET_OPTIONS, LEGAL_TLS_SOCKET_OPTIONS } from './cmap/connect'; import type { Connection } from './cmap/connection'; import type { ClientMetadata } from './cmap/handshake/client_metadata'; @@ -25,7 +25,7 @@ import { readPreferenceServerSelector } from './sdam/server_selection'; import type { SrvPoller } from './sdam/srv_polling'; import { Topology, TopologyEvents } from './sdam/topology'; import { ClientSession, ClientSessionOptions, ServerSessionPool } from './sessions'; -import { HostAddress, MongoDBNamespace, ns, resolveOptions } from './utils'; +import { HostAddress, MongoDBNamespace, hostMatchesWildcards, ns, resolveOptions } from './utils'; import type { W, WriteConcern, WriteConcernSettings } from './write_concern'; /** @public */ @@ -448,12 +448,24 @@ export class MongoClient extends TypedEventEmitter { } } console.log('OPTIONS', options); - for (const host of options.hosts) { - // Validation of allowed hosts is required by the spec to happen immediately - // after SRV record resolution. - console.log('HOST', host.host); - //if (!hostMatchesWildcards(hostAddress.host, allowedHosts)) { - //} + if (options.credentials?.mechanism === AuthMechanism.MONGODB_OIDC) { + const allowedHosts = + options.credentials?.mechanismProperties?.ALLOWED_HOSTS || DEFAULT_ALLOWED_HOSTS; + const isServiceAuth = !!options.credentials?.mechanismProperties?.PROVIDER_NAME; + if (!isServiceAuth) { + for (const host of options.hosts) { + // Validation of allowed hosts is required by the spec to happen immediately + // after SRV record resolution. + console.log('HOST', host.host); + if (!hostMatchesWildcards(host.host || 'localhost', allowedHosts)) { + throw new MongoInvalidArgumentError( + `Host '${host}' is not valid for OIDC authentication with ALLOWED_HOSTS of '${allowedHosts.join( + ',' + )}'` + ); + } + } + } } const topology = new Topology(options.hosts, options); diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 2f7edbda455..974fce1e54c 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -233,7 +233,9 @@ describe('MONGODB-OIDC', function () { await collection.findOne(); } catch (error) { expect(error).to.be.instanceOf(MongoInvalidArgumentError); - expect(error.message).to.include('Host does not match provided ALLOWED_HOSTS values'); + expect(error.message).to.include( + 'is not valid for OIDC authentication with ALLOWED_HOSTS' + ); } }); }); @@ -289,7 +291,9 @@ describe('MONGODB-OIDC', function () { } catch (error) { console.log(error); expect(error).to.be.instanceOf(MongoInvalidArgumentError); - expect(error.message).to.include('Host does not match provided ALLOWED_HOSTS values'); + expect(error.message).to.include( + 'is not valid for OIDC authentication with ALLOWED_HOSTS' + ); } }); }); From 82f3e636587da5638cfec1e49ecb0db9030b8523 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 27 Apr 2023 21:20:43 +0200 Subject: [PATCH 66/93] chore: remove console debug --- src/mongo_client.ts | 13 +++++++++---- test/manual/mongodb_oidc.prose.test.ts | 1 - 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/mongo_client.ts b/src/mongo_client.ts index ded980285fd..99ffafe1849 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -447,16 +447,21 @@ export class MongoClient extends TypedEventEmitter { options.hosts[index] = host; } } - console.log('OPTIONS', options); + + // This logic is here rather than validating in the connection_string module + // or elsewhere for 2 reasons. The OIDC auth spec states that the hostname + // validation against the user provided ALLOWED_HOSTS entries or the + // default values MUST happen after SRV record resolution. At the same time, + // however, we don't even want an attempted connection via a SDAM monitor + // to even attempt to contact a host that is not allowed. So this is placed + // here directly after SRV resolution and before the creation of the + // topology to short circuit as quickly as possible. if (options.credentials?.mechanism === AuthMechanism.MONGODB_OIDC) { const allowedHosts = options.credentials?.mechanismProperties?.ALLOWED_HOSTS || DEFAULT_ALLOWED_HOSTS; const isServiceAuth = !!options.credentials?.mechanismProperties?.PROVIDER_NAME; if (!isServiceAuth) { for (const host of options.hosts) { - // Validation of allowed hosts is required by the spec to happen immediately - // after SRV record resolution. - console.log('HOST', host.host); if (!hostMatchesWildcards(host.host || 'localhost', allowedHosts)) { throw new MongoInvalidArgumentError( `Host '${host}' is not valid for OIDC authentication with ALLOWED_HOSTS of '${allowedHosts.join( diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 974fce1e54c..3c467c10af2 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -289,7 +289,6 @@ describe('MONGODB-OIDC', function () { try { await collection.findOne(); } catch (error) { - console.log(error); expect(error).to.be.instanceOf(MongoInvalidArgumentError); expect(error.message).to.include( 'is not valid for OIDC authentication with ALLOWED_HOSTS' From 08fc2efdfc6e9ff54fd8c22831fd5f0bde5740c9 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 27 Apr 2023 21:33:07 +0200 Subject: [PATCH 67/93] test: fail reauth twice --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 1 + test/manual/mongodb_oidc.prose.test.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index db160567f54..1a5588443d7 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -127,6 +127,7 @@ export class CallbackWorkflow implements Workflow { } } } else { + console.log('NO ENTRY IN THE CACHE', reauthenticating); // No entry in the cache requires us to do all authentication steps // from start to finish, including getting a fresh token for the cache. const startDocument = await this.startAuthentication( diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 3c467c10af2..4ffe4c6772b 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -1096,7 +1096,7 @@ describe('MONGODB-OIDC', function () { .command({ configureFailPoint: 'failCommand', mode: { - times: 1 + times: 2 }, data: { failCommands: ['find', 'saslStart'], From 699e5a1311418036736b0c94c0860db3657ffb9c Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 27 Apr 2023 21:48:00 +0200 Subject: [PATCH 68/93] test: updating test assertions --- test/manual/mongodb_oidc.prose.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 4ffe4c6772b..8659d4732aa 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -782,6 +782,7 @@ describe('MONGODB-OIDC', function () { expect(cache.entries.size).to.equal(1); try { await collection.findOne(); + expect.fail('Expected OIDC auth to fail with invalid fields from refresh callback'); } catch (e) { expect(cache.entries.size).to.equal(0); } @@ -1142,7 +1143,13 @@ describe('MONGODB-OIDC', function () { // Perform a find operation that fails. // Close the client. it('fails authentication', async function () { - await client.db('test').collection('test').findOne(); + try { + await client.db('test').collection('test').findOne(); + expect.fail('Reauthentication must fail on the saslStart error'); + } catch (error) { + // This is the saslStart failCommand bubbled up. + expect(error).to.be.instanceOf(MongoServerError); + } }); }); }); From e969628e0c90d38bbcd7c8a953bafd6da95b78cf Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 27 Apr 2023 21:55:51 +0200 Subject: [PATCH 69/93] fix: lint --- src/cmap/auth/mongodb_oidc.ts | 3 +-- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 5 ++--- src/connection_string.ts | 1 - src/mongo_client.ts | 10 +++++++--- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index 702daaecf4e..ae9b5cfe439 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -1,8 +1,7 @@ import { MongoInvalidArgumentError, MongoMissingCredentialsError } from '../../error'; -import { hostMatchesWildcards } from '../../utils'; import type { HandshakeDocument } from '../connect'; import { type AuthContext, AuthProvider } from './auth_provider'; -import { DEFAULT_ALLOWED_HOSTS, MongoCredentials } from './mongo_credentials'; +import type { MongoCredentials } from './mongo_credentials'; import { AwsServiceWorkflow } from './mongodb_oidc/aws_service_workflow'; import { CallbackWorkflow } from './mongodb_oidc/callback_workflow'; import type { Workflow } from './mongodb_oidc/workflow'; diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 1a5588443d7..55808296bb3 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -76,9 +76,9 @@ export class CallbackWorkflow implements Workflow { refreshCallback || null ); let result; - // Reauthentication must go through all the steps again regards of a cache entry - // being present. if (entry) { + // Reauthentication cannot use a token from the cache since the server has + // stated it is invalid by the request for reauthentication. if (entry.isValid() && !reauthenticating) { // Presence of a valid cache entry means we can skip to the finishing step. result = await this.finishAuthentication( @@ -127,7 +127,6 @@ export class CallbackWorkflow implements Workflow { } } } else { - console.log('NO ENTRY IN THE CACHE', reauthenticating); // No entry in the cache requires us to do all authentication steps // from start to finish, including getting a fresh token for the cache. const startDocument = await this.startAuthentication( diff --git a/src/connection_string.ts b/src/connection_string.ts index 0e675de2233..ac2f2d25f66 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -32,7 +32,6 @@ import { emitWarning, emitWarningOnce, HostAddress, - hostMatchesWildcards, isRecord, matchesParentDomain, parseInteger, diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 99ffafe1849..3edfcd1cf0f 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -1,10 +1,14 @@ import type { TcpNetConnectOpts } from 'net'; -import { DEFAULT_ECDH_CURVE, ConnectionOptions as TLSConnectionOptions, TLSSocketOptions } from 'tls'; +import type { ConnectionOptions as TLSConnectionOptions, TLSSocketOptions } from 'tls'; import { promisify } from 'util'; import { BSONSerializeOptions, Document, resolveBSONOptions } from './bson'; import { ChangeStream, ChangeStreamDocument, ChangeStreamOptions } from './change_stream'; -import { AuthMechanismProperties, DEFAULT_ALLOWED_HOSTS, MongoCredentials } from './cmap/auth/mongo_credentials'; +import { + AuthMechanismProperties, + DEFAULT_ALLOWED_HOSTS, + MongoCredentials +} from './cmap/auth/mongo_credentials'; import { AuthMechanism } from './cmap/auth/providers'; import type { LEGAL_TCP_SOCKET_OPTIONS, LEGAL_TLS_SOCKET_OPTIONS } from './cmap/connect'; import type { Connection } from './cmap/connection'; @@ -25,7 +29,7 @@ import { readPreferenceServerSelector } from './sdam/server_selection'; import type { SrvPoller } from './sdam/srv_polling'; import { Topology, TopologyEvents } from './sdam/topology'; import { ClientSession, ClientSessionOptions, ServerSessionPool } from './sessions'; -import { HostAddress, MongoDBNamespace, hostMatchesWildcards, ns, resolveOptions } from './utils'; +import { HostAddress, hostMatchesWildcards, MongoDBNamespace, ns, resolveOptions } from './utils'; import type { W, WriteConcern, WriteConcernSettings } from './write_concern'; /** @public */ From 90b052239540945f8e8c4229757af10c94646963 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 27 Apr 2023 22:03:21 +0200 Subject: [PATCH 70/93] fix: invalidate token in cache on error --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 55808296bb3..365edc9ed5e 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -236,8 +236,15 @@ export class CallbackWorkflow implements Workflow { // With no token in the cache we use the request callback. result = await requestCallback(serverInfo, context); } - // Validate that the result returned by the callback is acceptable. + // Validate that the result returned by the callback is acceptable. If it is not + // we must clear the token result from the cache. if (isCallbackResultInvalid(result)) { + this.cache.deleteEntry( + connection.address, + credentials.username || '', + requestCallback, + refreshCallback || null + ); throw new MongoMissingCredentialsError( 'User provided OIDC callbacks must return a valid object with an accessToken.' ); From 916aca230cb8635500aa280454d971c41bd2d417 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 27 Apr 2023 22:21:17 +0200 Subject: [PATCH 71/93] chore: cleanup --- src/cmap/auth/mongodb_oidc.ts | 5 ++++- .../auth/mongodb_oidc/aws_service_workflow.ts | 5 ++++- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 17 +++++++++++------ test/manual/mongodb_oidc.prose.test.ts | 4 +++- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index ae9b5cfe439..06065947495 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -12,6 +12,9 @@ import type { Workflow } from './mongodb_oidc/workflow'; */ export const OIDC_VERSION = 0; +/** Error when credentials are missing. */ +const MISSING_CREDENTIALS_ERROR = 'AuthContext must provide credentials.'; + /** * @public * @experimental @@ -110,7 +113,7 @@ export class MongoDBOIDC extends AuthProvider { function getCredentials(authContext: AuthContext): MongoCredentials { const { credentials } = authContext; if (!credentials) { - throw new MongoMissingCredentialsError('AuthContext must provide credentials.'); + throw new MongoMissingCredentialsError(MISSING_CREDENTIALS_ERROR); } return credentials; } diff --git a/src/cmap/auth/mongodb_oidc/aws_service_workflow.ts b/src/cmap/auth/mongodb_oidc/aws_service_workflow.ts index a6563cc5206..9f2fc58a88e 100644 --- a/src/cmap/auth/mongodb_oidc/aws_service_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/aws_service_workflow.ts @@ -3,6 +3,9 @@ import { readFile } from 'fs/promises'; import { MongoAWSError } from '../../../error'; import { ServiceWorkflow } from './service_workflow'; +/** Error for when the token is missing in the environment. */ +const TOKEN_MISSING_ERROR = 'AWS_WEB_IDENTITY_TOKEN_FILE must be set in the environment.'; + /** * Device workflow implementation for AWS. * @@ -19,7 +22,7 @@ export class AwsServiceWorkflow extends ServiceWorkflow { async getToken(): Promise { const tokenFile = process.env.AWS_WEB_IDENTITY_TOKEN_FILE; if (!tokenFile) { - throw new MongoAWSError('AWS_WEB_IDENTITY_TOKEN_FILE must be set in the environment.'); + throw new MongoAWSError(TOKEN_MISSING_ERROR); } return readFile(tokenFile, 'utf8'); } diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 365edc9ed5e..28505bc7eb2 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -27,6 +27,14 @@ const TIMEOUT_S = 300; /** Properties allowed on results of callbacks. */ const RESULT_PROPERTIES = ['accessToken', 'expiresInSeconds', 'refreshToken']; +/** Error message when the callback result is invalid. */ +const CALLBACK_RESULT_ERROR = + 'User provided OIDC callbacks must return a valid object with an accessToken.'; + +/** Error message for when request callback is missing. */ +const REQUEST_CALLBACK_REQUIRED_ERROR = + 'Auth mechanism property REQUEST_TOKEN_CALLBACK is required.'; + /** * OIDC implementation of a callback based workflow. * @internal @@ -64,9 +72,7 @@ export class CallbackWorkflow implements Workflow { const refreshCallback = credentials.mechanismProperties.REFRESH_TOKEN_CALLBACK; // At minimum a request callback must be provided by the user. if (!requestCallback) { - throw new MongoInvalidArgumentError( - 'Auth mechanism property REQUEST_TOKEN_CALLBACK is required.' - ); + throw new MongoInvalidArgumentError(REQUEST_CALLBACK_REQUIRED_ERROR); } // Look for an existing entry in the cache. const entry = this.cache.getEntry( @@ -239,15 +245,14 @@ export class CallbackWorkflow implements Workflow { // Validate that the result returned by the callback is acceptable. If it is not // we must clear the token result from the cache. if (isCallbackResultInvalid(result)) { + console.log('GOT ERROR, DELETE FROM CACHE AND THROW'); this.cache.deleteEntry( connection.address, credentials.username || '', requestCallback, refreshCallback || null ); - throw new MongoMissingCredentialsError( - 'User provided OIDC callbacks must return a valid object with an accessToken.' - ); + throw new MongoMissingCredentialsError(CALLBACK_RESULT_ERROR); } // Cleanup the cache. this.cache.deleteExpiredEntries(); diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 8659d4732aa..e9d28013517 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -783,7 +783,9 @@ describe('MONGODB-OIDC', function () { try { await collection.findOne(); expect.fail('Expected OIDC auth to fail with invalid fields from refresh callback'); - } catch (e) { + } catch (error) { + expect(error).to.be.instanceOf(MongoMissingCredentialsError); + expect(error.message).to.include(''); expect(cache.entries.size).to.equal(0); } }); From 9f3f17c7328dec9364941e706729b6011dabede3 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 27 Apr 2023 22:35:28 +0200 Subject: [PATCH 72/93] chore: debug --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 28505bc7eb2..8ebc93fe295 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -83,9 +83,11 @@ export class CallbackWorkflow implements Workflow { ); let result; if (entry) { + console.log('ENTRY IN THE CACHE'); // Reauthentication cannot use a token from the cache since the server has // stated it is invalid by the request for reauthentication. if (entry.isValid() && !reauthenticating) { + console.log('ENTRY VALID NO REAUTH'); // Presence of a valid cache entry means we can skip to the finishing step. result = await this.finishAuthentication( connection, @@ -94,6 +96,7 @@ export class CallbackWorkflow implements Workflow { response?.speculativeAuthenticate?.conversationId ); } else { + console.log('FETCH ACCESS TOKEN'); // Presence of an expired cache entry means we must fetch a new one and // then execute the final step. const tokenResult = await this.fetchAccessToken( @@ -105,6 +108,7 @@ export class CallbackWorkflow implements Workflow { refreshCallback ); try { + console.log('FINISH AUTH'); result = await this.finishAuthentication( connection, credentials, @@ -112,6 +116,7 @@ export class CallbackWorkflow implements Workflow { reauthenticating ? undefined : response?.speculativeAuthenticate?.conversationId ); } catch (error) { + console.log('ERROR ON AUTH FINISH'); // If we are reauthenticating and this errors with reauthentication // required, we need to do the entire process over again and clear // the cache entry. @@ -133,6 +138,7 @@ export class CallbackWorkflow implements Workflow { } } } else { + console.log('NO CACHE ENTRY'); // No entry in the cache requires us to do all authentication steps // from start to finish, including getting a fresh token for the cache. const startDocument = await this.startAuthentication( @@ -227,6 +233,7 @@ export class CallbackWorkflow implements Workflow { if (entry) { // If the cache entry is valid, return the token result. if (entry.isValid() && !reauthenticating) { + console.log('FETCH - ENTRY VALID NO REAUTH'); return entry.tokenResult; } // If the cache entry is not valid, remove it from the cache and first attempt @@ -254,6 +261,7 @@ export class CallbackWorkflow implements Workflow { ); throw new MongoMissingCredentialsError(CALLBACK_RESULT_ERROR); } + console.log('DELETING EXPIRED ENTRIES'); // Cleanup the cache. this.cache.deleteExpiredEntries(); // Put the new entry into the cache. From 780e4df3b2eca55a79e8374d8307ea4805d323d6 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 27 Apr 2023 22:54:20 +0200 Subject: [PATCH 73/93] test: create new client --- test/manual/mongodb_oidc.prose.test.ts | 27 +++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index e9d28013517..a2e15fd64b3 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -758,17 +758,21 @@ describe('MONGODB-OIDC', function () { }); describe('4.4 Error clears cache', function () { - before(function () { + const authMechanismProperties = { + REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 300), + REFRESH_TOKEN_CALLBACK: () => { + return Promise.resolve({}); + } + }; + + before(async function () { cache.clear(); client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 300), - REFRESH_TOKEN_CALLBACK: () => { - return Promise.resolve({}); - } - } + authMechanismProperties: authMechanismProperties }); - collection = client.db('test').collection('test'); + await client.db('test').collection('test').findOne(); + expect(cache.entries.size).to.equal(1); + await client.close(); }); // Clear the cache. @@ -778,10 +782,11 @@ describe('MONGODB-OIDC', function () { // Ensure that the cached token has been cleared. // Close the client. it('clears the cache on authentication error', async function () { - await collection.findOne(); - expect(cache.entries.size).to.equal(1); + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: authMechanismProperties + }); try { - await collection.findOne(); + await client.db('test').collection('test').findOne(); expect.fail('Expected OIDC auth to fail with invalid fields from refresh callback'); } catch (error) { expect(error).to.be.instanceOf(MongoMissingCredentialsError); From 2e6d7a30a855b528b03dbb8e34e043c9144c0dde Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 27 Apr 2023 23:02:51 +0200 Subject: [PATCH 74/93] chore: remove console logs --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 8ebc93fe295..42266f26b1c 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -83,11 +83,9 @@ export class CallbackWorkflow implements Workflow { ); let result; if (entry) { - console.log('ENTRY IN THE CACHE'); // Reauthentication cannot use a token from the cache since the server has // stated it is invalid by the request for reauthentication. if (entry.isValid() && !reauthenticating) { - console.log('ENTRY VALID NO REAUTH'); // Presence of a valid cache entry means we can skip to the finishing step. result = await this.finishAuthentication( connection, @@ -96,7 +94,6 @@ export class CallbackWorkflow implements Workflow { response?.speculativeAuthenticate?.conversationId ); } else { - console.log('FETCH ACCESS TOKEN'); // Presence of an expired cache entry means we must fetch a new one and // then execute the final step. const tokenResult = await this.fetchAccessToken( @@ -108,7 +105,6 @@ export class CallbackWorkflow implements Workflow { refreshCallback ); try { - console.log('FINISH AUTH'); result = await this.finishAuthentication( connection, credentials, @@ -116,7 +112,6 @@ export class CallbackWorkflow implements Workflow { reauthenticating ? undefined : response?.speculativeAuthenticate?.conversationId ); } catch (error) { - console.log('ERROR ON AUTH FINISH'); // If we are reauthenticating and this errors with reauthentication // required, we need to do the entire process over again and clear // the cache entry. @@ -138,7 +133,6 @@ export class CallbackWorkflow implements Workflow { } } } else { - console.log('NO CACHE ENTRY'); // No entry in the cache requires us to do all authentication steps // from start to finish, including getting a fresh token for the cache. const startDocument = await this.startAuthentication( @@ -233,7 +227,6 @@ export class CallbackWorkflow implements Workflow { if (entry) { // If the cache entry is valid, return the token result. if (entry.isValid() && !reauthenticating) { - console.log('FETCH - ENTRY VALID NO REAUTH'); return entry.tokenResult; } // If the cache entry is not valid, remove it from the cache and first attempt @@ -252,7 +245,6 @@ export class CallbackWorkflow implements Workflow { // Validate that the result returned by the callback is acceptable. If it is not // we must clear the token result from the cache. if (isCallbackResultInvalid(result)) { - console.log('GOT ERROR, DELETE FROM CACHE AND THROW'); this.cache.deleteEntry( connection.address, credentials.username || '', @@ -261,7 +253,6 @@ export class CallbackWorkflow implements Workflow { ); throw new MongoMissingCredentialsError(CALLBACK_RESULT_ERROR); } - console.log('DELETING EXPIRED ENTRIES'); // Cleanup the cache. this.cache.deleteExpiredEntries(); // Put the new entry into the cache. From e0899f630fa3c1d007169fb99627e1c99027cbb3 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 27 Apr 2023 23:25:24 +0200 Subject: [PATCH 75/93] test: fix unit tests --- test/unit/cmap/auth/mongodb_oidc/token_entry_cache.test.ts | 2 +- test/unit/utils.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit/cmap/auth/mongodb_oidc/token_entry_cache.test.ts b/test/unit/cmap/auth/mongodb_oidc/token_entry_cache.test.ts index b71ecb17b27..8507bf54555 100644 --- a/test/unit/cmap/auth/mongodb_oidc/token_entry_cache.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/token_entry_cache.test.ts @@ -35,7 +35,7 @@ describe('TokenEntryCache', function () { }); it('adds the server result', function () { - expect(entry.serverResult).to.deep.equal(serverResult); + expect(entry.serverInfo).to.deep.equal(serverResult); }); it('creates an expiration', function () { diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts index 6dc632b2728..3c580450d56 100644 --- a/test/unit/utils.test.ts +++ b/test/unit/utils.test.ts @@ -130,13 +130,13 @@ describe('driver utils', function () { context('when using IPv6', function () { context('when the host matches at least one', function () { it('returns true', function () { - expect(hostMatchesWildcards('[::1]', ['::1', 'other'])).to.be.true; + expect(hostMatchesWildcards('::1', ['::1', 'other'])).to.be.true; }); }); context('when the host does not match any', function () { it('returns false', function () { - expect(hostMatchesWildcards('[::1]', ['::2', 'test2'])).to.be.false; + expect(hostMatchesWildcards('::1', ['::2', 'test2'])).to.be.false; }); }); }); From 87345ecb0925cbd777f001c4aa2be1a42bc9eee4 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 28 Apr 2023 13:40:28 +0200 Subject: [PATCH 76/93] fix: ts import version --- src/cmap/auth/mongodb_oidc.ts | 8 +------- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 6 ++++-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index 06065947495..c06ac38100c 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -1,17 +1,11 @@ import { MongoInvalidArgumentError, MongoMissingCredentialsError } from '../../error'; import type { HandshakeDocument } from '../connect'; -import { type AuthContext, AuthProvider } from './auth_provider'; +import { AuthContext, AuthProvider } from './auth_provider'; import type { MongoCredentials } from './mongo_credentials'; import { AwsServiceWorkflow } from './mongodb_oidc/aws_service_workflow'; import { CallbackWorkflow } from './mongodb_oidc/callback_workflow'; import type { Workflow } from './mongodb_oidc/workflow'; -/** - * @internal - * The current version of OIDC implementation. - */ -export const OIDC_VERSION = 0; - /** Error when credentials are missing. */ const MISSING_CREDENTIALS_ERROR = 'AuthContext must provide credentials.'; diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 42266f26b1c..14db2e9db67 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -9,10 +9,9 @@ import { import { ns } from '../../../utils'; import type { Connection } from '../../connection'; import type { MongoCredentials } from '../mongo_credentials'; -import { +import type { IdPServerInfo, IdPServerResponse, - OIDC_VERSION, OIDCCallbackContext, OIDCRefreshFunction, OIDCRequestFunction @@ -21,6 +20,9 @@ import { AuthMechanism } from '../providers'; import { TokenEntryCache } from './token_entry_cache'; import type { Workflow } from './workflow'; +/** The current version of OIDC implementation. */ +const OIDC_VERSION = 0; + /** 5 minutes in seconds */ const TIMEOUT_S = 300; From f193239af0d681f1bd9a93fb639365364ab789df Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 28 Apr 2023 13:59:21 +0200 Subject: [PATCH 77/93] test: add case for unix sockets --- src/mongo_client.ts | 2 +- src/utils.ts | 3 ++- test/unit/utils.test.ts | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 3edfcd1cf0f..004c6d27f38 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -466,7 +466,7 @@ export class MongoClient extends TypedEventEmitter { const isServiceAuth = !!options.credentials?.mechanismProperties?.PROVIDER_NAME; if (!isServiceAuth) { for (const host of options.hosts) { - if (!hostMatchesWildcards(host.host || 'localhost', allowedHosts)) { + if (!hostMatchesWildcards(host.host || host.socketPath || 'localhost', allowedHosts)) { throw new MongoInvalidArgumentError( `Host '${host}' is not valid for OIDC authentication with ALLOWED_HOSTS of '${allowedHosts.join( ',' diff --git a/src/utils.ts b/src/utils.ts index a989180cdfd..23debd74e82 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -66,7 +66,8 @@ export function hostMatchesWildcards(host: string, wildcards: string[]): boolean for (const wildcard of wildcards) { if ( host === wildcard || - (wildcard.startsWith('*.') && host?.endsWith(wildcard.substring(2, wildcard.length))) + (wildcard.startsWith('*.') && host?.endsWith(wildcard.substring(2, wildcard.length))) || + (wildcard.startsWith('*/') && host?.endsWith(wildcard.substring(2, wildcard.length))) ) { return true; } diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts index 3c580450d56..519aef78458 100644 --- a/test/unit/utils.test.ts +++ b/test/unit/utils.test.ts @@ -141,6 +141,24 @@ describe('driver utils', function () { }); }); }); + + context('when using unix domain sockets', function () { + context('when the host matches at least one', function () { + it('returns true', function () { + expect( + hostMatchesWildcards('/tmp/mongodb-27017.sock', ['*/mongodb-27017.sock', 'other']) + ).to.be.true; + }); + }); + + context('when the host does not match any', function () { + it('returns false', function () { + expect( + hostMatchesWildcards('/tmp/mongodb-27017.sock', ['*/mongod-27017.sock', 'test2']) + ).to.be.false; + }); + }); + }); }); context('eachAsync()', function () { From ce5fcab6fefb05b99fbb8b2a2bd629a5a010f439 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 28 Apr 2023 14:35:00 +0200 Subject: [PATCH 78/93] feat: prevent simultaneous callback execution --- .../auth/mongodb_oidc/callback_workflow.ts | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 14db2e9db67..cb2f104656a 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -236,13 +236,13 @@ export class CallbackWorkflow implements Workflow { // exists, then fallback to the request callback. if (refreshCallback) { context.refreshToken = entry.tokenResult.refreshToken; - result = await refreshCallback(serverInfo, context); + result = await withLock(refreshCallback)(serverInfo, context); } else { - result = await requestCallback(serverInfo, context); + result = await withLock(requestCallback)(serverInfo, context); } } else { // With no token in the cache we use the request callback. - result = await requestCallback(serverInfo, context); + result = await withLock(requestCallback)(serverInfo, context); } // Validate that the result returned by the callback is acceptable. If it is not // we must clear the token result from the cache. @@ -263,10 +263,10 @@ export class CallbackWorkflow implements Workflow { credentials.username || '', requestCallback, refreshCallback || null, - result, + result as IdPServerResponse, serverInfo ); - return result; + return result as IdPServerResponse; } } @@ -319,3 +319,15 @@ function startCommandDocument(credentials: MongoCredentials): Document { payload: new Binary(BSON.serialize(payload)) }; } + +/** + * Ensure the callback is only executed one at a time. + */ +function withLock(callback: OIDCRequestFunction | OIDCRefreshFunction) { + let lock: Promise = Promise.resolve(); + return async (info: IdPServerInfo, context: OIDCCallbackContext) => { + await lock; + lock = lock.then(() => callback(info, context)); + return lock; + }; +} From 8bb8ae1f8b8a0a8c7a8e3a07e6d6d5ea0122b90e Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 28 Apr 2023 14:58:40 +0200 Subject: [PATCH 79/93] test: add lock prose test --- test/manual/mongodb_oidc.prose.test.ts | 61 +++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index a2e15fd64b3..57c922ebb15 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -1,5 +1,6 @@ import { readFile } from 'node:fs/promises'; import * as path from 'node:path'; +import { setTimeout } from 'node:timers/promises'; import { expect } from 'chai'; import * as sinon from 'sinon'; @@ -15,8 +16,7 @@ import { MongoMissingCredentialsError, MongoServerError, OIDC_WORKFLOWS, - OIDCCallbackContext, - OIDCRequestTokenResult + OIDCCallbackContext } from '../mongodb'; describe('MONGODB-OIDC', function () { @@ -297,6 +297,63 @@ describe('MONGODB-OIDC', function () { }); }); }); + + describe('1.7 Lock Avoids Extra Callback Calls', function () { + before(function () { + cache.clear(); + }); + + const requestCallback = async () => { + const token = await readFile(path.join(process.env.OIDC_TOKEN_DIR, 'test_user1'), { + encoding: 'utf8' + }); + await setTimeout(2000); + return generateResult(token, 300); + }; + const refreshCallback = createRefreshCallback(); + const requestSpy = sinon.spy(requestCallback); + const refreshSpy = sinon.spy(refreshCallback); + + const createClient = () => { + return new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: requestSpy, + REFRESH_TOKEN_CALLBACK: refreshSpy + } + }); + }; + + const authenticate = async () => { + const client = createClient(); + await client.db('test').collection('test').findOne(); + await client.close(); + }; + + const testPromise = async () => { + await authenticate(); + await authenticate(); + }; + + // Clear the cache. + // Create a request callback that returns a token that will expire soon, and + // a refresh callback. Ensure that the request callback has a time delay, and + // that we can record the number of times each callback is called. + // Spawn two threads that do the following: + // - Create a client with the callbacks. + // - Run a find operation that succeeds. + // - Close the client. + // - Create a new client with the callbacks. + // - Run a find operation that succeeds. + // - Close the client. + // Join the two threads. + // Ensure that the request callback has been called once, and the refresh + // callback has been called twice. + it('does not simultaneously enter a callback', async function () { + await Promise.allSettled([testPromise(), testPromise()]); + expect(requestSpy).to.have.been.calledOnce; + expect(refreshSpy).to.have.been.calledTwice; + }); + }); }); describe('2. AWS Automatic Auth', function () { From dda9dcbd4aaf4e846bcdb093c691f47fa4450ef0 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 28 Apr 2023 15:12:41 +0200 Subject: [PATCH 80/93] chore: debug --- test/manual/mongodb_oidc.prose.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 57c922ebb15..d25a3778ee8 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -304,10 +304,12 @@ describe('MONGODB-OIDC', function () { }); const requestCallback = async () => { + console.log('REFRESH ENTER'); const token = await readFile(path.join(process.env.OIDC_TOKEN_DIR, 'test_user1'), { encoding: 'utf8' }); await setTimeout(2000); + console.log('REFRESH EXIT'); return generateResult(token, 300); }; const refreshCallback = createRefreshCallback(); From 98b722546b209cceb0152347578f574906c168f6 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 28 Apr 2023 15:25:33 +0200 Subject: [PATCH 81/93] chore: lock again --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index cb2f104656a..ef01cc5aff5 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -326,8 +326,11 @@ function startCommandDocument(credentials: MongoCredentials): Document { function withLock(callback: OIDCRequestFunction | OIDCRefreshFunction) { let lock: Promise = Promise.resolve(); return async (info: IdPServerInfo, context: OIDCCallbackContext) => { - await lock; - lock = lock.then(() => callback(info, context)); - return lock; + const result = lock + .then(() => callback(info, context)) + .finally(() => { + lock = Promise.resolve(); + }); + return result; }; } From b4f20089d9da98c6fd2c5371321d9b00a6d54f55 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 28 Apr 2023 17:21:57 +0200 Subject: [PATCH 82/93] test: use lock cache --- .../auth/mongodb_oidc/callback_lock_cache.ts | 84 +++++++++++ .../auth/mongodb_oidc/callback_workflow.ts | 50 ++----- test/mongodb.ts | 1 + .../mongodb_oidc/callback_lock_cache.test.ts | 137 ++++++++++++++++++ 4 files changed, 236 insertions(+), 36 deletions(-) create mode 100644 src/cmap/auth/mongodb_oidc/callback_lock_cache.ts create mode 100644 test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts diff --git a/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts b/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts new file mode 100644 index 00000000000..ca2def39633 --- /dev/null +++ b/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts @@ -0,0 +1,84 @@ +import { MongoInvalidArgumentError } from '../../../error'; +import type { Connection } from '../../connection'; +import type { MongoCredentials } from '../mongo_credentials'; +import type { + IdPServerInfo, + IdPServerResponse, + OIDCCallbackContext, + OIDCRefreshFunction, + OIDCRequestFunction +} from '../mongodb_oidc'; + +/** Error message for when request callback is missing. */ +const REQUEST_CALLBACK_REQUIRED_ERROR = + 'Auth mechanism property REQUEST_TOKEN_CALLBACK is required.'; + +/** + * An entry of callbacks in the cache. + */ +interface CallbacksEntry { + requestCallback: OIDCRequestFunction; + refreshCallback?: OIDCRefreshFunction; +} + +/** + * A cache of request and refresh callbacks per server/user. + */ +export class CallbackLockCache { + entries: Map; + + /** + * Instantiate the new cache. + */ + constructor() { + this.entries = new Map(); + } + + /** + * Get the callbacks for the connection and credentials. If an entry does not + * exist a new one will get set. + */ + getCallbacks(connection: Connection, credentials: MongoCredentials): CallbacksEntry { + const entry = this.entries.get(cacheKey(connection, credentials)); + if (entry) { + return entry; + } + return this.setCallbacks(connection, credentials); + } + + /** + * Set locked callbacks on for connection and credentials. + */ + private setCallbacks(connection: Connection, credentials: MongoCredentials): CallbacksEntry { + const requestCallback = credentials.mechanismProperties.REQUEST_TOKEN_CALLBACK; + const refreshCallback = credentials.mechanismProperties.REFRESH_TOKEN_CALLBACK; + if (!requestCallback) { + throw new MongoInvalidArgumentError(REQUEST_CALLBACK_REQUIRED_ERROR); + } + const entry = { + requestCallback: withLock(requestCallback), + refreshCallback: refreshCallback ? withLock(refreshCallback) : undefined + }; + this.entries.set(cacheKey(connection, credentials), entry); + return entry; + } +} + +/** + * Get a cache key based on connection and credentials. + */ +function cacheKey(connection: Connection, credentials: MongoCredentials): string { + return `${connection.address}-${credentials.username}`; +} + +/** + * Ensure the callback is only executed one at a time. + */ +function withLock(callback: OIDCRequestFunction | OIDCRefreshFunction) { + let lock: Promise = Promise.resolve(); + return async (info: IdPServerInfo, context: OIDCCallbackContext): Promise => { + await lock; + lock = lock.then(() => callback(info, context)); + return lock; + }; +} diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index ef01cc5aff5..72c4b530c8e 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -1,11 +1,6 @@ import { Binary, BSON, type Document } from 'bson'; -import { - MONGODB_ERROR_CODES, - MongoError, - MongoInvalidArgumentError, - MongoMissingCredentialsError -} from '../../../error'; +import { MONGODB_ERROR_CODES, MongoError, MongoMissingCredentialsError } from '../../../error'; import { ns } from '../../../utils'; import type { Connection } from '../../connection'; import type { MongoCredentials } from '../mongo_credentials'; @@ -17,6 +12,7 @@ import type { OIDCRequestFunction } from '../mongodb_oidc'; import { AuthMechanism } from '../providers'; +import { CallbackLockCache } from './callback_lock_cache'; import { TokenEntryCache } from './token_entry_cache'; import type { Workflow } from './workflow'; @@ -33,22 +29,20 @@ const RESULT_PROPERTIES = ['accessToken', 'expiresInSeconds', 'refreshToken']; const CALLBACK_RESULT_ERROR = 'User provided OIDC callbacks must return a valid object with an accessToken.'; -/** Error message for when request callback is missing. */ -const REQUEST_CALLBACK_REQUIRED_ERROR = - 'Auth mechanism property REQUEST_TOKEN_CALLBACK is required.'; - /** * OIDC implementation of a callback based workflow. * @internal */ export class CallbackWorkflow implements Workflow { cache: TokenEntryCache; + callbackCache: CallbackLockCache; /** * Instantiate the workflow */ constructor() { this.cache = new TokenEntryCache(); + this.callbackCache = new CallbackLockCache(); } /** @@ -70,12 +64,11 @@ export class CallbackWorkflow implements Workflow { reauthenticating: boolean, response?: Document ): Promise { - const requestCallback = credentials.mechanismProperties.REQUEST_TOKEN_CALLBACK; - const refreshCallback = credentials.mechanismProperties.REFRESH_TOKEN_CALLBACK; - // At minimum a request callback must be provided by the user. - if (!requestCallback) { - throw new MongoInvalidArgumentError(REQUEST_CALLBACK_REQUIRED_ERROR); - } + // Get the callbacks with locks from the callback lock cache. + const { requestCallback, refreshCallback } = this.callbackCache.getCallbacks( + connection, + credentials + ); // Look for an existing entry in the cache. const entry = this.cache.getEntry( connection.address, @@ -236,13 +229,13 @@ export class CallbackWorkflow implements Workflow { // exists, then fallback to the request callback. if (refreshCallback) { context.refreshToken = entry.tokenResult.refreshToken; - result = await withLock(refreshCallback)(serverInfo, context); + result = await refreshCallback(serverInfo, context); } else { - result = await withLock(requestCallback)(serverInfo, context); + result = await requestCallback(serverInfo, context); } } else { // With no token in the cache we use the request callback. - result = await withLock(requestCallback)(serverInfo, context); + result = await requestCallback(serverInfo, context); } // Validate that the result returned by the callback is acceptable. If it is not // we must clear the token result from the cache. @@ -263,10 +256,10 @@ export class CallbackWorkflow implements Workflow { credentials.username || '', requestCallback, refreshCallback || null, - result as IdPServerResponse, + result, serverInfo ); - return result as IdPServerResponse; + return result; } } @@ -319,18 +312,3 @@ function startCommandDocument(credentials: MongoCredentials): Document { payload: new Binary(BSON.serialize(payload)) }; } - -/** - * Ensure the callback is only executed one at a time. - */ -function withLock(callback: OIDCRequestFunction | OIDCRefreshFunction) { - let lock: Promise = Promise.resolve(); - return async (info: IdPServerInfo, context: OIDCCallbackContext) => { - const result = lock - .then(() => callback(info, context)) - .finally(() => { - lock = Promise.resolve(); - }); - return result; - }; -} diff --git a/test/mongodb.ts b/test/mongodb.ts index b209ffbdf25..ec2e39dd208 100644 --- a/test/mongodb.ts +++ b/test/mongodb.ts @@ -107,6 +107,7 @@ export * from '../src/cmap/auth/mongocr'; export * from '../src/cmap/auth/mongodb_aws'; export * from '../src/cmap/auth/mongodb_oidc'; export * from '../src/cmap/auth/mongodb_oidc/aws_service_workflow'; +export * from '../src/cmap/auth/mongodb_oidc/callback_lock_cache'; export * from '../src/cmap/auth/mongodb_oidc/callback_workflow'; export * from '../src/cmap/auth/mongodb_oidc/service_workflow'; export * from '../src/cmap/auth/mongodb_oidc/token_entry_cache'; diff --git a/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts b/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts new file mode 100644 index 00000000000..5ef7f0b8a19 --- /dev/null +++ b/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts @@ -0,0 +1,137 @@ +import { setTimeout } from 'node:timers/promises'; + +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import { + CallbackLockCache, + Connection, + MongoCredentials, + MongoInvalidArgumentError +} from '../../../../mongodb'; + +describe('CallbackLockCache', function () { + describe('#getCallbacks', function () { + const connection = sinon.createStubInstance(Connection); + connection.address = 'localhost:27017'; + + context('when a request callback does not exist', function () { + const credentials = new MongoCredentials({ + username: 'test_user', + password: 'pwd', + source: '$external', + mechanismProperties: {} + }); + const cache = new CallbackLockCache(); + + it('raises an error', function () { + try { + cache.getCallbacks(connection, credentials); + } catch (error) { + expect(error).to.be.instanceOf(MongoInvalidArgumentError); + expect(error.message).to.include( + 'Auth mechanism property REQUEST_TOKEN_CALLBACK is required' + ); + } + }); + }); + + context('when no entry exists in the cache', function () { + context('when a refresh callback exists', function () { + let requestCount = 0; + let refreshCount = 0; + + const request = async () => { + requestCount++; + if (requestCount > 1) { + throw new Error('Cannot execute request simultaneously.'); + } + await setTimeout(1000); + requestCount--; + return Promise.resolve({ accessToken: '' }); + }; + const refresh = async () => { + refreshCount++; + if (refreshCount > 1) { + throw new Error('Cannot execute refresh simultaneously.'); + } + await setTimeout(1000); + refreshCount--; + return Promise.resolve({ accessToken: '' }); + }; + const requestSpy = sinon.spy(request); + const refreshSpy = sinon.spy(refresh); + const credentials = new MongoCredentials({ + username: 'test_user', + password: 'pwd', + source: '$external', + mechanismProperties: { + REQUEST_TOKEN_CALLBACK: requestSpy, + REFRESH_TOKEN_CALLBACK: refreshSpy + } + }); + const cache = new CallbackLockCache(); + const { requestCallback, refreshCallback } = cache.getCallbacks(connection, credentials); + + it('puts a new entry in the cache', function () { + expect(cache.entries.size).to.equal(1); + }); + + it('returns the new entry', function () { + expect(requestCallback).to.exist; + expect(refreshCallback).to.exist; + }); + + it('locks the callbacks', async function () { + await Promise.allSettled([ + requestCallback(), + requestCallback(), + refreshCallback(), + refreshCallback() + ]); + expect(requestSpy).to.have.been.calledTwice; + expect(refreshSpy).to.have.been.calledTwice; + }); + }); + + context('when a refresh function does not exist', function () { + let requestCount = 0; + + const request = async () => { + requestCount++; + if (requestCount > 1) { + throw new Error('Cannot execute request simultaneously.'); + } + await setTimeout(1000); + requestCount--; + return Promise.resolve({ accessToken: '' }); + }; + const requestSpy = sinon.spy(request); + const credentials = new MongoCredentials({ + username: 'test_user', + password: 'pwd', + source: '$external', + mechanismProperties: { + REQUEST_TOKEN_CALLBACK: requestSpy + } + }); + const cache = new CallbackLockCache(); + const { requestCallback, refreshCallback } = cache.getCallbacks(connection, credentials); + + it('puts a new entry in the cache', function () { + expect(cache.entries.size).to.equal(1); + }); + + it('returns the new entry', function () { + expect(requestCallback).to.exist; + expect(refreshCallback).to.not.exist; + }); + + it('locks the callbacks', async function () { + await Promise.allSettled([requestCallback(), requestCallback()]); + expect(requestSpy).to.have.been.calledTwice; + }); + }); + }); + }); +}); From 1abd3b0a2cf3f50d5497da3cda769c5e4a51e967 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 28 Apr 2023 17:54:01 +0200 Subject: [PATCH 83/93] test: clear callback cache --- src/cmap/auth/mongodb_oidc/callback_lock_cache.ts | 7 +++++++ test/manual/mongodb_oidc.prose.test.ts | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts b/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts index ca2def39633..df9fde8f1c1 100644 --- a/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts +++ b/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts @@ -34,6 +34,13 @@ export class CallbackLockCache { this.entries = new Map(); } + /** + * Clear the cache. + */ + clear() { + this.entries.clear(); + } + /** * Get the callbacks for the connection and credentials. If an entry does not * exist a new one will get set. diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index d25a3778ee8..45362f16f27 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -29,6 +29,7 @@ describe('MONGODB-OIDC', function () { describe('OIDC Auth Spec Prose Tests', function () { // Set up the cache variable. const cache = OIDC_WORKFLOWS.get('callback').cache; + const callbackCache = OIDC_WORKFLOWS.get('callback').callbackCache; // Creates a request function for use in the test. const createRequestCallback = ( username = 'test_user1', @@ -77,6 +78,10 @@ describe('MONGODB-OIDC', function () { return response; }; + beforeEach(function () { + callbackCache.clear(); + }); + describe('1. Callback-Driven Auth', function () { let client: MongoClient; let collection: Collection; From 76897a435639f7fc6301b6a3ae7791f684f0a0cb Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 28 Apr 2023 18:10:28 +0200 Subject: [PATCH 84/93] chore: debug --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 3 +++ test/manual/mongodb_oidc.prose.test.ts | 6 ++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 72c4b530c8e..dffd278eddc 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -69,6 +69,7 @@ export class CallbackWorkflow implements Workflow { connection, credentials ); + console.log('CALLBACKS', requestCallback, refreshCallback); // Look for an existing entry in the cache. const entry = this.cache.getEntry( connection.address, @@ -76,6 +77,7 @@ export class CallbackWorkflow implements Workflow { requestCallback, refreshCallback || null ); + console.log('ENTRY', entry); let result; if (entry) { // Reauthentication cannot use a token from the cache since the server has @@ -239,6 +241,7 @@ export class CallbackWorkflow implements Workflow { } // Validate that the result returned by the callback is acceptable. If it is not // we must clear the token result from the cache. + console.log('RESULT', result); if (isCallbackResultInvalid(result)) { this.cache.deleteEntry( connection.address, diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 45362f16f27..c5722bc279b 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -309,12 +309,10 @@ describe('MONGODB-OIDC', function () { }); const requestCallback = async () => { - console.log('REFRESH ENTER'); const token = await readFile(path.join(process.env.OIDC_TOKEN_DIR, 'test_user1'), { encoding: 'utf8' }); - await setTimeout(2000); - console.log('REFRESH EXIT'); + await setTimeout(3000); return generateResult(token, 300); }; const refreshCallback = createRefreshCallback(); @@ -356,7 +354,7 @@ describe('MONGODB-OIDC', function () { // Ensure that the request callback has been called once, and the refresh // callback has been called twice. it('does not simultaneously enter a callback', async function () { - await Promise.allSettled([testPromise(), testPromise()]); + await Promise.all([testPromise(), testPromise()]); expect(requestSpy).to.have.been.calledOnce; expect(refreshSpy).to.have.been.calledTwice; }); From 8e5c113a7a760f77fab030e8eebba9ea02a0e604 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 28 Apr 2023 18:35:37 +0200 Subject: [PATCH 85/93] refactor: move function hashing --- .../auth/mongodb_oidc/callback_lock_cache.ts | 68 ++++++++++++++--- .../auth/mongodb_oidc/callback_workflow.ts | 36 +++------ .../auth/mongodb_oidc/token_entry_cache.ts | 74 +++---------------- .../mongodb_oidc/callback_lock_cache.test.ts | 12 ++- .../mongodb_oidc/token_entry_cache.test.ts | 47 +++++------- 5 files changed, 104 insertions(+), 133 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts b/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts index df9fde8f1c1..6151d9f5b5e 100644 --- a/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts +++ b/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts @@ -12,6 +12,16 @@ import type { /** Error message for when request callback is missing. */ const REQUEST_CALLBACK_REQUIRED_ERROR = 'Auth mechanism property REQUEST_TOKEN_CALLBACK is required.'; +/* Counter for function "hashes".*/ +let FN_HASH_COUNTER = 0; +/* No function present function */ +const NO_FUNCTION: OIDCRequestFunction = () => { + return Promise.resolve({ accessToken: 'test' }); +}; +/* The map of function hashes */ +const FN_HASHES = new WeakMap(); +/* Put the no function hash in the map. */ +FN_HASHES.set(NO_FUNCTION, FN_HASH_COUNTER); /** * An entry of callbacks in the cache. @@ -19,6 +29,7 @@ const REQUEST_CALLBACK_REQUIRED_ERROR = interface CallbacksEntry { requestCallback: OIDCRequestFunction; refreshCallback?: OIDCRefreshFunction; + callbackHash: string; } /** @@ -46,27 +57,35 @@ export class CallbackLockCache { * exist a new one will get set. */ getCallbacks(connection: Connection, credentials: MongoCredentials): CallbacksEntry { - const entry = this.entries.get(cacheKey(connection, credentials)); + const requestCallback = credentials.mechanismProperties.REQUEST_TOKEN_CALLBACK; + const refreshCallback = credentials.mechanismProperties.REFRESH_TOKEN_CALLBACK; + if (!requestCallback) { + throw new MongoInvalidArgumentError(REQUEST_CALLBACK_REQUIRED_ERROR); + } + const callbackHash = hashFunctions(requestCallback, refreshCallback); + const key = cacheKey(connection, credentials, callbackHash); + const entry = this.entries.get(key); if (entry) { return entry; } - return this.setCallbacks(connection, credentials); + return this.setCallbacks(key, callbackHash, requestCallback, refreshCallback); } /** * Set locked callbacks on for connection and credentials. */ - private setCallbacks(connection: Connection, credentials: MongoCredentials): CallbacksEntry { - const requestCallback = credentials.mechanismProperties.REQUEST_TOKEN_CALLBACK; - const refreshCallback = credentials.mechanismProperties.REFRESH_TOKEN_CALLBACK; - if (!requestCallback) { - throw new MongoInvalidArgumentError(REQUEST_CALLBACK_REQUIRED_ERROR); - } + private setCallbacks( + key: string, + callbackHash: string, + requestCallback: OIDCRequestFunction, + refreshCallback?: OIDCRefreshFunction + ): CallbacksEntry { const entry = { requestCallback: withLock(requestCallback), - refreshCallback: refreshCallback ? withLock(refreshCallback) : undefined + refreshCallback: refreshCallback ? withLock(refreshCallback) : undefined, + callbackHash: callbackHash }; - this.entries.set(cacheKey(connection, credentials), entry); + this.entries.set(key, entry); return entry; } } @@ -74,8 +93,12 @@ export class CallbackLockCache { /** * Get a cache key based on connection and credentials. */ -function cacheKey(connection: Connection, credentials: MongoCredentials): string { - return `${connection.address}-${credentials.username}`; +function cacheKey( + connection: Connection, + credentials: MongoCredentials, + callbackHash: string +): string { + return `${connection.address}-${credentials.username}=${callbackHash}`; } /** @@ -89,3 +112,24 @@ function withLock(callback: OIDCRequestFunction | OIDCRefreshFunction) { return lock; }; } + +/** + * Get the hash string for the request and refresh functions. + */ +function hashFunctions(requestFn: OIDCRequestFunction, refreshFn?: OIDCRefreshFunction): string { + let requestHash = FN_HASHES.get(requestFn || NO_FUNCTION); + let refreshHash = FN_HASHES.get(refreshFn || NO_FUNCTION); + if (!requestHash && requestFn) { + // Create a new one for the function and put it in the map. + FN_HASH_COUNTER++; + requestHash = FN_HASH_COUNTER; + FN_HASHES.set(requestFn, FN_HASH_COUNTER); + } + if (!refreshHash && refreshFn) { + // Create a new one for the function and put it in the map. + FN_HASH_COUNTER++; + refreshHash = FN_HASH_COUNTER; + FN_HASHES.set(refreshFn, FN_HASH_COUNTER); + } + return `${requestHash}-${refreshHash}`; +} diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index dffd278eddc..d202b7e867d 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -65,18 +65,13 @@ export class CallbackWorkflow implements Workflow { response?: Document ): Promise { // Get the callbacks with locks from the callback lock cache. - const { requestCallback, refreshCallback } = this.callbackCache.getCallbacks( + const { requestCallback, refreshCallback, callbackHash } = this.callbackCache.getCallbacks( connection, credentials ); console.log('CALLBACKS', requestCallback, refreshCallback); // Look for an existing entry in the cache. - const entry = this.cache.getEntry( - connection.address, - credentials.username, - requestCallback, - refreshCallback || null - ); + const entry = this.cache.getEntry(connection.address, credentials.username, callbackHash); console.log('ENTRY', entry); let result; if (entry) { @@ -98,6 +93,7 @@ export class CallbackWorkflow implements Workflow { credentials, entry.serverInfo, reauthenticating, + callbackHash, requestCallback, refreshCallback ); @@ -117,12 +113,7 @@ export class CallbackWorkflow implements Workflow { error instanceof MongoError && error.code === MONGODB_ERROR_CODES.Reauthenticate ) { - this.cache.deleteEntry( - connection.address, - credentials.username || '', - requestCallback, - refreshCallback || null - ); + this.cache.deleteEntry(connection.address, credentials.username || '', callbackHash); result = await this.execute(connection, credentials, reauthenticating); } else { throw error; @@ -145,6 +136,7 @@ export class CallbackWorkflow implements Workflow { credentials, serverResult, reauthenticating, + callbackHash, requestCallback, refreshCallback ); @@ -208,16 +200,12 @@ export class CallbackWorkflow implements Workflow { credentials: MongoCredentials, serverInfo: IdPServerInfo, reauthenticating: boolean, + callbackHash: string, requestCallback: OIDCRequestFunction, refreshCallback?: OIDCRefreshFunction ): Promise { // Get the token from the cache. - const entry = this.cache.getEntry( - connection.address, - credentials.username, - requestCallback, - refreshCallback || null - ); + const entry = this.cache.getEntry(connection.address, credentials.username, callbackHash); let result; const context: OIDCCallbackContext = { timeoutSeconds: TIMEOUT_S, version: OIDC_VERSION }; // Check if there's a token in the cache. @@ -243,12 +231,7 @@ export class CallbackWorkflow implements Workflow { // we must clear the token result from the cache. console.log('RESULT', result); if (isCallbackResultInvalid(result)) { - this.cache.deleteEntry( - connection.address, - credentials.username || '', - requestCallback, - refreshCallback || null - ); + this.cache.deleteEntry(connection.address, credentials.username || '', callbackHash); throw new MongoMissingCredentialsError(CALLBACK_RESULT_ERROR); } // Cleanup the cache. @@ -257,8 +240,7 @@ export class CallbackWorkflow implements Workflow { this.cache.addEntry( connection.address, credentials.username || '', - requestCallback, - refreshCallback || null, + callbackHash, result, serverInfo ); diff --git a/src/cmap/auth/mongodb_oidc/token_entry_cache.ts b/src/cmap/auth/mongodb_oidc/token_entry_cache.ts index 8e58ba1b8a5..95eb5d0ac18 100644 --- a/src/cmap/auth/mongodb_oidc/token_entry_cache.ts +++ b/src/cmap/auth/mongodb_oidc/token_entry_cache.ts @@ -1,25 +1,9 @@ -import type { - IdPServerInfo, - IdPServerResponse, - OIDCRefreshFunction, - OIDCRequestFunction -} from '../mongodb_oidc'; +import type { IdPServerInfo, IdPServerResponse } from '../mongodb_oidc'; /* 5 minutes in milliseonds */ const EXPIRATION_BUFFER_MS = 300000; /* Default expiration is now for when no expiration provided */ const DEFAULT_EXPIRATION_SECS = 0; -/* Counter for function "hashes".*/ -let FN_HASH_COUNTER = 0; -/* No function present function */ -const NO_FUNCTION: OIDCRequestFunction = () => { - return Promise.resolve({ accessToken: 'test' }); -}; -/* The map of function hashes */ -const FN_HASHES = new WeakMap(); -/* Put the no function hash in the map. */ -FN_HASHES.set(NO_FUNCTION, FN_HASH_COUNTER); - /** @internal */ export class TokenEntry { tokenResult: IdPServerResponse; @@ -61,8 +45,7 @@ export class TokenEntryCache { addEntry( address: string, username: string, - requestFn: OIDCRequestFunction | null, - refreshFn: OIDCRefreshFunction | null, + callbackHash: string, tokenResult: IdPServerResponse, serverInfo: IdPServerInfo ): TokenEntry { @@ -71,7 +54,7 @@ export class TokenEntryCache { serverInfo, expirationTime(tokenResult.expiresInSeconds) ); - this.entries.set(cacheKey(address, username, requestFn, refreshFn), entry); + this.entries.set(cacheKey(address, username, callbackHash), entry); return entry; } @@ -85,25 +68,15 @@ export class TokenEntryCache { /** * Delete an entry from the cache. */ - deleteEntry( - address: string, - username: string, - requestFn: OIDCRequestFunction | null, - refreshFn: OIDCRefreshFunction | null - ): void { - this.entries.delete(cacheKey(address, username, requestFn, refreshFn)); + deleteEntry(address: string, username: string, callbackHash: string): void { + this.entries.delete(cacheKey(address, username, callbackHash)); } /** * Get an entry from the cache. */ - getEntry( - address: string, - username: string, - requestFn: OIDCRequestFunction | null, - refreshFn: OIDCRefreshFunction | null - ): TokenEntry | undefined { - return this.entries.get(cacheKey(address, username, requestFn, refreshFn)); + getEntry(address: string, username: string, callbackHash: string): TokenEntry | undefined { + return this.entries.get(cacheKey(address, username, callbackHash)); } /** @@ -128,35 +101,6 @@ function expirationTime(expiresInSeconds?: number): number { /** * Create a cache key from the address and username. */ -function cacheKey( - address: string, - username: string, - requestFn: OIDCRequestFunction | null, - refreshFn: OIDCRefreshFunction | null -): string { - return `${address}-${username}-${hashFunctions(requestFn, refreshFn)}`; -} - -/** - * Get the hash string for the request and refresh functions. - */ -function hashFunctions( - requestFn: OIDCRequestFunction | null, - refreshFn: OIDCRefreshFunction | null -): string { - let requestHash = FN_HASHES.get(requestFn || NO_FUNCTION); - let refreshHash = FN_HASHES.get(refreshFn || NO_FUNCTION); - if (!requestHash && requestFn) { - // Create a new one for the function and put it in the map. - FN_HASH_COUNTER++; - requestHash = FN_HASH_COUNTER; - FN_HASHES.set(requestFn, FN_HASH_COUNTER); - } - if (!refreshHash && refreshFn) { - // Create a new one for the function and put it in the map. - FN_HASH_COUNTER++; - refreshHash = FN_HASH_COUNTER; - FN_HASHES.set(refreshFn, FN_HASH_COUNTER); - } - return `${requestHash}-${refreshHash}`; +function cacheKey(address: string, username: string, callbackHash: string): string { + return `${address}-${username}-${callbackHash}`; } diff --git a/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts b/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts index 5ef7f0b8a19..d1451ff101d 100644 --- a/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts @@ -71,7 +71,10 @@ describe('CallbackLockCache', function () { } }); const cache = new CallbackLockCache(); - const { requestCallback, refreshCallback } = cache.getCallbacks(connection, credentials); + const { requestCallback, refreshCallback, callbackHash } = cache.getCallbacks( + connection, + credentials + ); it('puts a new entry in the cache', function () { expect(cache.entries.size).to.equal(1); @@ -80,6 +83,7 @@ describe('CallbackLockCache', function () { it('returns the new entry', function () { expect(requestCallback).to.exist; expect(refreshCallback).to.exist; + expect(callbackHash).to.exist; }); it('locks the callbacks', async function () { @@ -116,7 +120,10 @@ describe('CallbackLockCache', function () { } }); const cache = new CallbackLockCache(); - const { requestCallback, refreshCallback } = cache.getCallbacks(connection, credentials); + const { requestCallback, refreshCallback, callbackHash } = cache.getCallbacks( + connection, + credentials + ); it('puts a new entry in the cache', function () { expect(cache.entries.size).to.equal(1); @@ -125,6 +132,7 @@ describe('CallbackLockCache', function () { it('returns the new entry', function () { expect(requestCallback).to.exist; expect(refreshCallback).to.not.exist; + expect(callbackHash).to.exist; }); it('locks the callbacks', async function () { diff --git a/test/unit/cmap/auth/mongodb_oidc/token_entry_cache.test.ts b/test/unit/cmap/auth/mongodb_oidc/token_entry_cache.test.ts index 8507bf54555..048a94765f6 100644 --- a/test/unit/cmap/auth/mongodb_oidc/token_entry_cache.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/token_entry_cache.test.ts @@ -7,18 +7,11 @@ describe('TokenEntryCache', function () { accessToken: 'test', expiresInSeconds: 100 }); - const serverResult = Object.freeze({ + issuer: 'test', clientId: '1' }); - - const fnOne = () => { - return { accessToken: 'test' }; - }; - - const fnTwo = () => { - return { accessToken: 'test' }; - }; + const callbackHash = '1'; describe('#addEntry', function () { context('when expiresInSeconds is provided', function () { @@ -26,8 +19,8 @@ describe('TokenEntryCache', function () { let entry; before(function () { - cache.addEntry('localhost', 'user', fnOne, fnTwo, tokenResultWithExpiration, serverResult); - entry = cache.getEntry('localhost', 'user', fnOne, fnTwo); + cache.addEntry('localhost', 'user', callbackHash, tokenResultWithExpiration, serverResult); + entry = cache.getEntry('localhost', 'user', callbackHash); }); it('adds the token result', function () { @@ -50,8 +43,8 @@ describe('TokenEntryCache', function () { const expiredResult = Object.freeze({ accessToken: 'test' }); before(function () { - cache.addEntry('localhost', 'user', fnOne, fnTwo, expiredResult, serverResult); - entry = cache.getEntry('localhost', 'user', fnOne, fnTwo); + cache.addEntry('localhost', 'user', callbackHash, expiredResult, serverResult); + entry = cache.getEntry('localhost', 'user', callbackHash); }); it('sets an immediate expiration', function () { @@ -69,8 +62,8 @@ describe('TokenEntryCache', function () { }); before(function () { - cache.addEntry('localhost', 'user', fnOne, fnTwo, expiredResult, serverResult); - entry = cache.getEntry('localhost', 'user', fnOne, fnTwo); + cache.addEntry('localhost', 'user', callbackHash, expiredResult, serverResult); + entry = cache.getEntry('localhost', 'user', callbackHash); }); it('sets an immediate expiration', function () { @@ -83,7 +76,7 @@ describe('TokenEntryCache', function () { const cache = new TokenEntryCache(); before(function () { - cache.addEntry('localhost', 'user', fnOne, fnTwo, tokenResultWithExpiration, serverResult); + cache.addEntry('localhost', 'user', callbackHash, tokenResultWithExpiration, serverResult); cache.clear(); }); @@ -101,15 +94,15 @@ describe('TokenEntryCache', function () { }); before(function () { - cache.addEntry('localhost', 'user', fnOne, fnTwo, tokenResultWithExpiration, serverResult); - cache.addEntry('localhost', 'user2', fnOne, fnTwo, nonExpiredResult, serverResult); + cache.addEntry('localhost', 'user', callbackHash, tokenResultWithExpiration, serverResult); + cache.addEntry('localhost', 'user2', callbackHash, nonExpiredResult, serverResult); cache.deleteExpiredEntries(); }); it('deletes all expired tokens from the cache 5 minutes before expiredInSeconds', function () { expect(cache.entries.size).to.equal(1); - expect(cache.getEntry('localhost', 'user', fnOne, fnTwo)).to.not.exist; - expect(cache.getEntry('localhost', 'user2', fnOne, fnTwo)).to.exist; + expect(cache.getEntry('localhost', 'user', callbackHash)).to.not.exist; + expect(cache.getEntry('localhost', 'user2', callbackHash)).to.exist; }); }); @@ -117,12 +110,12 @@ describe('TokenEntryCache', function () { const cache = new TokenEntryCache(); before(function () { - cache.addEntry('localhost', 'user', fnOne, fnTwo, tokenResultWithExpiration, serverResult); - cache.deleteEntry('localhost', 'user', fnOne, fnTwo); + cache.addEntry('localhost', 'user', callbackHash, tokenResultWithExpiration, serverResult); + cache.deleteEntry('localhost', 'user', callbackHash); }); it('deletes the entry', function () { - expect(cache.getEntry('localhost', 'user', fnOne, fnTwo)).to.not.exist; + expect(cache.getEntry('localhost', 'user', callbackHash)).to.not.exist; }); }); @@ -130,13 +123,13 @@ describe('TokenEntryCache', function () { const cache = new TokenEntryCache(); before(function () { - cache.addEntry('localhost', 'user', fnOne, fnTwo, tokenResultWithExpiration, serverResult); - cache.addEntry('localhost', 'user2', fnOne, fnTwo, tokenResultWithExpiration, serverResult); + cache.addEntry('localhost', 'user', callbackHash, tokenResultWithExpiration, serverResult); + cache.addEntry('localhost', 'user2', callbackHash, tokenResultWithExpiration, serverResult); }); context('when there is a matching entry', function () { it('returns the entry', function () { - expect(cache.getEntry('localhost', 'user', fnOne, fnTwo)?.tokenResult).to.equal( + expect(cache.getEntry('localhost', 'user', callbackHash)?.tokenResult).to.equal( tokenResultWithExpiration ); }); @@ -144,7 +137,7 @@ describe('TokenEntryCache', function () { context('when there is no matching entry', function () { it('returns undefined', function () { - expect(cache.getEntry('localhost', 'user1', fnOne, fnTwo)).to.equal(undefined); + expect(cache.getEntry('localhost', 'user1', callbackHash)).to.equal(undefined); }); }); }); From 933596664e54af89c6f40aa58b7ac513ffc6a2f4 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 28 Apr 2023 19:05:14 +0200 Subject: [PATCH 86/93] test: throw if entered with lock --- test/manual/mongodb_oidc.prose.test.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index c5722bc279b..46bd0e50361 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -304,15 +304,22 @@ describe('MONGODB-OIDC', function () { }); describe('1.7 Lock Avoids Extra Callback Calls', function () { + let requestCounter = 0; + before(function () { cache.clear(); }); const requestCallback = async () => { + requestCounter++; + if (requestCounter > 1) { + throw new Error('Request callback was entered simultaneously.'); + } const token = await readFile(path.join(process.env.OIDC_TOKEN_DIR, 'test_user1'), { encoding: 'utf8' }); await setTimeout(3000); + requestCounter--; return generateResult(token, 300); }; const refreshCallback = createRefreshCallback(); @@ -355,7 +362,10 @@ describe('MONGODB-OIDC', function () { // callback has been called twice. it('does not simultaneously enter a callback', async function () { await Promise.all([testPromise(), testPromise()]); - expect(requestSpy).to.have.been.calledOnce; + // The request callback will get called twice, but will not be entered + // simultaneously. If it does, the function will throw and we'll have + // and exception here. + expect(requestSpy).to.have.been.calledTwice; expect(refreshSpy).to.have.been.calledTwice; }); }); From f28c29dc2cabd00e5a7d71d9ce16499fe159825a Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 28 Apr 2023 19:26:01 +0200 Subject: [PATCH 87/93] fix:lint --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index d202b7e867d..84c3b9e7317 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -69,10 +69,8 @@ export class CallbackWorkflow implements Workflow { connection, credentials ); - console.log('CALLBACKS', requestCallback, refreshCallback); // Look for an existing entry in the cache. const entry = this.cache.getEntry(connection.address, credentials.username, callbackHash); - console.log('ENTRY', entry); let result; if (entry) { // Reauthentication cannot use a token from the cache since the server has @@ -229,7 +227,6 @@ export class CallbackWorkflow implements Workflow { } // Validate that the result returned by the callback is acceptable. If it is not // we must clear the token result from the cache. - console.log('RESULT', result); if (isCallbackResultInvalid(result)) { this.cache.deleteEntry(connection.address, credentials.username || '', callbackHash); throw new MongoMissingCredentialsError(CALLBACK_RESULT_ERROR); From 68c52ef0000d38860ec8f4b12874f5fe3aee5f8d Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 28 Apr 2023 19:32:55 +0200 Subject: [PATCH 88/93] fix: change cache keys --- src/cmap/auth/mongodb_oidc/callback_lock_cache.ts | 2 +- src/cmap/auth/mongodb_oidc/token_entry_cache.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts b/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts index 6151d9f5b5e..e22b440e6cb 100644 --- a/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts +++ b/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts @@ -98,7 +98,7 @@ function cacheKey( credentials: MongoCredentials, callbackHash: string ): string { - return `${connection.address}-${credentials.username}=${callbackHash}`; + return JSON.stringify([connection.address, credentials.username, callbackHash]); } /** diff --git a/src/cmap/auth/mongodb_oidc/token_entry_cache.ts b/src/cmap/auth/mongodb_oidc/token_entry_cache.ts index 95eb5d0ac18..4fed4bebbb5 100644 --- a/src/cmap/auth/mongodb_oidc/token_entry_cache.ts +++ b/src/cmap/auth/mongodb_oidc/token_entry_cache.ts @@ -102,5 +102,5 @@ function expirationTime(expiresInSeconds?: number): number { * Create a cache key from the address and username. */ function cacheKey(address: string, username: string, callbackHash: string): string { - return `${address}-${username}-${callbackHash}`; + return JSON.stringify([address, username, callbackHash]); } From fd5a71265103a6feec3ce38f5b9c100bcc6485bf Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 28 Apr 2023 20:15:13 +0200 Subject: [PATCH 89/93] fix: timers on node 14 --- test/manual/mongodb_oidc.prose.test.ts | 2 +- test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 46bd0e50361..fb82858432f 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -1,9 +1,9 @@ import { readFile } from 'node:fs/promises'; import * as path from 'node:path'; -import { setTimeout } from 'node:timers/promises'; import { expect } from 'chai'; import * as sinon from 'sinon'; +import { setTimeout } from 'timers/promises'; import { Collection, diff --git a/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts b/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts index d1451ff101d..40fbd0df820 100644 --- a/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts @@ -1,7 +1,6 @@ -import { setTimeout } from 'node:timers/promises'; - import { expect } from 'chai'; import * as sinon from 'sinon'; +import { setTimeout } from 'timers/promises'; import { CallbackLockCache, From 76aa52b858bfc75735b29479d28376cdb755afed Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 28 Apr 2023 21:19:55 +0200 Subject: [PATCH 90/93] fix: settimeout issues --- test/manual/mongodb_oidc.prose.test.ts | 4 ++-- .../cmap/auth/mongodb_oidc/callback_lock_cache.test.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index fb82858432f..f39574b9c70 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -3,7 +3,7 @@ import * as path from 'node:path'; import { expect } from 'chai'; import * as sinon from 'sinon'; -import { setTimeout } from 'timers/promises'; +import { setTimeout } from 'timers'; import { Collection, @@ -318,7 +318,7 @@ describe('MONGODB-OIDC', function () { const token = await readFile(path.join(process.env.OIDC_TOKEN_DIR, 'test_user1'), { encoding: 'utf8' }); - await setTimeout(3000); + await new Promise(resolve => setTimeout(() => resolve(), 3000)); requestCounter--; return generateResult(token, 300); }; diff --git a/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts b/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts index 40fbd0df820..b249be89398 100644 --- a/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; -import { setTimeout } from 'timers/promises'; +import { setTimeout } from 'timers'; import { CallbackLockCache, @@ -45,7 +45,7 @@ describe('CallbackLockCache', function () { if (requestCount > 1) { throw new Error('Cannot execute request simultaneously.'); } - await setTimeout(1000); + await new Promise(resolve => setTimeout(() => resolve(), 1000)); requestCount--; return Promise.resolve({ accessToken: '' }); }; @@ -54,7 +54,7 @@ describe('CallbackLockCache', function () { if (refreshCount > 1) { throw new Error('Cannot execute refresh simultaneously.'); } - await setTimeout(1000); + await new Promise(resolve => setTimeout(() => resolve(), 1000)); refreshCount--; return Promise.resolve({ accessToken: '' }); }; @@ -105,7 +105,7 @@ describe('CallbackLockCache', function () { if (requestCount > 1) { throw new Error('Cannot execute request simultaneously.'); } - await setTimeout(1000); + await new Promise(resolve => setTimeout(() => resolve(), 1000)); requestCount--; return Promise.resolve({ accessToken: '' }); }; From 5fecafc9f76b6d11640d742b0ed1eb8eddf40c6f Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 3 May 2023 15:01:54 +0200 Subject: [PATCH 91/93] fix: suggestions --- src/cmap/auth/gssapi.ts | 2 +- src/cmap/auth/mongo_credentials.ts | 4 +- src/cmap/auth/mongodb_oidc.ts | 22 +++++++++- .../auth/mongodb_oidc/aws_service_workflow.ts | 4 +- src/cmap/auth/mongodb_oidc/cache.ts | 27 ++++++++++++ .../auth/mongodb_oidc/callback_lock_cache.ts | 44 ++++--------------- .../auth/mongodb_oidc/callback_workflow.ts | 20 ++++----- .../auth/mongodb_oidc/service_workflow.ts | 2 +- .../auth/mongodb_oidc/token_entry_cache.ts | 31 +++---------- src/cmap/auth/mongodb_oidc/workflow.ts | 22 ---------- src/cmap/auth/scram.ts | 4 +- src/mongo_client.ts | 12 ++--- src/utils.ts | 8 ++++ test/manual/mongodb_oidc.prose.test.ts | 32 ++++++-------- test/mongodb.ts | 1 - .../mongodb_oidc/callback_lock_cache.test.ts | 17 +++---- 16 files changed, 113 insertions(+), 139 deletions(-) create mode 100644 src/cmap/auth/mongodb_oidc/cache.ts delete mode 100644 src/cmap/auth/mongodb_oidc/workflow.ts diff --git a/src/cmap/auth/gssapi.ts b/src/cmap/auth/gssapi.ts index 8b5d6613e76..946c45414a2 100644 --- a/src/cmap/auth/gssapi.ts +++ b/src/cmap/auth/gssapi.ts @@ -58,7 +58,7 @@ export class GSSAPI extends AuthProvider { saslContinue(negotiatedPayload, saslStartResponse.conversationId) ); - const finalizePayload = await finalize(client, username, saslContinueResponse.payload); + const finalizePayload = await finalize(client, username ?? '', saslContinueResponse.payload); await externalCommand(connection, { saslContinue: 1, diff --git a/src/cmap/auth/mongo_credentials.ts b/src/cmap/auth/mongo_credentials.ts index 8014a95f8de..42ef26b46b9 100644 --- a/src/cmap/auth/mongo_credentials.ts +++ b/src/cmap/auth/mongo_credentials.ts @@ -61,7 +61,7 @@ export interface AuthMechanismProperties extends Document { /** @public */ export interface MongoCredentialsOptions { - username: string; + username?: string; password: string; source: string; db?: string; @@ -75,7 +75,7 @@ export interface MongoCredentialsOptions { */ export class MongoCredentials { /** The username used for authentication */ - readonly username: string; + readonly username?: string; /** The password used for authentication */ readonly password: string; /** The database that the user should authenticate against */ diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index c06ac38100c..d62591f2bfe 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -1,10 +1,12 @@ +import type { Document } from 'bson'; + import { MongoInvalidArgumentError, MongoMissingCredentialsError } from '../../error'; import type { HandshakeDocument } from '../connect'; +import type { Connection } from '../connection'; import { AuthContext, AuthProvider } from './auth_provider'; import type { MongoCredentials } from './mongo_credentials'; import { AwsServiceWorkflow } from './mongodb_oidc/aws_service_workflow'; import { CallbackWorkflow } from './mongodb_oidc/callback_workflow'; -import type { Workflow } from './mongodb_oidc/workflow'; /** Error when credentials are missing. */ const MISSING_CREDENTIALS_ERROR = 'AuthContext must provide credentials.'; @@ -60,6 +62,24 @@ export type OIDCRefreshFunction = ( type ProviderName = 'aws' | 'callback'; +export interface Workflow { + /** + * All device workflows must implement this method in order to get the access + * token and then call authenticate with it. + */ + execute( + connection: Connection, + credentials: MongoCredentials, + reauthenticating: boolean, + response?: Document + ): Promise; + + /** + * Get the document to add for speculative authentication. + */ + speculativeAuth(credentials: MongoCredentials): Promise; +} + /** @internal */ export const OIDC_WORKFLOWS: Map = new Map(); OIDC_WORKFLOWS.set('callback', new CallbackWorkflow()); diff --git a/src/cmap/auth/mongodb_oidc/aws_service_workflow.ts b/src/cmap/auth/mongodb_oidc/aws_service_workflow.ts index 9f2fc58a88e..5dd07b1d28e 100644 --- a/src/cmap/auth/mongodb_oidc/aws_service_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/aws_service_workflow.ts @@ -1,4 +1,4 @@ -import { readFile } from 'fs/promises'; +import * as fs from 'fs'; import { MongoAWSError } from '../../../error'; import { ServiceWorkflow } from './service_workflow'; @@ -24,6 +24,6 @@ export class AwsServiceWorkflow extends ServiceWorkflow { if (!tokenFile) { throw new MongoAWSError(TOKEN_MISSING_ERROR); } - return readFile(tokenFile, 'utf8'); + return fs.promises.readFile(tokenFile, 'utf8'); } } diff --git a/src/cmap/auth/mongodb_oidc/cache.ts b/src/cmap/auth/mongodb_oidc/cache.ts new file mode 100644 index 00000000000..4a0a825bd4e --- /dev/null +++ b/src/cmap/auth/mongodb_oidc/cache.ts @@ -0,0 +1,27 @@ +/** + * Base class for OIDC caches. + */ +export abstract class Cache { + entries: Map; + + /** + * Create a new cache. + */ + constructor() { + this.entries = new Map(); + } + + /** + * Clear the cache. + */ + clear() { + this.entries.clear(); + } + + /** + * Create a cache key from the address and username. + */ + cacheKey(address: string, username: string, callbackHash: string): string { + return JSON.stringify([address, username, callbackHash]); + } +} diff --git a/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts b/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts index e22b440e6cb..86c88091fb4 100644 --- a/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts +++ b/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts @@ -8,6 +8,7 @@ import type { OIDCRefreshFunction, OIDCRequestFunction } from '../mongodb_oidc'; +import { Cache } from './cache'; /** Error message for when request callback is missing. */ const REQUEST_CALLBACK_REQUIRED_ERROR = @@ -15,9 +16,7 @@ const REQUEST_CALLBACK_REQUIRED_ERROR = /* Counter for function "hashes".*/ let FN_HASH_COUNTER = 0; /* No function present function */ -const NO_FUNCTION: OIDCRequestFunction = () => { - return Promise.resolve({ accessToken: 'test' }); -}; +const NO_FUNCTION: OIDCRequestFunction = async () => ({ accessToken: 'test' }); /* The map of function hashes */ const FN_HASHES = new WeakMap(); /* Put the no function hash in the map. */ @@ -35,23 +34,7 @@ interface CallbacksEntry { /** * A cache of request and refresh callbacks per server/user. */ -export class CallbackLockCache { - entries: Map; - - /** - * Instantiate the new cache. - */ - constructor() { - this.entries = new Map(); - } - - /** - * Clear the cache. - */ - clear() { - this.entries.clear(); - } - +export class CallbackLockCache extends Cache { /** * Get the callbacks for the connection and credentials. If an entry does not * exist a new one will get set. @@ -63,7 +46,7 @@ export class CallbackLockCache { throw new MongoInvalidArgumentError(REQUEST_CALLBACK_REQUIRED_ERROR); } const callbackHash = hashFunctions(requestCallback, refreshCallback); - const key = cacheKey(connection, credentials, callbackHash); + const key = this.cacheKey(connection.address, credentials.username ?? '', callbackHash); const entry = this.entries.get(key); if (entry) { return entry; @@ -90,17 +73,6 @@ export class CallbackLockCache { } } -/** - * Get a cache key based on connection and credentials. - */ -function cacheKey( - connection: Connection, - credentials: MongoCredentials, - callbackHash: string -): string { - return JSON.stringify([connection.address, credentials.username, callbackHash]); -} - /** * Ensure the callback is only executed one at a time. */ @@ -117,15 +89,15 @@ function withLock(callback: OIDCRequestFunction | OIDCRefreshFunction) { * Get the hash string for the request and refresh functions. */ function hashFunctions(requestFn: OIDCRequestFunction, refreshFn?: OIDCRefreshFunction): string { - let requestHash = FN_HASHES.get(requestFn || NO_FUNCTION); - let refreshHash = FN_HASHES.get(refreshFn || NO_FUNCTION); - if (!requestHash && requestFn) { + let requestHash = FN_HASHES.get(requestFn); + let refreshHash = FN_HASHES.get(refreshFn ?? NO_FUNCTION); + if (requestHash == null) { // Create a new one for the function and put it in the map. FN_HASH_COUNTER++; requestHash = FN_HASH_COUNTER; FN_HASHES.set(requestFn, FN_HASH_COUNTER); } - if (!refreshHash && refreshFn) { + if (refreshHash == null && refreshFn) { // Create a new one for the function and put it in the map. FN_HASH_COUNTER++; refreshHash = FN_HASH_COUNTER; diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 84c3b9e7317..066e8e01cd4 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -9,12 +9,12 @@ import type { IdPServerResponse, OIDCCallbackContext, OIDCRefreshFunction, - OIDCRequestFunction + OIDCRequestFunction, + Workflow } from '../mongodb_oidc'; import { AuthMechanism } from '../providers'; import { CallbackLockCache } from './callback_lock_cache'; import { TokenEntryCache } from './token_entry_cache'; -import type { Workflow } from './workflow'; /** The current version of OIDC implementation. */ const OIDC_VERSION = 0; @@ -70,7 +70,7 @@ export class CallbackWorkflow implements Workflow { credentials ); // Look for an existing entry in the cache. - const entry = this.cache.getEntry(connection.address, credentials.username, callbackHash); + const entry = this.cache.getEntry(connection.address, credentials.username ?? '', callbackHash); let result; if (entry) { // Reauthentication cannot use a token from the cache since the server has @@ -111,7 +111,7 @@ export class CallbackWorkflow implements Workflow { error instanceof MongoError && error.code === MONGODB_ERROR_CODES.Reauthenticate ) { - this.cache.deleteEntry(connection.address, credentials.username || '', callbackHash); + this.cache.deleteEntry(connection.address, credentials.username ?? '', callbackHash); result = await this.execute(connection, credentials, reauthenticating); } else { throw error; @@ -203,7 +203,7 @@ export class CallbackWorkflow implements Workflow { refreshCallback?: OIDCRefreshFunction ): Promise { // Get the token from the cache. - const entry = this.cache.getEntry(connection.address, credentials.username, callbackHash); + const entry = this.cache.getEntry(connection.address, credentials.username ?? '', callbackHash); let result; const context: OIDCCallbackContext = { timeoutSeconds: TIMEOUT_S, version: OIDC_VERSION }; // Check if there's a token in the cache. @@ -228,7 +228,7 @@ export class CallbackWorkflow implements Workflow { // Validate that the result returned by the callback is acceptable. If it is not // we must clear the token result from the cache. if (isCallbackResultInvalid(result)) { - this.cache.deleteEntry(connection.address, credentials.username || '', callbackHash); + this.cache.deleteEntry(connection.address, credentials.username ?? '', callbackHash); throw new MongoMissingCredentialsError(CALLBACK_RESULT_ERROR); } // Cleanup the cache. @@ -250,7 +250,7 @@ export class CallbackWorkflow implements Workflow { * saslStart or saslContinue depending on the presence of a conversation id. */ function finishCommandDocument(token: string, conversationId?: number): Document { - if (conversationId) { + if (conversationId != null && typeof conversationId === 'number') { return { saslContinue: 1, conversationId: conversationId, @@ -273,9 +273,9 @@ function finishCommandDocument(token: string, conversationId?: number): Document * function is invalid. This means the result is nullish, doesn't contain * the accessToken required field, and does not contain extra fields. */ -function isCallbackResultInvalid(tokenResult: any): boolean { - if (!tokenResult) return true; - if (!tokenResult.accessToken) return true; +function isCallbackResultInvalid(tokenResult: unknown): boolean { + if (tokenResult == null || typeof tokenResult !== 'object') return true; + if (!('accessToken' in tokenResult)) return true; return !Object.getOwnPropertyNames(tokenResult).every(prop => RESULT_PROPERTIES.includes(prop)); } diff --git a/src/cmap/auth/mongodb_oidc/service_workflow.ts b/src/cmap/auth/mongodb_oidc/service_workflow.ts index b0fb2cc142a..4c3e5bb3164 100644 --- a/src/cmap/auth/mongodb_oidc/service_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/service_workflow.ts @@ -3,8 +3,8 @@ import { BSON, type Document } from 'bson'; import { ns } from '../../../utils'; import type { Connection } from '../../connection'; import type { MongoCredentials } from '../mongo_credentials'; +import type { Workflow } from '../mongodb_oidc'; import { AuthMechanism } from '../providers'; -import type { Workflow } from './workflow'; /** * Common behaviour for OIDC device workflows. diff --git a/src/cmap/auth/mongodb_oidc/token_entry_cache.ts b/src/cmap/auth/mongodb_oidc/token_entry_cache.ts index 4fed4bebbb5..0c24838a5dc 100644 --- a/src/cmap/auth/mongodb_oidc/token_entry_cache.ts +++ b/src/cmap/auth/mongodb_oidc/token_entry_cache.ts @@ -1,6 +1,7 @@ import type { IdPServerInfo, IdPServerResponse } from '../mongodb_oidc'; +import { Cache } from './cache'; -/* 5 minutes in milliseonds */ +/* 5 minutes in milliseconds */ const EXPIRATION_BUFFER_MS = 300000; /* Default expiration is now for when no expiration provided */ const DEFAULT_EXPIRATION_SECS = 0; @@ -32,13 +33,7 @@ export class TokenEntry { * Cache of OIDC token entries. * @internal */ -export class TokenEntryCache { - entries: Map; - - constructor() { - this.entries = new Map(); - } - +export class TokenEntryCache extends Cache { /** * Set an entry in the token cache. */ @@ -54,29 +49,22 @@ export class TokenEntryCache { serverInfo, expirationTime(tokenResult.expiresInSeconds) ); - this.entries.set(cacheKey(address, username, callbackHash), entry); + this.entries.set(this.cacheKey(address, username, callbackHash), entry); return entry; } - /** - * Clear the cache. - */ - clear(): void { - this.entries.clear(); - } - /** * Delete an entry from the cache. */ deleteEntry(address: string, username: string, callbackHash: string): void { - this.entries.delete(cacheKey(address, username, callbackHash)); + this.entries.delete(this.cacheKey(address, username, callbackHash)); } /** * Get an entry from the cache. */ getEntry(address: string, username: string, callbackHash: string): TokenEntry | undefined { - return this.entries.get(cacheKey(address, username, callbackHash)); + return this.entries.get(this.cacheKey(address, username, callbackHash)); } /** @@ -97,10 +85,3 @@ export class TokenEntryCache { function expirationTime(expiresInSeconds?: number): number { return Date.now() + (expiresInSeconds ?? DEFAULT_EXPIRATION_SECS) * 1000; } - -/** - * Create a cache key from the address and username. - */ -function cacheKey(address: string, username: string, callbackHash: string): string { - return JSON.stringify([address, username, callbackHash]); -} diff --git a/src/cmap/auth/mongodb_oidc/workflow.ts b/src/cmap/auth/mongodb_oidc/workflow.ts deleted file mode 100644 index 797f7a7a8d5..00000000000 --- a/src/cmap/auth/mongodb_oidc/workflow.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Document } from 'bson'; - -import type { Connection } from '../../connection'; -import type { MongoCredentials } from '../mongo_credentials'; - -export interface Workflow { - /** - * All device workflows must implement this method in order to get the access - * token and then call authenticate with it. - */ - execute( - connection: Connection, - credentials: MongoCredentials, - reauthenticating: boolean, - response?: Document - ): Promise; - - /** - * Get the document to add for speculative authentication. - */ - speculativeAuth(credentials: MongoCredentials): Promise; -} diff --git a/src/cmap/auth/scram.ts b/src/cmap/auth/scram.ts index 4321e5548e5..f47a296b5b0 100644 --- a/src/cmap/auth/scram.ts +++ b/src/cmap/auth/scram.ts @@ -86,7 +86,7 @@ function makeFirstMessage( credentials: MongoCredentials, nonce: Buffer ) { - const username = cleanUsername(credentials.username); + const username = cleanUsername(credentials.username ?? ''); const mechanism = cryptoMethod === 'sha1' ? AuthMechanism.MONGODB_SCRAM_SHA1 : AuthMechanism.MONGODB_SCRAM_SHA256; @@ -135,7 +135,7 @@ async function continueScramConversation( const nonce = authContext.nonce; const db = credentials.source; - const username = cleanUsername(credentials.username); + const username = cleanUsername(credentials.username ?? ''); const password = credentials.password; let processedPassword; diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 004c6d27f38..fef1da07585 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -452,21 +452,15 @@ export class MongoClient extends TypedEventEmitter { } } - // This logic is here rather than validating in the connection_string module - // or elsewhere for 2 reasons. The OIDC auth spec states that the hostname - // validation against the user provided ALLOWED_HOSTS entries or the - // default values MUST happen after SRV record resolution. At the same time, - // however, we don't even want an attempted connection via a SDAM monitor - // to even attempt to contact a host that is not allowed. So this is placed - // here directly after SRV resolution and before the creation of the - // topology to short circuit as quickly as possible. + // It is important to perform validation of hosts AFTER SRV resolution, to check the real hostname, + // but BEFORE we even attempt connecting with a potentially not allowed hostname if (options.credentials?.mechanism === AuthMechanism.MONGODB_OIDC) { const allowedHosts = options.credentials?.mechanismProperties?.ALLOWED_HOSTS || DEFAULT_ALLOWED_HOSTS; const isServiceAuth = !!options.credentials?.mechanismProperties?.PROVIDER_NAME; if (!isServiceAuth) { for (const host of options.hosts) { - if (!hostMatchesWildcards(host.host || host.socketPath || 'localhost', allowedHosts)) { + if (!hostMatchesWildcards(host.toHostPort().host, allowedHosts)) { throw new MongoInvalidArgumentError( `Host '${host}' is not valid for OIDC authentication with ALLOWED_HOSTS of '${allowedHosts.join( ',' diff --git a/src/utils.ts b/src/utils.ts index 23debd74e82..c070471e777 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,6 @@ import * as crypto from 'crypto'; import type { SrvRecord } from 'dns'; +import { setTimeout } from 'timers'; import { URL } from 'url'; import { Document, ObjectId, resolveBSONOptions } from './bson'; @@ -1086,6 +1087,13 @@ export function enumToString(en: Record): string { return Object.values(en).join(', '); } +/** + * Sleeps for provided duration in milliseconds. + */ +export function sleep(milliseconds: number): Promise { + return new Promise(resolve => setTimeout(() => resolve(), milliseconds)); +} + /** * Determine if a server supports retryable writes. * diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index f39574b9c70..771cbf28bf1 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -3,7 +3,6 @@ import * as path from 'node:path'; import { expect } from 'chai'; import * as sinon from 'sinon'; -import { setTimeout } from 'timers'; import { Collection, @@ -16,7 +15,8 @@ import { MongoMissingCredentialsError, MongoServerError, OIDC_WORKFLOWS, - OIDCCallbackContext + OIDCCallbackContext, + sleep } from '../mongodb'; describe('MONGODB-OIDC', function () { @@ -234,14 +234,11 @@ describe('MONGODB-OIDC', function () { }); it('fails validation', async function () { - try { - await collection.findOne(); - } catch (error) { - expect(error).to.be.instanceOf(MongoInvalidArgumentError); - expect(error.message).to.include( - 'is not valid for OIDC authentication with ALLOWED_HOSTS' - ); - } + const error = await collection.findOne().catch(error => error); + expect(error).to.be.instanceOf(MongoInvalidArgumentError); + expect(error.message).to.include( + 'is not valid for OIDC authentication with ALLOWED_HOSTS' + ); }); }); @@ -291,14 +288,11 @@ describe('MONGODB-OIDC', function () { }); it('fails validation', async function () { - try { - await collection.findOne(); - } catch (error) { - expect(error).to.be.instanceOf(MongoInvalidArgumentError); - expect(error.message).to.include( - 'is not valid for OIDC authentication with ALLOWED_HOSTS' - ); - } + const error = await collection.findOne().catch(error => error); + expect(error).to.be.instanceOf(MongoInvalidArgumentError); + expect(error.message).to.include( + 'is not valid for OIDC authentication with ALLOWED_HOSTS' + ); }); }); }); @@ -318,7 +312,7 @@ describe('MONGODB-OIDC', function () { const token = await readFile(path.join(process.env.OIDC_TOKEN_DIR, 'test_user1'), { encoding: 'utf8' }); - await new Promise(resolve => setTimeout(() => resolve(), 3000)); + await sleep(3000); requestCounter--; return generateResult(token, 300); }; diff --git a/test/mongodb.ts b/test/mongodb.ts index ec2e39dd208..18327be63ba 100644 --- a/test/mongodb.ts +++ b/test/mongodb.ts @@ -111,7 +111,6 @@ export * from '../src/cmap/auth/mongodb_oidc/callback_lock_cache'; export * from '../src/cmap/auth/mongodb_oidc/callback_workflow'; export * from '../src/cmap/auth/mongodb_oidc/service_workflow'; export * from '../src/cmap/auth/mongodb_oidc/token_entry_cache'; -export * from '../src/cmap/auth/mongodb_oidc/workflow'; export * from '../src/cmap/auth/plain'; export * from '../src/cmap/auth/providers'; export * from '../src/cmap/auth/scram'; diff --git a/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts b/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts index b249be89398..f2976e45f01 100644 --- a/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts @@ -1,12 +1,12 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; -import { setTimeout } from 'timers'; import { CallbackLockCache, Connection, MongoCredentials, - MongoInvalidArgumentError + MongoInvalidArgumentError, + sleep } from '../../../../mongodb'; describe('CallbackLockCache', function () { @@ -26,6 +26,7 @@ describe('CallbackLockCache', function () { it('raises an error', function () { try { cache.getCallbacks(connection, credentials); + expect.fail('Must raise error when no request callback exists.'); } catch (error) { expect(error).to.be.instanceOf(MongoInvalidArgumentError); expect(error.message).to.include( @@ -45,16 +46,16 @@ describe('CallbackLockCache', function () { if (requestCount > 1) { throw new Error('Cannot execute request simultaneously.'); } - await new Promise(resolve => setTimeout(() => resolve(), 1000)); + await sleep(1000); requestCount--; - return Promise.resolve({ accessToken: '' }); + return { accessToken: '' }; }; const refresh = async () => { refreshCount++; if (refreshCount > 1) { throw new Error('Cannot execute refresh simultaneously.'); } - await new Promise(resolve => setTimeout(() => resolve(), 1000)); + await sleep(1000); refreshCount--; return Promise.resolve({ accessToken: '' }); }; @@ -76,7 +77,7 @@ describe('CallbackLockCache', function () { ); it('puts a new entry in the cache', function () { - expect(cache.entries.size).to.equal(1); + expect(cache.entries).to.have.lengthOf(1); }); it('returns the new entry', function () { @@ -105,7 +106,7 @@ describe('CallbackLockCache', function () { if (requestCount > 1) { throw new Error('Cannot execute request simultaneously.'); } - await new Promise(resolve => setTimeout(() => resolve(), 1000)); + await sleep(1000); requestCount--; return Promise.resolve({ accessToken: '' }); }; @@ -125,7 +126,7 @@ describe('CallbackLockCache', function () { ); it('puts a new entry in the cache', function () { - expect(cache.entries.size).to.equal(1); + expect(cache.entries).to.have.lengthOf(1); }); it('returns the new entry', function () { From b947b11de81c68f3935c1c43463ad4bb0a47d68b Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 3 May 2023 22:16:52 +0200 Subject: [PATCH 92/93] fix: credentials defaulting --- src/cmap/auth/gssapi.ts | 2 +- src/cmap/auth/mongo_credentials.ts | 4 ++-- src/cmap/auth/mongodb_oidc/callback_lock_cache.ts | 2 +- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 8 ++++---- src/cmap/auth/scram.ts | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/cmap/auth/gssapi.ts b/src/cmap/auth/gssapi.ts index 946c45414a2..8b5d6613e76 100644 --- a/src/cmap/auth/gssapi.ts +++ b/src/cmap/auth/gssapi.ts @@ -58,7 +58,7 @@ export class GSSAPI extends AuthProvider { saslContinue(negotiatedPayload, saslStartResponse.conversationId) ); - const finalizePayload = await finalize(client, username ?? '', saslContinueResponse.payload); + const finalizePayload = await finalize(client, username, saslContinueResponse.payload); await externalCommand(connection, { saslContinue: 1, diff --git a/src/cmap/auth/mongo_credentials.ts b/src/cmap/auth/mongo_credentials.ts index 42ef26b46b9..9239cc171b0 100644 --- a/src/cmap/auth/mongo_credentials.ts +++ b/src/cmap/auth/mongo_credentials.ts @@ -75,7 +75,7 @@ export interface MongoCredentialsOptions { */ export class MongoCredentials { /** The username used for authentication */ - readonly username?: string; + readonly username: string; /** The password used for authentication */ readonly password: string; /** The database that the user should authenticate against */ @@ -86,7 +86,7 @@ export class MongoCredentials { readonly mechanismProperties: AuthMechanismProperties; constructor(options: MongoCredentialsOptions) { - this.username = options.username; + this.username = options.username ?? ''; this.password = options.password; this.source = options.source; if (!this.source && options.db) { diff --git a/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts b/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts index 86c88091fb4..9e77b0614c5 100644 --- a/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts +++ b/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts @@ -46,7 +46,7 @@ export class CallbackLockCache extends Cache { throw new MongoInvalidArgumentError(REQUEST_CALLBACK_REQUIRED_ERROR); } const callbackHash = hashFunctions(requestCallback, refreshCallback); - const key = this.cacheKey(connection.address, credentials.username ?? '', callbackHash); + const key = this.cacheKey(connection.address, credentials.username, callbackHash); const entry = this.entries.get(key); if (entry) { return entry; diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 066e8e01cd4..3ef1251fc43 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -70,7 +70,7 @@ export class CallbackWorkflow implements Workflow { credentials ); // Look for an existing entry in the cache. - const entry = this.cache.getEntry(connection.address, credentials.username ?? '', callbackHash); + const entry = this.cache.getEntry(connection.address, credentials.username, callbackHash); let result; if (entry) { // Reauthentication cannot use a token from the cache since the server has @@ -111,7 +111,7 @@ export class CallbackWorkflow implements Workflow { error instanceof MongoError && error.code === MONGODB_ERROR_CODES.Reauthenticate ) { - this.cache.deleteEntry(connection.address, credentials.username ?? '', callbackHash); + this.cache.deleteEntry(connection.address, credentials.username, callbackHash); result = await this.execute(connection, credentials, reauthenticating); } else { throw error; @@ -203,7 +203,7 @@ export class CallbackWorkflow implements Workflow { refreshCallback?: OIDCRefreshFunction ): Promise { // Get the token from the cache. - const entry = this.cache.getEntry(connection.address, credentials.username ?? '', callbackHash); + const entry = this.cache.getEntry(connection.address, credentials.username, callbackHash); let result; const context: OIDCCallbackContext = { timeoutSeconds: TIMEOUT_S, version: OIDC_VERSION }; // Check if there's a token in the cache. @@ -228,7 +228,7 @@ export class CallbackWorkflow implements Workflow { // Validate that the result returned by the callback is acceptable. If it is not // we must clear the token result from the cache. if (isCallbackResultInvalid(result)) { - this.cache.deleteEntry(connection.address, credentials.username ?? '', callbackHash); + this.cache.deleteEntry(connection.address, credentials.username, callbackHash); throw new MongoMissingCredentialsError(CALLBACK_RESULT_ERROR); } // Cleanup the cache. diff --git a/src/cmap/auth/scram.ts b/src/cmap/auth/scram.ts index f47a296b5b0..4321e5548e5 100644 --- a/src/cmap/auth/scram.ts +++ b/src/cmap/auth/scram.ts @@ -86,7 +86,7 @@ function makeFirstMessage( credentials: MongoCredentials, nonce: Buffer ) { - const username = cleanUsername(credentials.username ?? ''); + const username = cleanUsername(credentials.username); const mechanism = cryptoMethod === 'sha1' ? AuthMechanism.MONGODB_SCRAM_SHA1 : AuthMechanism.MONGODB_SCRAM_SHA256; @@ -135,7 +135,7 @@ async function continueScramConversation( const nonce = authContext.nonce; const db = credentials.source; - const username = cleanUsername(credentials.username ?? ''); + const username = cleanUsername(credentials.username); const password = credentials.password; let processedPassword; From bc167a2a7896c701a4ea61ad95e15338b716d925 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 3 May 2023 22:23:33 +0200 Subject: [PATCH 93/93] fix: comment addressing --- src/utils.ts | 8 -------- test/manual/mongodb_oidc.prose.test.ts | 4 ++-- .../cmap/auth/mongodb_oidc/callback_lock_cache.test.ts | 4 ++-- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index c070471e777..23debd74e82 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,5 @@ import * as crypto from 'crypto'; import type { SrvRecord } from 'dns'; -import { setTimeout } from 'timers'; import { URL } from 'url'; import { Document, ObjectId, resolveBSONOptions } from './bson'; @@ -1087,13 +1086,6 @@ export function enumToString(en: Record): string { return Object.values(en).join(', '); } -/** - * Sleeps for provided duration in milliseconds. - */ -export function sleep(milliseconds: number): Promise { - return new Promise(resolve => setTimeout(() => resolve(), milliseconds)); -} - /** * Determine if a server supports retryable writes. * diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index 771cbf28bf1..1dd3eaf0dd6 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -15,9 +15,9 @@ import { MongoMissingCredentialsError, MongoServerError, OIDC_WORKFLOWS, - OIDCCallbackContext, - sleep + OIDCCallbackContext } from '../mongodb'; +import { sleep } from '../tools/utils'; describe('MONGODB-OIDC', function () { context('when running in the environment', function () { diff --git a/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts b/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts index f2976e45f01..f7e79081426 100644 --- a/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts @@ -5,9 +5,9 @@ import { CallbackLockCache, Connection, MongoCredentials, - MongoInvalidArgumentError, - sleep + MongoInvalidArgumentError } from '../../../../mongodb'; +import { sleep } from '../../../../tools/utils'; describe('CallbackLockCache', function () { describe('#getCallbacks', function () {