Skip to content

Commit 973a1dd

Browse files
baileympearsonaditi-khare-mongoDB
authored andcommitted
refactor(NODE-6040): extract aws temporary credential acquisition logic into a standalone module (#4050)
1 parent 489f4b3 commit 973a1dd

File tree

5 files changed

+194
-130
lines changed

5 files changed

+194
-130
lines changed
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { type AWSCredentials, getAwsCredentialProvider } from '../../deps';
2+
import { MongoAWSError } from '../../error';
3+
import { request } from '../../utils';
4+
5+
const AWS_RELATIVE_URI = 'http://169.254.170.2';
6+
const AWS_EC2_URI = 'http://169.254.169.254';
7+
const AWS_EC2_PATH = '/latest/meta-data/iam/security-credentials';
8+
9+
/**
10+
* @internal
11+
* This interface matches the final result of fetching temporary credentials manually, outlined
12+
* in the spec [here](https://github.com/mongodb/specifications/blob/master/source/auth/auth.md#ec2-endpoint).
13+
*
14+
* When we use the AWS SDK, we map the response from the SDK to conform to this interface.
15+
*/
16+
export interface AWSTempCredentials {
17+
AccessKeyId?: string;
18+
SecretAccessKey?: string;
19+
Token?: string;
20+
RoleArn?: string;
21+
Expiration?: Date;
22+
}
23+
24+
/**
25+
* @internal
26+
*
27+
* Fetches temporary AWS credentials.
28+
*/
29+
export abstract class AWSTemporaryCredentialProvider {
30+
abstract getCredentials(): Promise<AWSTempCredentials>;
31+
private static _awsSDK: ReturnType<typeof getAwsCredentialProvider>;
32+
protected static get awsSDK() {
33+
AWSTemporaryCredentialProvider._awsSDK ??= getAwsCredentialProvider();
34+
return AWSTemporaryCredentialProvider._awsSDK;
35+
}
36+
37+
static get isAWSSDKInstalled(): boolean {
38+
return !('kModuleError' in AWSTemporaryCredentialProvider.awsSDK);
39+
}
40+
}
41+
42+
/** @internal */
43+
export class AWSSDKCredentialProvider extends AWSTemporaryCredentialProvider {
44+
private _provider?: () => Promise<AWSCredentials>;
45+
/**
46+
* The AWS SDK caches credentials automatically and handles refresh when the credentials have expired.
47+
* To ensure this occurs, we need to cache the `provider` returned by the AWS sdk and re-use it when fetching credentials.
48+
*/
49+
private get provider(): () => Promise<AWSCredentials> {
50+
if ('kModuleError' in AWSTemporaryCredentialProvider.awsSDK) {
51+
throw AWSTemporaryCredentialProvider.awsSDK.kModuleError;
52+
}
53+
if (this._provider) {
54+
return this._provider;
55+
}
56+
let { AWS_STS_REGIONAL_ENDPOINTS = '', AWS_REGION = '' } = process.env;
57+
AWS_STS_REGIONAL_ENDPOINTS = AWS_STS_REGIONAL_ENDPOINTS.toLowerCase();
58+
AWS_REGION = AWS_REGION.toLowerCase();
59+
60+
/** The option setting should work only for users who have explicit settings in their environment, the driver should not encode "defaults" */
61+
const awsRegionSettingsExist =
62+
AWS_REGION.length !== 0 && AWS_STS_REGIONAL_ENDPOINTS.length !== 0;
63+
64+
/**
65+
* The following regions use the global AWS STS endpoint, sts.amazonaws.com, by default
66+
* https://docs.aws.amazon.com/sdkref/latest/guide/feature-sts-regionalized-endpoints.html
67+
*/
68+
const LEGACY_REGIONS = new Set([
69+
'ap-northeast-1',
70+
'ap-south-1',
71+
'ap-southeast-1',
72+
'ap-southeast-2',
73+
'aws-global',
74+
'ca-central-1',
75+
'eu-central-1',
76+
'eu-north-1',
77+
'eu-west-1',
78+
'eu-west-2',
79+
'eu-west-3',
80+
'sa-east-1',
81+
'us-east-1',
82+
'us-east-2',
83+
'us-west-1',
84+
'us-west-2'
85+
]);
86+
/**
87+
* If AWS_STS_REGIONAL_ENDPOINTS is set to regional, users are opting into the new behavior of respecting the region settings
88+
*
89+
* If AWS_STS_REGIONAL_ENDPOINTS is set to legacy, then "old" regions need to keep using the global setting.
90+
* Technically the SDK gets this wrong, it reaches out to 'sts.us-east-1.amazonaws.com' when it should be 'sts.amazonaws.com'.
91+
* That is not our bug to fix here. We leave that up to the SDK.
92+
*/
93+
const useRegionalSts =
94+
AWS_STS_REGIONAL_ENDPOINTS === 'regional' ||
95+
(AWS_STS_REGIONAL_ENDPOINTS === 'legacy' && !LEGACY_REGIONS.has(AWS_REGION));
96+
97+
this._provider =
98+
awsRegionSettingsExist && useRegionalSts
99+
? AWSTemporaryCredentialProvider.awsSDK.fromNodeProviderChain({
100+
clientConfig: { region: AWS_REGION }
101+
})
102+
: AWSTemporaryCredentialProvider.awsSDK.fromNodeProviderChain();
103+
104+
return this._provider;
105+
}
106+
107+
override async getCredentials(): Promise<AWSTempCredentials> {
108+
/*
109+
* Creates a credential provider that will attempt to find credentials from the
110+
* following sources (listed in order of precedence):
111+
*
112+
* - Environment variables exposed via process.env
113+
* - SSO credentials from token cache
114+
* - Web identity token credentials
115+
* - Shared credentials and config ini files
116+
* - The EC2/ECS Instance Metadata Service
117+
*/
118+
try {
119+
const creds = await this.provider();
120+
return {
121+
AccessKeyId: creds.accessKeyId,
122+
SecretAccessKey: creds.secretAccessKey,
123+
Token: creds.sessionToken,
124+
Expiration: creds.expiration
125+
};
126+
} catch (error) {
127+
throw new MongoAWSError(error.message, { cause: error });
128+
}
129+
}
130+
}
131+
132+
/**
133+
* @internal
134+
* Fetches credentials manually (without the AWS SDK), as outlined in the [Obtaining Credentials](https://github.com/mongodb/specifications/blob/master/source/auth/auth.md#obtaining-credentials)
135+
* section of the Auth spec.
136+
*/
137+
export class LegacyAWSTemporaryCredentialProvider extends AWSTemporaryCredentialProvider {
138+
override async getCredentials(): Promise<AWSTempCredentials> {
139+
// If the environment variable AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
140+
// is set then drivers MUST assume that it was set by an AWS ECS agent
141+
if (process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) {
142+
return request(`${AWS_RELATIVE_URI}${process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}`);
143+
}
144+
145+
// Otherwise assume we are on an EC2 instance
146+
147+
// get a token
148+
const token = await request(`${AWS_EC2_URI}/latest/api/token`, {
149+
method: 'PUT',
150+
json: false,
151+
headers: { 'X-aws-ec2-metadata-token-ttl-seconds': 30 }
152+
});
153+
154+
// get role name
155+
const roleName = await request(`${AWS_EC2_URI}/${AWS_EC2_PATH}`, {
156+
json: false,
157+
headers: { 'X-aws-ec2-metadata-token': token }
158+
});
159+
160+
// get temp credentials
161+
const creds = await request(`${AWS_EC2_URI}/${AWS_EC2_PATH}/${roleName}`, {
162+
headers: { 'X-aws-ec2-metadata-token': token }
163+
});
164+
165+
return creds;
166+
}
167+
}

src/cmap/auth/mongodb_aws.ts

Lines changed: 19 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,23 @@
1-
import * as process from 'process';
2-
31
import type { Binary, BSONSerializeOptions } from '../../bson';
42
import * as BSON from '../../bson';
5-
import { aws4, type AWSCredentials, getAwsCredentialProvider } from '../../deps';
3+
import { aws4 } from '../../deps';
64
import {
7-
MongoAWSError,
85
MongoCompatibilityError,
96
MongoMissingCredentialsError,
107
MongoRuntimeError
118
} from '../../error';
12-
import { ByteUtils, maxWireVersion, ns, randomBytes, request } from '../../utils';
9+
import { ByteUtils, maxWireVersion, ns, randomBytes } from '../../utils';
1310
import { type AuthContext, AuthProvider } from './auth_provider';
11+
import {
12+
AWSSDKCredentialProvider,
13+
type AWSTempCredentials,
14+
AWSTemporaryCredentialProvider,
15+
LegacyAWSTemporaryCredentialProvider
16+
} from './aws_temporary_credentials';
1417
import { MongoCredentials } from './mongo_credentials';
1518
import { AuthMechanism } from './providers';
1619

17-
/**
18-
* The following regions use the global AWS STS endpoint, sts.amazonaws.com, by default
19-
* https://docs.aws.amazon.com/sdkref/latest/guide/feature-sts-regionalized-endpoints.html
20-
*/
21-
const LEGACY_REGIONS = new Set([
22-
'ap-northeast-1',
23-
'ap-south-1',
24-
'ap-southeast-1',
25-
'ap-southeast-2',
26-
'aws-global',
27-
'ca-central-1',
28-
'eu-central-1',
29-
'eu-north-1',
30-
'eu-west-1',
31-
'eu-west-2',
32-
'eu-west-3',
33-
'sa-east-1',
34-
'us-east-1',
35-
'us-east-2',
36-
'us-west-1',
37-
'us-west-2'
38-
]);
3920
const ASCII_N = 110;
40-
const AWS_RELATIVE_URI = 'http://169.254.170.2';
41-
const AWS_EC2_URI = 'http://169.254.169.254';
42-
const AWS_EC2_PATH = '/latest/meta-data/iam/security-credentials';
4321
const bsonOptions: BSONSerializeOptions = {
4422
useBigInt64: false,
4523
promoteLongs: true,
@@ -55,40 +33,13 @@ interface AWSSaslContinuePayload {
5533
}
5634

5735
export class MongoDBAWS extends AuthProvider {
58-
static credentialProvider: ReturnType<typeof getAwsCredentialProvider>;
59-
provider?: () => Promise<AWSCredentials>;
60-
36+
private credentialFetcher: AWSTemporaryCredentialProvider;
6137
constructor() {
6238
super();
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));
8339

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-
}
40+
this.credentialFetcher = AWSTemporaryCredentialProvider.isAWSSDKInstalled
41+
? new AWSSDKCredentialProvider()
42+
: new LegacyAWSTemporaryCredentialProvider();
9243
}
9344

