Skip to content

Commit 3d047ed

Browse files
authored
feat(NODE-6141): allow custom aws sdk config (#4373)
1 parent 6895b25 commit 3d047ed

12 files changed

+323
-47
lines changed

src/client-side-encryption/auto_encrypter.ts

+17-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ import { autoSelectSocketOptions } from './client_encryption';
1717
import * as cryptoCallbacks from './crypto_callbacks';
1818
import { MongoCryptInvalidArgumentError } from './errors';
1919
import { MongocryptdManager } from './mongocryptd_manager';
20-
import { type KMSProviders, refreshKMSCredentials } from './providers';
20+
import {
21+
type CredentialProviders,
22+
isEmptyCredentials,
23+
type KMSProviders,
24+
refreshKMSCredentials
25+
} from './providers';
2126
import { type CSFLEKMSTlsOptions, StateMachine } from './state_machine';
2227

2328
/** @public */
@@ -30,6 +35,8 @@ export interface AutoEncryptionOptions {
3035
keyVaultNamespace?: string;
3136
/** Configuration options that are used by specific KMS providers during key generation, encryption, and decryption. */
3237
kmsProviders?: KMSProviders;
38+
/** Configuration options for custom credential providers. */
39+
credentialProviders?: CredentialProviders;
3340
/**
3441
* A map of namespaces to a local JSON schema for encryption
3542
*
@@ -153,6 +160,7 @@ export class AutoEncrypter {
153160
_kmsProviders: KMSProviders;
154161
_bypassMongocryptdAndCryptShared: boolean;
155162
_contextCounter: number;
163+
_credentialProviders?: CredentialProviders;
156164

157165
_mongocryptdManager?: MongocryptdManager;
158166
_mongocryptdClient?: MongoClient;
@@ -237,6 +245,13 @@ export class AutoEncrypter {
237245
this._proxyOptions = options.proxyOptions || {};
238246
this._tlsOptions = options.tlsOptions || {};
239247
this._kmsProviders = options.kmsProviders || {};
248+
this._credentialProviders = options.credentialProviders;
249+
250+
if (options.credentialProviders?.aws && !isEmptyCredentials('aws', this._kmsProviders)) {
251+
throw new MongoCryptInvalidArgumentError(
252+
'Can only provide a custom AWS credential provider when the state machine is configured for automatic AWS credential fetching'
253+
);
254+
}
240255

241256
const mongoCryptOptions: MongoCryptOptions = {
242257
enableMultipleCollinfo: true,
@@ -439,7 +454,7 @@ export class AutoEncrypter {
439454
* the original ones.
440455
*/
441456
async askForKMSCredentials(): Promise<KMSProviders> {
442-
return await refreshKMSCredentials(this._kmsProviders);
457+
return await refreshKMSCredentials(this._kmsProviders, this._credentialProviders);
443458
}
444459

445460
/**

src/client-side-encryption/client_encryption.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import {
3434
} from './errors';
3535
import {
3636
type ClientEncryptionDataKeyProvider,
37+
type CredentialProviders,
38+
isEmptyCredentials,
3739
type KMSProviders,
3840
refreshKMSCredentials
3941
} from './providers/index';
@@ -81,6 +83,9 @@ export class ClientEncryption {
8183
/** @internal */
8284
_mongoCrypt: MongoCrypt;
8385

86+
/** @internal */
87+
_credentialProviders?: CredentialProviders;
88+
8489
/** @internal */
8590
static getMongoCrypt(): MongoCryptConstructor {
8691
const encryption = getMongoDBClientEncryption();
@@ -125,6 +130,13 @@ export class ClientEncryption {
125130
this._kmsProviders = options.kmsProviders || {};
126131
const { timeoutMS } = resolveTimeoutOptions(client, options);
127132
this._timeoutMS = timeoutMS;
133+
this._credentialProviders = options.credentialProviders;
134+
135+
if (options.credentialProviders?.aws && !isEmptyCredentials('aws', this._kmsProviders)) {
136+
throw new MongoCryptInvalidArgumentError(
137+
'Can only provide a custom AWS credential provider when the state machine is configured for automatic AWS credential fetching'
138+
);
139+
}
128140

129141
if (options.keyVaultNamespace == null) {
130142
throw new MongoCryptInvalidArgumentError('Missing required option `keyVaultNamespace`');
@@ -712,7 +724,7 @@ export class ClientEncryption {
712724
* the original ones.
713725
*/
714726
async askForKMSCredentials(): Promise<KMSProviders> {
715-
return await refreshKMSCredentials(this._kmsProviders);
727+
return await refreshKMSCredentials(this._kmsProviders, this._credentialProviders);
716728
}
717729

718730
static get libmongocryptVersion() {
@@ -858,6 +870,11 @@ export interface ClientEncryptionOptions {
858870
*/
859871
kmsProviders?: KMSProviders;
860872

873+
/**
874+
* Options for user provided custom credential providers.
875+
*/
876+
credentialProviders?: CredentialProviders;
877+
861878
/**
862879
* Options for specifying a Socks5 proxy to use for connecting to the KMS.
863880
*/

src/client-side-encryption/providers/aws.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
import { AWSSDKCredentialProvider } from '../../cmap/auth/aws_temporary_credentials';
1+
import {
2+
type AWSCredentialProvider,
3+
AWSSDKCredentialProvider
4+
} from '../../cmap/auth/aws_temporary_credentials';
25
import { type KMSProviders } from '.';
36

47
/**
58
* @internal
69
*/
7-
export async function loadAWSCredentials(kmsProviders: KMSProviders): Promise<KMSProviders> {
8-
const credentialProvider = new AWSSDKCredentialProvider();
10+
export async function loadAWSCredentials(
11+
kmsProviders: KMSProviders,
12+
provider?: AWSCredentialProvider
13+
): Promise<KMSProviders> {
14+
const credentialProvider = new AWSSDKCredentialProvider(provider);
915

1016
// We shouldn't ever receive a response from the AWS SDK that doesn't have a `SecretAccessKey`
1117
// or `AccessKeyId`. However, TS says these fields are optional. We provide empty strings

src/client-side-encryption/providers/index.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Binary } from '../../bson';
2+
import { type AWSCredentialProvider } from '../../cmap/auth/aws_temporary_credentials';
23
import { loadAWSCredentials } from './aws';
34
import { loadAzureCredentials } from './azure';
45
import { loadGCPCredentials } from './gcp';
@@ -112,6 +113,15 @@ export type GCPKMSProviderConfiguration =
112113
accessToken: string;
113114
};
114115

116+
/**
117+
* @public
118+
* Configuration options for custom credential providers for KMS requests.
119+
*/
120+
export interface CredentialProviders {
121+
/* A custom AWS credential provider */
122+
aws?: AWSCredentialProvider;
123+
}
124+
115125
/**
116126
* @public
117127
* Configuration options that are used by specific KMS providers during key generation, encryption, and decryption.
@@ -176,11 +186,14 @@ export function isEmptyCredentials(
176186
*
177187
* @internal
178188
*/
179-
export async function refreshKMSCredentials(kmsProviders: KMSProviders): Promise<KMSProviders> {
189+
export async function refreshKMSCredentials(
190+
kmsProviders: KMSProviders,
191+
credentialProviders?: CredentialProviders
192+
): Promise<KMSProviders> {
180193
let finalKMSProviders = kmsProviders;
181194

182195
if (isEmptyCredentials('aws', kmsProviders)) {
183-
finalKMSProviders = await loadAWSCredentials(finalKMSProviders);
196+
finalKMSProviders = await loadAWSCredentials(finalKMSProviders, credentialProviders?.aws);
184197
}
185198

186199
if (isEmptyCredentials('gcp', kmsProviders)) {

src/cmap/auth/aws_temporary_credentials.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ export interface AWSTempCredentials {
2121
Expiration?: Date;
2222
}
2323

24+
/** @public **/
25+
export type AWSCredentialProvider = () => Promise<AWSCredentials>;
26+
2427
/**
2528
* @internal
2629
*
@@ -41,7 +44,20 @@ export abstract class AWSTemporaryCredentialProvider {
4144

4245
/** @internal */
4346
export class AWSSDKCredentialProvider extends AWSTemporaryCredentialProvider {
44-
private _provider?: () => Promise<AWSCredentials>;
47+
private _provider?: AWSCredentialProvider;
48+
49+
/**
50+
* Create the SDK credentials provider.
51+
* @param credentialsProvider - The credentials provider.
52+
*/
53+
constructor(credentialsProvider?: AWSCredentialProvider) {
54+
super();
55+
56+
if (credentialsProvider) {
57+
this._provider = credentialsProvider;
58+
}
59+
}
60+
4561
/**
4662
* The AWS SDK caches credentials automatically and handles refresh when the credentials have expired.
4763
* To ensure this occurs, we need to cache the `provider` returned by the AWS sdk and re-use it when fetching credentials.

src/cmap/auth/mongo_credentials.ts

+28
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
MongoInvalidArgumentError,
77
MongoMissingCredentialsError
88
} from '../../error';
9+
import type { AWSCredentialProvider } from './aws_temporary_credentials';
910
import { GSSAPICanonicalizationValue } from './gssapi';
1011
import type { OIDCCallbackFunction } from './mongodb_oidc';
1112
import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism } from './providers';
@@ -68,6 +69,33 @@ export interface AuthMechanismProperties extends Document {
6869
ALLOWED_HOSTS?: string[];
6970
/** The resource token for OIDC auth in Azure and GCP. */
7071
TOKEN_RESOURCE?: string;
72+
/**
73+
* A custom AWS credential provider to use. An example using the AWS SDK default provider chain:
74+
*
75+
* ```ts
76+
* const client = new MongoClient(process.env.MONGODB_URI, {
77+
* authMechanismProperties: {
78+
* AWS_CREDENTIAL_PROVIDER: fromNodeProviderChain()
79+
* }
80+
* });
81+
* ```
82+
*
83+
* Using a custom function that returns AWS credentials:
84+
*
85+
* ```ts
86+
* const client = new MongoClient(process.env.MONGODB_URI, {
87+
* authMechanismProperties: {
88+
* AWS_CREDENTIAL_PROVIDER: async () => {
89+
* return {
90+
* accessKeyId: process.env.ACCESS_KEY_ID,
91+
* secretAccessKey: process.env.SECRET_ACCESS_KEY
92+
* }
93+
* }
94+
* }
95+
* });
96+
* ```
97+
*/
98+
AWS_CREDENTIAL_PROVIDER?: AWSCredentialProvider;
7199
}
72100

73101
/** @public */

src/cmap/auth/mongodb_aws.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { ByteUtils, maxWireVersion, ns, randomBytes } from '../../utils';
1010
import { type AuthContext, AuthProvider } from './auth_provider';
1111
import {
12+
type AWSCredentialProvider,
1213
AWSSDKCredentialProvider,
1314
type AWSTempCredentials,
1415
AWSTemporaryCredentialProvider,
@@ -34,11 +35,14 @@ interface AWSSaslContinuePayload {
3435

3536
export class MongoDBAWS extends AuthProvider {
3637
private credentialFetcher: AWSTemporaryCredentialProvider;
37-
constructor() {
38+
private credentialProvider?: AWSCredentialProvider;
39+
40+
constructor(credentialProvider?: AWSCredentialProvider) {
3841
super();
3942

43+
this.credentialProvider = credentialProvider;
4044
this.credentialFetcher = AWSTemporaryCredentialProvider.isAWSSDKInstalled
41-
? new AWSSDKCredentialProvider()
45+
? new AWSSDKCredentialProvider(credentialProvider)
4246
: new LegacyAWSTemporaryCredentialProvider();
4347
}
4448

src/deps.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,14 @@ export function getZstdLibrary(): ZStandardLib | { kModuleError: MongoMissingDep
7878
}
7979

8080
/**
81-
* @internal
81+
* @public
8282
* Copy of the AwsCredentialIdentityProvider interface from [`smithy/types`](https://socket.dev/npm/package/\@smithy/types/files/1.1.1/dist-types/identity/awsCredentialIdentity.d.ts),
8383
* the return type of the aws-sdk's `fromNodeProviderChain().provider()`.
8484
*/
8585
export interface AWSCredentials {
8686
accessKeyId: string;
8787
secretAccessKey: string;
88-
sessionToken: string;
88+
sessionToken?: string;
8989
expiration?: Date;
9090
}
9191

src/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,11 @@ export { ReadPreferenceMode } from './read_preference';
128128
export { ServerType, TopologyType } from './sdam/common';
129129

130130
// Helper classes
131+
export type { AWSCredentialProvider } from './cmap/auth/aws_temporary_credentials';
132+
export type { AWSCredentials } from './deps';
131133
export { ReadConcern } from './read_concern';
132134
export { ReadPreference } from './read_preference';
133135
export { WriteConcern } from './write_concern';
134-
135136
// events
136137
export {
137138
CommandFailedEvent,
@@ -255,6 +256,7 @@ export type {
255256
AWSKMSProviderConfiguration,
256257
AzureKMSProviderConfiguration,
257258
ClientEncryptionDataKeyProvider,
259+
CredentialProviders,
258260
GCPKMSProviderConfiguration,
259261
KMIPKMSProviderConfiguration,
260262
KMSProviders,

src/mongo_client_auth_providers.ts

+26-29
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,14 @@ import { X509 } from './cmap/auth/x509';
1313
import { MongoInvalidArgumentError } from './error';
1414

1515
/** @internal */
16-
const AUTH_PROVIDERS = new Map<AuthMechanism | string, (workflow?: Workflow) => AuthProvider>([
17-
[AuthMechanism.MONGODB_AWS, () => new MongoDBAWS()],
16+
const AUTH_PROVIDERS = new Map<
17+
AuthMechanism | string,
18+
(authMechanismProperties: AuthMechanismProperties) => AuthProvider
19+
>([
20+
[
21+
AuthMechanism.MONGODB_AWS,
22+
({ AWS_CREDENTIAL_PROVIDER }) => new MongoDBAWS(AWS_CREDENTIAL_PROVIDER)
23+
],
1824
[
1925
AuthMechanism.MONGODB_CR,
2026
() => {
@@ -24,7 +30,7 @@ const AUTH_PROVIDERS = new Map<AuthMechanism | string, (workflow?: Workflow) =>
2430
}
2531
],
2632
[AuthMechanism.MONGODB_GSSAPI, () => new GSSAPI()],
27-
[AuthMechanism.MONGODB_OIDC, (workflow?: Workflow) => new MongoDBOIDC(workflow)],
33+
[AuthMechanism.MONGODB_OIDC, properties => new MongoDBOIDC(getWorkflow(properties))],
2834
[AuthMechanism.MONGODB_PLAIN, () => new Plain()],
2935
[AuthMechanism.MONGODB_SCRAM_SHA1, () => new ScramSHA1()],
3036
[AuthMechanism.MONGODB_SCRAM_SHA256, () => new ScramSHA256()],
@@ -62,37 +68,28 @@ export class MongoClientAuthProviders {
6268
throw new MongoInvalidArgumentError(`authMechanism ${name} not supported`);
6369
}
6470

65-
let provider;
66-
if (name === AuthMechanism.MONGODB_OIDC) {
67-
provider = providerFunction(this.getWorkflow(authMechanismProperties));
68-
} else {
69-
provider = providerFunction();
70-
}
71-
71+
const provider = providerFunction(authMechanismProperties);
7272
this.existingProviders.set(name, provider);
7373
return provider;
7474
}
75+
}
7576

76-
/**
77-
* Gets either a device workflow or callback workflow.
78-
*/
79-
getWorkflow(authMechanismProperties: AuthMechanismProperties): Workflow {
80-
if (authMechanismProperties.OIDC_HUMAN_CALLBACK) {
81-
return new HumanCallbackWorkflow(
82-
new TokenCache(),
83-
authMechanismProperties.OIDC_HUMAN_CALLBACK
77+
/**
78+
* Gets either a device workflow or callback workflow.
79+
*/
80+
function getWorkflow(authMechanismProperties: AuthMechanismProperties): Workflow {
81+
if (authMechanismProperties.OIDC_HUMAN_CALLBACK) {
82+
return new HumanCallbackWorkflow(new TokenCache(), authMechanismProperties.OIDC_HUMAN_CALLBACK);
83+
} else if (authMechanismProperties.OIDC_CALLBACK) {
84+
return new AutomatedCallbackWorkflow(new TokenCache(), authMechanismProperties.OIDC_CALLBACK);
85+
} else {
86+
const environment = authMechanismProperties.ENVIRONMENT;
87+
const workflow = OIDC_WORKFLOWS.get(environment)?.();
88+
if (!workflow) {
89+
throw new MongoInvalidArgumentError(
90+
`Could not load workflow for environment ${authMechanismProperties.ENVIRONMENT}`
8491
);
85-
} else if (authMechanismProperties.OIDC_CALLBACK) {
86-
return new AutomatedCallbackWorkflow(new TokenCache(), authMechanismProperties.OIDC_CALLBACK);
87-
} else {
88-
const environment = authMechanismProperties.ENVIRONMENT;
89-
const workflow = OIDC_WORKFLOWS.get(environment)?.();
90-
if (!workflow) {
91-
throw new MongoInvalidArgumentError(
92-
`Could not load workflow for environment ${authMechanismProperties.ENVIRONMENT}`
93-
);
94-
}
95-
return workflow;
9692
}
93+
return workflow;
9794
}
9895
}

0 commit comments

Comments
 (0)