Skip to content

Commit 60bfc48

Browse files
feat(NODE-5940): cache the AWS credentials provider in the MONGODB-AWS auth logic (#4000)
1 parent 4893330 commit 60bfc48

16 files changed

+229
-106
lines changed

src/cmap/auth/auth_provider.ts

+4
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ export class AuthContext {
3434
}
3535
}
3636

37+
/**
38+
* Provider used during authentication.
39+
* @internal
40+
*/
3741
export abstract class AuthProvider {
3842
/**
3943
* Prepare the handshake document before the initial handshake.

src/cmap/auth/mongodb_aws.ts

+62-61
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
1-
import * as crypto from 'crypto';
21
import * as process from 'process';
3-
import { promisify } from 'util';
42

53
import type { Binary, BSONSerializeOptions } from '../../bson';
64
import * as BSON from '../../bson';
7-
import { aws4, getAwsCredentialProvider } from '../../deps';
5+
import { aws4, type AWSCredentials, getAwsCredentialProvider } from '../../deps';
86
import {
97
MongoAWSError,
108
MongoCompatibilityError,
119
MongoMissingCredentialsError,
1210
MongoRuntimeError
1311
} from '../../error';
14-
import { ByteUtils, maxWireVersion, ns, request } from '../../utils';
12+
import { ByteUtils, maxWireVersion, ns, randomBytes, request } from '../../utils';
1513
import { type AuthContext, AuthProvider } from './auth_provider';
1614
import { MongoCredentials } from './mongo_credentials';
1715
import { AuthMechanism } from './providers';
@@ -57,12 +55,40 @@ interface AWSSaslContinuePayload {
5755
}
5856

5957
export class MongoDBAWS extends AuthProvider {
60-
static credentialProvider: ReturnType<typeof getAwsCredentialProvider> | null = null;
61-
randomBytesAsync: (size: number) => Promise<Buffer>;
58+
static credentialProvider: ReturnType<typeof getAwsCredentialProvider>;
59+
provider?: () => Promise<AWSCredentials>;
6260

6361
constructor() {
6462
super();
65-
this.randomBytesAsync = promisify(crypto.randomBytes);
63+
MongoDBAWS.credentialProvider ??= getAwsCredentialProvider();
64+
65+
let { AWS_STS_REGIONAL_ENDPOINTS = '', AWS_REGION = '' } = process.env;
66+
AWS_STS_REGIONAL_ENDPOINTS = AWS_STS_REGIONAL_ENDPOINTS.toLowerCase();
67+
AWS_REGION = AWS_REGION.toLowerCase();
68+
69+
/** The option setting should work only for users who have explicit settings in their environment, the driver should not encode "defaults" */
70+
const awsRegionSettingsExist =
71+
AWS_REGION.length !== 0 && AWS_STS_REGIONAL_ENDPOINTS.length !== 0;
72+
73+
/**
74+
* If AWS_STS_REGIONAL_ENDPOINTS is set to regional, users are opting into the new behavior of respecting the region settings
75+
*
76+
* If AWS_STS_REGIONAL_ENDPOINTS is set to legacy, then "old" regions need to keep using the global setting.
77+
* Technically the SDK gets this wrong, it reaches out to 'sts.us-east-1.amazonaws.com' when it should be 'sts.amazonaws.com'.
78+
* That is not our bug to fix here. We leave that up to the SDK.
79+
*/
80+
const useRegionalSts =
81+
AWS_STS_REGIONAL_ENDPOINTS === 'regional' ||
82+
(AWS_STS_REGIONAL_ENDPOINTS === 'legacy' && !LEGACY_REGIONS.has(AWS_REGION));
83+
84+
if ('fromNodeProviderChain' in MongoDBAWS.credentialProvider) {
85+
this.provider =
86+
awsRegionSettingsExist && useRegionalSts
87+
? MongoDBAWS.credentialProvider.fromNodeProviderChain({
88+
clientConfig: { region: AWS_REGION }
89+
})
90+
: MongoDBAWS.credentialProvider.fromNodeProviderChain();
91+
}
6692
}
6793