9445
override async auth(authContext: AuthContext): Promise<void> {
@@ -109,7 +60,10 @@ export class MongoDBAWS extends AuthProvider {
10960
}
11061

11162
if (!authContext.credentials.username) {
112-
authContext.credentials = await makeTempCredentials(authContext.credentials, this.provider);
63+
authContext.credentials = await makeTempCredentials(
64+
authContext.credentials,
65+
this.credentialFetcher
66+
);
11367
}
11468

11569
const { credentials } = authContext;
@@ -202,17 +156,9 @@ export class MongoDBAWS extends AuthProvider {
202156
}
203157
}
204158

205-
interface AWSTempCredentials {
206-
AccessKeyId?: string;
207-
SecretAccessKey?: string;
208-
Token?: string;
209-
RoleArn?: string;
210-
Expiration?: Date;
211-
}
212-
213159
async function makeTempCredentials(
214160
credentials: MongoCredentials,
215-
provider?: () => Promise<AWSCredentials>
161+
awsCredentialFetcher: AWSTemporaryCredentialProvider
216162
): Promise<MongoCredentials> {
217163
function makeMongoCredentialsFromAWSTemp(creds: AWSTempCredentials) {
218164
// The AWS session token (creds.Token) may or may not be set.
@@ -230,62 +176,9 @@ async function makeTempCredentials(
230176
}
231177
});
232178
}
179+
const temporaryCredentials = await awsCredentialFetcher.getCredentials();
233180

