Skip to content

Commit 811d453

Browse files
committed
fix issues and add a test, and a simple way to run it
1 parent 7cc1156 commit 811d453

4 files changed

Lines changed: 172 additions & 132 deletions

File tree

etc/aws-test.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/usr/bin/env bash
2+
3+
cd $DRIVERS_TOOLS/.evergreen/auth_aws
4+
5+
. ./activate-authawsvenv.sh
6+
7+
# Test with permanent credentials
8+
. aws_setup.sh env-creds
9+
unset MONGODB_URI
10+
echo "AWS_SESSION_TOKEN is set to '${AWS_SESSION_TOKEN-NOT SET}'"
11+
npm run check:test -- --grep "AwsSigV4"
12+
13+
# Test with session credentials
14+
. aws_setup.sh session-creds
15+
unset MONGODB_URI
16+
echo "AWS_SESSION_TOKEN is set to '${AWS_SESSION_TOKEN-NOT SET}'"
17+
npm run check:test -- --grep "AwsSigV4"

src/aws4.ts

Lines changed: 50 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as crypto from 'node:crypto';
2-
import * as queryString from 'node:querystring';
32

43
export interface AWS4 {
54
/**
@@ -42,6 +41,29 @@ export interface AWS4 {
4241
};
4342
}
4443

44+
const getHash = (str: string): string => {
45+
return crypto.createHash('sha256').update(str, 'utf8').digest('hex');
46+
};
47+
const getHmacArray = (key: string | Uint8Array, str: string): Uint8Array => {
48+
return crypto.createHmac('sha256', key).update(str, 'utf8').digest();
49+
};
50+
const getHmacString = (key: Uint8Array, str: string): string => {
51+
return crypto.createHmac('sha256', key).update(str, 'utf8').digest('hex');
52+
};
53+
54+
const getEnvCredentials = () => {
55+
const env = process.env;
56+
return {
57+
accessKeyId: env.AWS_ACCESS_KEY_ID || env.AWS_ACCESS_KEY,
58+
secretAccessKey: env.AWS_SECRET_ACCESS_KEY || env.AWS_SECRET_KEY,
59+
sessionToken: env.AWS_SESSION_TOKEN
60+
};
61+
};
62+
63+
const convertHeaderValue = (value: string | number) => {
64+
return value.toString().trim().replace(/\s+/g, ' ');
65+
};
66+
4567
export function aws4Sign(
4668
this: void,
4769
options: {
@@ -75,118 +97,56 @@ export function aws4Sign(
7597
'X-Amz-Date': string;
7698
};
7799
} {
78-
let path: string;
79-
let query: queryString.ParsedUrlQuery | undefined;
80-
81-
const encode = (str: string) => {
82-
const encoded = encodeURIComponent(str);
83-
const replaced = encoded.replace(/[!'()*]/g, function (c) {
84-
return '%' + c.charCodeAt(0).toString(16).toUpperCase();
85-
});
86-
return replaced;
87-
};
88-
89-
const queryIndex = options.path.indexOf('?');
90-
if (queryIndex < 0) {
91-
path = options.path;
92-
query = undefined;
93-
} else {
94-
path = options.path.slice(0, queryIndex);
95-
query = queryString.parse(options.path.slice(queryIndex + 1));
96-
}
97-
98-
let canonicalQuerystring = '';
99-
if (query) {
100-
const isS3 = options.service === 's3';
101-
const useFirstArrayValue = isS3;
102-
// const decodeSlashesInPath = isS3;
103-
// const decodePath = isS3;
104-
// const normalizePath = !isS3;
105-
const queryStrings: string[] = [];
106-
const sortedQueryKeys = Object.keys(query).sort();
107-
for (const key of sortedQueryKeys) {
108-
if (!key) {
109-
continue;
110-
}
111-
112-
const encodedKey = encode(key);
113-
let value: string | string[] | undefined = query[key];
114-
if (Array.isArray(value)) {
115-
let values: string[] = value;
116-
if (useFirstArrayValue) {
117-
values = [value[0]];
118-
}
100+
const method = options.method;
101+
const canonicalUri = options.path;
102+
const canonicalQuerystring = '';
103+
const creds = credentials || getEnvCredentials();
119104

120-
for (const item of values) {
121-
const encodedValue = encode(item);
122-
queryStrings.push(`${encodedKey}=${encodedValue}`);
123-
}
124-
} else {
125-
value = value ?? '';
126-
const encodedValue = encode(value);
127-
queryStrings.push(`${encodedKey}=${encodedValue}`);
128-
}
129-
}
130-
canonicalQuerystring = queryStrings.join('&');
131-
}
105+
const date = new Date();
106+
const requestDateTime = date.toISOString().replace(/[:-]|\.\d{3}/g, '');
107+
const requestDate = requestDateTime.substring(0, 8);
132108

133-
const convertHeaderValue = (value: string | number) => {
134-
return value.toString().trim().replace(/\s+/g, ' ');
135-
};
136109
const headers: string[] = [
137-
`content-length:${convertHeaderValue(options.headers['Content-Length'])}\n`,
138-
`content-type:${convertHeaderValue(options.headers['Content-Type'])}\n`,
139-
`x-mongodb-gs2-cb-flag:${convertHeaderValue(options.headers['X-MongoDB-GS2-CB-Flag'])}\n`,
140-
`x-mongodb-server-nonce:${convertHeaderValue(options.headers['X-MongoDB-Server-Nonce'])}\n`
110+
`content-length:${convertHeaderValue(options.headers['Content-Length'])}`,
111+
`content-type:${convertHeaderValue(options.headers['Content-Type'])}`,
112+
`host:${convertHeaderValue(options.host)}`,
113+
`x-amz-date:${convertHeaderValue(requestDateTime)}`,
114+
`x-mongodb-gs2-cb-flag:${convertHeaderValue(options.headers['X-MongoDB-GS2-CB-Flag'])}`,
115+
`x-mongodb-server-nonce:${convertHeaderValue(options.headers['X-MongoDB-Server-Nonce'])}`
141116
];
117+
if ('sessionToken' in creds && creds.sessionToken) {
118+
headers.push(`x-amz-security-token:${convertHeaderValue(creds.sessionToken)}`);
119+
}
142120
const canonicalHeaders = headers.sort().join('\n');
121+
const canonicalHeaderNames = headers.map(header => header.split(':', 2)[0].toLowerCase());
122+
const signedHeaders = canonicalHeaderNames.sort().join(';');
143123

144-
const signedHeaders = 'content-length;content-type;x-mongodb-gs2-cb-flag;x-mongodb-server-nonce';
145-
146-
const getHash = (str: string): string => {
147-
return crypto.createHash('sha256').update(str, 'utf8').digest('hex');
148-
};
149-
const getHmac = (key: string, str: string): string => {
150-
return crypto.createHmac('sha256', key).update(str, 'utf8').digest('hex');
151-
};
152124
const hashedPayload = getHash(options.body || '');
153125

154-
const canonicalUri = path;
155126
const canonicalRequest = [
156-
options.method,
127+
method,
157128
canonicalUri,
158129
canonicalQuerystring,
159-
canonicalHeaders,
130+
canonicalHeaders + '\n',
160131
signedHeaders,
161132
hashedPayload
162133
].join('\n');
163134

164-
const canonicRequestHash = getHash(canonicalRequest);
165-
const requestDateTime = new Date().toISOString().replace(/[:-]|\.\d{3}/g, '');
166-
const requestDate = requestDateTime.substring(0, 8);
135+
const canonicalRequestHash = getHash(canonicalRequest);
167136
const credentialScope = `${requestDate}/${options.region}/${options.service}/aws4_request`;
168137

169138
const stringToSign = [
170139
'AWS4-HMAC-SHA256',
171140
requestDateTime,
172141
credentialScope,
173-
canonicRequestHash
142+
canonicalRequestHash
174143
].join('\n');
175144

176-
const getEnvCredentials = () => {
177-
const env = process.env;
178-
return {
179-
accessKeyId: env.AWS_ACCESS_KEY_ID || env.AWS_ACCESS_KEY,
180-
secretAccessKey: env.AWS_SECRET_ACCESS_KEY || env.AWS_SECRET_KEY,
181-
sessionToken: env.AWS_SESSION_TOKEN
182-
};
183-
};
184-
const creds = credentials || getEnvCredentials();
185-
const dateKey = getHmac('AWS4' + creds.secretAccessKey, requestDate);
186-
const dateRegionKey = getHmac(dateKey, options.region);
187-
const dateRegionServiceKey = getHmac(dateRegionKey, options.service);
188-
const signingKey = getHmac(dateRegionServiceKey, 'aws4_request');
189-
const signature = getHmac(signingKey, stringToSign);
145+
const dateKey = getHmacArray('AWS4' + creds.secretAccessKey, requestDate);
146+
const dateRegionKey = getHmacArray(dateKey, options.region);
147+
const dateRegionServiceKey = getHmacArray(dateRegionKey, options.service);
148+
const signingKey = getHmacArray(dateRegionServiceKey, 'aws4_request');
149+
const signature = getHmacString(signingKey, stringToSign);
190150

191151
const authorizationHeader = [
192152
'AWS4-HMAC-SHA256 Credential=' + creds.accessKeyId + '/' + credentialScope,
@@ -201,7 +161,3 @@ export function aws4Sign(
201161
}
202162
};
203163
}
204-
205-
// export const aws4: AWS4 = {
206-
// sign: aws4Sign
207-
// };

test/integration/auth/aws.test.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import * as process from 'node:process';
2+
3+
import { expect } from 'chai';
4+
5+
import { aws4Sign } from '../../../src/aws4';
6+
7+
// This test verifies that our AWS SigV4 signing works correctly with real AWS credentials.
8+
// This is done by calculating a signature, then using it to make a real request to the AWS STS service.
9+
// To run this test, simply run `./etc/aws-test.sh`.
10+
11+
describe('AwsSigV4', function () {
12+
beforeEach(function () {
13+
if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) {
14+
this.skipReason = 'AWS credentials are not present in the environment';
15+
this.skip();
16+
}
17+
});
18+
19+
const testSigning = async credentials => {
20+
const host = 'sts.amazonaws.com';
21+
const body = 'Action=GetCallerIdentity&Version=2011-06-15';
22+
const headers: {
23+
'Content-Type': 'application/x-www-form-urlencoded';
24+
'Content-Length': number;
25+
'X-MongoDB-Server-Nonce': string;
26+
'X-MongoDB-GS2-CB-Flag': 'n';
27+
} = {
28+
'Content-Type': 'application/x-www-form-urlencoded',
29+
'Content-Length': body.length,
30+
'X-MongoDB-Server-Nonce': 'fakenonce',
31+
'X-MongoDB-GS2-CB-Flag': 'n'
32+
};
33+
const options = aws4Sign(
34+
{
35+
method: 'POST',
36+
host,
37+
path: '/',
38+
region: 'us-east-1',
39+
service: 'sts',
40+
headers: headers,
41+
body
42+
},
43+
credentials
44+
);
45+
46+
const authorization = options.headers.Authorization;
47+
const xAmzDate = options.headers['X-Amz-Date'];
48+
49+
const fetchHeaders = new Headers();
50+
for (const [key, value] of Object.entries(headers)) {
51+
fetchHeaders.append(key, value.toString());
52+
}
53+
if (credentials.sessionToken) {
54+
fetchHeaders.append('X-Amz-Security-Token', credentials.sessionToken);
55+
}
56+
fetchHeaders.append('Authorization', authorization);
57+
fetchHeaders.append('X-Amz-Date', xAmzDate);
58+
const response = await fetch('https://sts.amazonaws.com', {
59+
method: 'POST',
60+
headers: fetchHeaders,
61+
body
62+
});
63+
expect(response.status).to.equal(200);
64+
expect(response.statusText).to.equal('OK');
65+
const text = await response.text();
66+
expect(text).to.match(
67+
/<GetCallerIdentityResponse xmlns="https:\/\/sts.amazonaws.com\/doc\/2011-06-15\/">/
68+
);
69+
};
70+
71+
describe('AWS4 signs requests with AWS permanent env vars', function () {
72+
before(function () {
73+
if (process.env.AWS_SESSION_TOKEN) {
74+
this.skipReason = 'Skipping permanent credentials test because session token is set';
75+
this.skip();
76+
}
77+
});
78+
79+
it('AWS4 signs requests with AWS permanent env vars', async () => {
80+
const awsCredentials = {
81+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
82+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
83+
};
84+
await testSigning(awsCredentials);
85+
});
86+
});
87+
88+
describe('AWS4 signs requests with AWS session env vars', function () {
89+
before(function () {
90+
if (!process.env.AWS_SESSION_TOKEN) {
91+
this.skipReason = 'Skipping session credentials test because session token is not set';
92+
this.skip();
93+
}
94+
});
95+
96+
it('AWS4 signs requests with AWS session env vars', async () => {
97+
const awsCredentials = {
98+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
99+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
100+
sessionToken: process.env.AWS_SESSION_TOKEN
101+
};
102+
await testSigning(awsCredentials);
103+
});
104+
});
105+
});

test/integration/auth/mongodb_aws.test.ts

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -38,44 +38,6 @@ describe('MONGODB-AWS', function () {
3838
await client?.close();
3939
});
4040

41-
context('when the AWS SDK is not present', function () {
42-
beforeEach(function () {
43-
AWSSDKCredentialProvider.awsSDK['kModuleError'] = new MongoMissingDependencyError(
44-
'Missing dependency @aws-sdk/credential-providers',
45-
{
46-
cause: new Error(),
47-
dependencyName: '@aws-sdk/credential-providers'
48-
}
49-
);
50-
});
51-
52-
afterEach(function () {
53-
delete AWSSDKCredentialProvider.awsSDK['kModuleError'];
54-
});
55-
56-
describe('when attempting AWS auth', function () {
57-
it('throws an error', async function () {
58-
client = this.configuration.newClient(process.env.MONGODB_URI); // use the URI built by the test environment
59-
60-
const result = await client
61-
.db('aws')
62-
.collection('aws_test')
63-
.estimatedDocumentCount()
64-
.catch(e => e);
65-
66-
// TODO(NODE-7046): Remove branch when removing support for AWS credentials in URI.
67-
// The drivers tools scripts put the credentials in the URI currently for some environments,
68-
// this will need to change when doing the DRIVERS-3131 work.
69-
if (!client.options.credentials.username) {
70-
expect(result).to.be.instanceof(MongoAWSError);
71-
expect(result.message).to.match(/credential-providers/);
72-
} else {
73-
expect(result).to.equal(0);
74-
}
75-
});
76-
});
77-
});
78-
7941
context('when the AWS SDK is present', function () {
8042
it('should authorize when successfully authenticated', async function () {
8143
client = this.configuration.newClient(process.env.MONGODB_URI); // use the URI built by the test environment

0 commit comments

Comments
 (0)