6894
override async auth(authContext: AuthContext): Promise<void> {
@@ -83,7 +109,7 @@ export class MongoDBAWS extends AuthProvider {
83109
}
84110

85111
if (!authContext.credentials.username) {
86-
authContext.credentials = await makeTempCredentials(authContext.credentials);
112+
authContext.credentials = await makeTempCredentials(authContext.credentials, this.provider);
87113
}
88114

89115
const { credentials } = authContext;
@@ -101,7 +127,7 @@ export class MongoDBAWS extends AuthProvider {
101127
: undefined;
102128

103129
const db = credentials.source;
104-
const nonce = await this.randomBytesAsync(32);
130+
const nonce = await randomBytes(32);
105131

106132
const saslStart = {
107133
saslStart: 1,
@@ -181,7 +207,10 @@ interface AWSTempCredentials {
181207
Expiration?: Date;
182208
}
183209

184-
async function makeTempCredentials(credentials: MongoCredentials): Promise<MongoCredentials> {
210+
async function makeTempCredentials(
211+
credentials: MongoCredentials,
212+
provider?: () => Promise<AWSCredentials>
213+
): Promise<MongoCredentials> {
185214
function makeMongoCredentialsFromAWSTemp(creds: AWSTempCredentials) {
186215
if (!creds.AccessKeyId || !creds.SecretAccessKey || !creds.Token) {
187216
throw new MongoMissingCredentialsError('Could not obtain temporary MONGODB-AWS credentials');
@@ -198,11 +227,31 @@ async function makeTempCredentials(credentials: MongoCredentials): Promise<Mongo
198227
});
199228
}
200229

201-
MongoDBAWS.credentialProvider ??= getAwsCredentialProvider();
202-
203230
// Check if the AWS credential provider from the SDK is present. If not,
204231
// use the old method.
205-
if ('kModuleError' in MongoDBAWS.credentialProvider) {
232+
if (provider && !('kModuleError' in MongoDBAWS.credentialProvider)) {
233+
/*
234+
* Creates a credential provider that will attempt to find credentials from the
235+
* following sources (listed in order of precedence):
236+
*
237+
* - Environment variables exposed via process.env
238+
* - SSO credentials from token cache
239+
* - Web identity token credentials
240+
* - Shared credentials and config ini files
241+
* - The EC2/ECS Instance Metadata Service
242+
*/
243+
try {
244+
const creds = await provider();
245+
return makeMongoCredentialsFromAWSTemp({
246+
AccessKeyId: creds.accessKeyId,
247+
SecretAccessKey: creds.secretAccessKey,
248+
Token: creds.sessionToken,
249+
Expiration: creds.expiration
250+
});
251+
} catch (error) {
252+
throw new MongoAWSError(error.message);
253+
}
254+
} else {
206255
// If the environment variable AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
207256
// is set then drivers MUST assume that it was set by an AWS ECS agent
208257
if (process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) {
@@ -232,54 +281,6 @@ async function makeTempCredentials(credentials: MongoCredentials): Promise<Mongo
232281
});
233282

234283
return makeMongoCredentialsFromAWSTemp(creds);
235-
} else {
236-
let { AWS_STS_REGIONAL_ENDPOINTS = '', AWS_REGION = '' } = process.env;
237-
AWS_STS_REGIONAL_ENDPOINTS = AWS_STS_REGIONAL_ENDPOINTS.toLowerCase();
238-
AWS_REGION = AWS_REGION.toLowerCase();
239-
240-
/** The option setting should work only for users who have explicit settings in their environment, the driver should not encode "defaults" */
241-
const awsRegionSettingsExist =
242-
AWS_REGION.length !== 0 && AWS_STS_REGIONAL_ENDPOINTS.length !== 0;
243-
244-
/**
245-
* If AWS_STS_REGIONAL_ENDPOINTS is set to regional, users are opting into the new behavior of respecting the region settings
246-
*
247-
* If AWS_STS_REGIONAL_ENDPOINTS is set to legacy, then "old" regions need to keep using the global setting.
248-
* Technically the SDK gets this wrong, it reaches out to 'sts.us-east-1.amazonaws.com' when it should be 'sts.amazonaws.com'.
249-
* That is not our bug to fix here. We leave that up to the SDK.
250-
*/
251-
const useRegionalSts =
252-
AWS_STS_REGIONAL_ENDPOINTS === 'regional' ||
253-
(AWS_STS_REGIONAL_ENDPOINTS === 'legacy' && !LEGACY_REGIONS.has(AWS_REGION));
254-
255-
const provider =
256-
awsRegionSettingsExist && useRegionalSts
257-
? MongoDBAWS.credentialProvider.fromNodeProviderChain({
258-
clientConfig: { region: AWS_REGION }
259-
})
260-
: MongoDBAWS.credentialProvider.fromNodeProviderChain();
261-
262-
/*
263-
* Creates a credential provider that will attempt to find credentials from the
264-
* following sources (listed in order of precedence):
265-
*
266-
* - Environment variables exposed via process.env
267-
* - SSO credentials from token cache
268-
* - Web identity token credentials
269-
* - Shared credentials and config ini files
270-
* - The EC2/ECS Instance Metadata Service
271-
*/
272-
try {
273-
const creds = await provider();
274-
return makeMongoCredentialsFromAWSTemp({
275-
AccessKeyId: creds.accessKeyId,
276-
SecretAccessKey: creds.secretAccessKey,
277-
Token: creds.sessionToken,
278-
Expiration: creds.expiration
279-
});
280-
} catch (error) {
281-
throw new MongoAWSError(error.message);
282-
}
283284
}
284285
}
285286

src/cmap/auth/scram.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as crypto from 'crypto';
2-
import { promisify } from 'util';
32

43
import { Binary, type Document } from '../../bson';
54
import { saslprep } from '../../deps';
@@ -8,7 +7,7 @@ import {
87
MongoMissingCredentialsError,
98
MongoRuntimeError
109
} from '../../error';
11-
import { emitWarning, ns } from '../../utils';
10+
import { emitWarning, ns, randomBytes } from '../../utils';
1211
import type { HandshakeDocument } from '../connect';
1312
import { type AuthContext, AuthProvider } from './auth_provider';
1413
import type { MongoCredentials } from './mongo_credentials';
@@ -18,11 +17,9 @@ type CryptoMethod = 'sha1' | 'sha256';
1817

1918
class ScramSHA extends AuthProvider {
2019
cryptoMethod: CryptoMethod;
21-
randomBytesAsync: (size: number) => Promise<Buffer>;
2220
constructor(cryptoMethod: CryptoMethod) {
2321
super();
2422
this.cryptoMethod = cryptoMethod || 'sha1';
25-
this.randomBytesAsync = promisify(crypto.randomBytes);
2623
}
2724

2825
override async prepare(
@@ -41,7 +38,7 @@ class ScramSHA extends AuthProvider {
4138
emitWarning('Warning: no saslprep library specified. Passwords will not be sanitized');
4239
}
4340

44-
const nonce = await this.randomBytesAsync(24);
41+
const nonce = await randomBytes(24);
4542
// store the nonce for later use
4643
authContext.nonce = nonce;
4744

src/cmap/connect.ts

+11-22
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,8 @@ import {
1717
needsRetryableWriteLabel
1818
} from '../error';
1919
import { type Callback, HostAddress, ns } from '../utils';
20-
import { AuthContext, type AuthProvider } from './auth/auth_provider';
21-
import { GSSAPI } from './auth/gssapi';
22-
import { MongoCR } from './auth/mongocr';
23-
import { MongoDBAWS } from './auth/mongodb_aws';
24-
import { Plain } from './auth/plain';
20+
import { AuthContext } from './auth/auth_provider';
2521
import { AuthMechanism } from './auth/providers';
26-
import { ScramSHA1, ScramSHA256 } from './auth/scram';
27-
import { X509 } from './auth/x509';
2822
import {
2923
type CommandOptions,
3024
Connection,
@@ -39,17 +33,6 @@ import {
3933
MIN_SUPPORTED_WIRE_VERSION
4034
} from './wire_protocol/constants';
4135

42-
/** @internal */
43-
export const AUTH_PROVIDERS = new Map<AuthMechanism | string, AuthProvider>([
44-
[AuthMechanism.MONGODB_AWS, new MongoDBAWS()],
45-
[AuthMechanism.MONGODB_CR, new MongoCR()],
46-
[AuthMechanism.MONGODB_GSSAPI, new GSSAPI()],
47-
[AuthMechanism.MONGODB_PLAIN, new Plain()],
48-
[AuthMechanism.MONGODB_SCRAM_SHA1, new ScramSHA1()],
49-
[AuthMechanism.MONGODB_SCRAM_SHA256, new ScramSHA256()],
50-
[AuthMechanism.MONGODB_X509, new X509()]
51-
]);
52-
5336
/** @public */
5437
export type Stream = Socket | TLSSocket;
5538

@@ -110,7 +93,7 @@ async function performInitialHandshake(
11093
if (credentials) {
11194
if (
11295
!(credentials.mechanism === AuthMechanism.MONGODB_DEFAULT) &&
113-
!AUTH_PROVIDERS.get(credentials.mechanism)
96+
!options.authProviders.getOrCreateProvider(credentials.mechanism)
11497
) {
11598
throw new MongoInvalidArgumentError(`AuthMechanism '${credentials.mechanism}' not supported`);
11699
}
@@ -165,7 +148,7 @@ async function performInitialHandshake(
165148
authContext.response = response;
166149

167150
const resolvedCredentials = credentials.resolveAuthMechanism(response);
168-
const provider = AUTH_PROVIDERS.get(resolvedCredentials.mechanism);
151+
const provider = options.authProviders.getOrCreateProvider(resolvedCredentials.mechanism);
169152
if (!provider) {
170153
throw new MongoInvalidArgumentError(
171154
`No AuthProvider for ${resolvedCredentials.mechanism} defined.`
@@ -186,6 +169,10 @@ async function performInitialHandshake(
186169
}
187170
}
188171

172+
/**
173+
* HandshakeDocument used during authentication.
174+
* @internal
175+
*/
189176
export interface HandshakeDocument extends Document {
190177
/**
191178
* @deprecated Use hello instead
@@ -227,7 +214,9 @@ export async function prepareHandshakeDocument(
227214
if (credentials.mechanism === AuthMechanism.MONGODB_DEFAULT && credentials.username) {
228215
handshakeDoc.saslSupportedMechs = `${credentials.source}.${credentials.username}`;
229216

230-
const provider = AUTH_PROVIDERS.get(AuthMechanism.MONGODB_SCRAM_SHA256);
217+
const provider = authContext.options.authProviders.getOrCreateProvider(
218+
AuthMechanism.MONGODB_SCRAM_SHA256
219+
);
231220
if (!provider) {
232221
// This auth mechanism is always present.
233222
throw new MongoInvalidArgumentError(
@@ -236,7 +225,7 @@ export async function prepareHandshakeDocument(
236225
}
237226
return provider.prepare(handshakeDoc, authContext);
238227
}
239-
const provider = AUTH_PROVIDERS.get(credentials.mechanism);
228+
const provider = authContext.options.authProviders.getOrCreateProvider(credentials.mechanism);
240229
if (!provider) {
241230
throw new MongoInvalidArgumentError(`No AuthProvider for ${credentials.mechanism} defined.`);
242231
}

src/cmap/connection.ts

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
MongoWriteConcernError
2424
} from '../error';
2525
import type { ServerApi, SupportedNodeConnectionOptions } from '../mongo_client';
26+
import { type MongoClientAuthProviders } from '../mongo_client_auth_providers';
2627
import { type CancellationToken, TypedEventEmitter } from '../mongo_types';
2728
import type { ReadPreferenceLike } from '../read_preference';
2829
import { applySession, type ClientSession, updateSessionFromResponse } from '../sessions';
@@ -120,6 +121,8 @@ export interface ConnectionOptions
120121
/** @internal */
121122
connectionType?: typeof Connection;
122123
credentials?: MongoCredentials;
124+
/** @internal */
125+
authProviders: MongoClientAuthProviders;
123126
connectTimeoutMS?: number;
124127
tls: boolean;
125128
/** @deprecated - Will not be able to turn off in the future. */

src/cmap/connection_pool.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
import { CancellationToken, TypedEventEmitter } from '../mongo_types';
2929
import type { Server } from '../sdam/server';
3030
import { type Callback, eachAsync, List, makeCounter } from '../utils';
31-
import { AUTH_PROVIDERS, connect } from './connect';
31+
import { connect } from './connect';
3232
import { Connection, type ConnectionEvents, type ConnectionOptions } from './connection';
3333
import {
3434
ConnectionCheckedInEvent,
@@ -620,7 +620,9 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
620620
);
621621
}
622622
const resolvedCredentials = credentials.resolveAuthMechanism(connection.hello || undefined);
623-
const provider = AUTH_PROVIDERS.get(resolvedCredentials.mechanism);
623+
const provider = this[kServer].topology.client.s.authProviders.getOrCreateProvider(
624+
resolvedCredentials.mechanism
625+
);
624626
if (!provider) {
625627
return callback(
626628
new MongoMissingCredentialsError(
@@ -697,7 +699,8 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
697699
...this.options,
698700
id: this[kConnectionCounter].next().value,
699701
generation: this[kGeneration],
700-
cancellationToken: this[kCancellationToken]
702+
cancellationToken: this[kCancellationToken],
703+
authProviders: this[kServer].topology.client.s.authProviders
701704
};
702705

703706
this[kPending]++;

src/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ export type {
200200
ResumeToken,
201201
UpdateDescription
202202
} from './change_stream';
203-
export type { AuthContext } from './cmap/auth/auth_provider';
203+
export type { AuthContext, AuthProvider } from './cmap/auth/auth_provider';
204204
export type {
205205
AuthMechanismProperties,
206206
MongoCredentials,
@@ -217,6 +217,7 @@ export type {
217217
Response,
218218
WriteProtocolMessageType
219219
} from './cmap/commands';
220+
export type { HandshakeDocument } from './cmap/connect';
220221
export type { LEGAL_TCP_SOCKET_OPTIONS, LEGAL_TLS_SOCKET_OPTIONS, Stream } from './cmap/connect';
221222
export type {
222223
CommandOptions,
@@ -304,6 +305,7 @@ export type {
304305
SupportedTLSSocketOptions,
305306
WithSessionCallback
306307
} from './mongo_client';
308+
export { MongoClientAuthProviders } from './mongo_client_auth_providers';
307309
export type {
308310
Log,
309311
LogConvertible,

0 commit comments

Comments
 (0)