234-
// Check if the AWS credential provider from the SDK is present. If not,
235-
// use the old method.
236-
if (provider && !('kModuleError' in MongoDBAWS.credentialProvider)) {
237-
/*
238-
* Creates a credential provider that will attempt to find credentials from the
239-
* following sources (listed in order of precedence):
240-
*
241-
* - Environment variables exposed via process.env
242-
* - SSO credentials from token cache
243-
* - Web identity token credentials
244-
* - Shared credentials and config ini files
245-
* - The EC2/ECS Instance Metadata Service
246-
*/
247-
try {
248-
const creds = await provider();
249-
return makeMongoCredentialsFromAWSTemp({
250-
AccessKeyId: creds.accessKeyId,
251-
SecretAccessKey: creds.secretAccessKey,
252-
Token: creds.sessionToken,
253-
Expiration: creds.expiration
254-
});
255-
} catch (error) {
256-
throw new MongoAWSError(error.message);
257-
}
258-
} else {
259-
// If the environment variable AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
260-
// is set then drivers MUST assume that it was set by an AWS ECS agent
261-
if (process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) {
262-
return makeMongoCredentialsFromAWSTemp(
263-
await request(`${AWS_RELATIVE_URI}${process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}`)
264-
);
265-
}
266-
267-
// Otherwise assume we are on an EC2 instance
268-
269-
// get a token
270-
const token = await request(`${AWS_EC2_URI}/latest/api/token`, {
271-
method: 'PUT',
272-
json: false,
273-
headers: { 'X-aws-ec2-metadata-token-ttl-seconds': 30 }
274-
});
275-
276-
// get role name
277-
const roleName = await request(`${AWS_EC2_URI}/${AWS_EC2_PATH}`, {
278-
json: false,
279-
headers: { 'X-aws-ec2-metadata-token': token }
280-
});
281-
282-
// get temp credentials
283-
const creds = await request(`${AWS_EC2_URI}/${AWS_EC2_PATH}/${roleName}`, {
284-
headers: { 'X-aws-ec2-metadata-token': token }
285-
});
286-
287-
return makeMongoCredentialsFromAWSTemp(creds);
288-
}
181+
return makeMongoCredentialsFromAWSTemp(temporaryCredentials);
289182
}
290183

291184
function deriveRegion(host: string) {

src/error.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -520,8 +520,8 @@ export class MongoAWSError extends MongoRuntimeError {
520520
*
521521
* @public
522522
**/
523-
constructor(message: string) {
524-
super(message);
523+
constructor(message: string, options?: { cause?: Error }) {
524+
super(message, options);
525525
}
526526

527527
override get name(): string {

0 commit comments

Comments
 (0)