Skip to content

Commit a0ba1ec

Browse files
committed
added unit tests for the signing logic, added comments about how to compare the output with the old aws4 library
1 parent 811d453 commit a0ba1ec

5 files changed

Lines changed: 166 additions & 71 deletions

File tree

etc/aws-test.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,10 @@ npm run check:test -- --grep "AwsSigV4"
1515
unset MONGODB_URI
1616
echo "AWS_SESSION_TOKEN is set to '${AWS_SESSION_TOKEN-NOT SET}'"
1717
npm run check:test -- --grep "AwsSigV4"
18+
19+
# Test with missing credentials
20+
unset MONGODB_URI
21+
unset AWS_ACCESS_KEY_ID
22+
unset AWS_SECRET_ACCESS_KEY
23+
unset AWS_SESSION_TOKEN
24+
npm run check:test -- --grep "AwsSigV4"

src/aws4.ts

Lines changed: 41 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,39 @@
11
import * as crypto from 'node:crypto';
22

3+
export type Options = {
4+
path: '/';
5+
body: string;
6+
host: string;
7+
method: 'POST';
8+
headers: {
9+
'Content-Type': 'application/x-www-form-urlencoded';
10+
'Content-Length': number;
11+
'X-MongoDB-Server-Nonce': string;
12+
'X-MongoDB-GS2-CB-Flag': 'n';
13+
};
14+
service: string;
15+
region: string;
16+
date?: Date;
17+
};
18+
19+
export type AwsSessionCredentials = {
20+
accessKeyId: string;
21+
secretAccessKey: string;
22+
sessionToken: string;
23+
};
24+
25+
export type AwsLongtermCredentials = {
26+
accessKeyId: string;
27+
secretAccessKey: string;
28+
};
29+
30+
export type SignedHeaders = {
31+
headers: {
32+
Authorization: string;
33+
'X-Amz-Date': string;
34+
};
35+
};
36+
337
export interface AWS4 {
438
/**
539
* Created these inline types to better assert future usage of this API
@@ -8,37 +42,9 @@ export interface AWS4 {
842
*/
943
sign(
1044
this: void,
11-
options: {
12-
path: '/';
13-
body: string;
14-
host: string;
15-
method: 'POST';
16-
headers: {
17-
'Content-Type': 'application/x-www-form-urlencoded';
18-
'Content-Length': number;
19-
'X-MongoDB-Server-Nonce': string;
20-
'X-MongoDB-GS2-CB-Flag': 'n';
21-
};
22-
service: string;
23-
region: string;
24-
},
25-
credentials:
26-
| {
27-
accessKeyId: string;
28-
secretAccessKey: string;
29-
sessionToken: string;
30-
}
31-
| {
32-
accessKeyId: string;
33-
secretAccessKey: string;
34-
}
35-
| undefined
36-
): {
37-
headers: {
38-
Authorization: string;
39-
'X-Amz-Date': string;
40-
};
41-
};
45+
options: Options,
46+
credentials: AwsSessionCredentials | AwsLongtermCredentials | undefined
47+
): SignedHeaders;
4248
}
4349

4450
const getHash = (str: string): string => {
@@ -66,43 +72,15 @@ const convertHeaderValue = (value: string | number) => {
6672

6773
export function aws4Sign(
6874
this: void,
69-
options: {
70-
path: '/';
71-
body: string;
72-
host: string;
73-
method: 'POST';
74-
headers: {
75-
'Content-Type': 'application/x-www-form-urlencoded';
76-
'Content-Length': number;
77-
'X-MongoDB-Server-Nonce': string;
78-
'X-MongoDB-GS2-CB-Flag': 'n';
79-
};
80-
service: string;
81-
region: string;
82-
},
83-
credentials:
84-
| {
85-
accessKeyId: string;
86-
secretAccessKey: string;
87-
sessionToken: string;
88-
}
89-
| {
90-
accessKeyId: string;
91-
secretAccessKey: string;
92-
}
93-
| undefined
94-
): {
95-
headers: {
96-
Authorization: string;
97-
'X-Amz-Date': string;
98-
};
99-
} {
75+
options: Options,
76+
credentials: AwsSessionCredentials | AwsLongtermCredentials | undefined
77+
): SignedHeaders {
10078
const method = options.method;
10179
const canonicalUri = options.path;
10280
const canonicalQuerystring = '';
10381
const creds = credentials || getEnvCredentials();
10482

105-
const date = new Date();
83+
const date = options.date || new Date();
10684
const requestDateTime = date.toISOString().replace(/[:-]|\.\d{3}/g, '');
10785
const requestDate = requestDateTime.substring(0, 8);
10886

src/cmap/auth/mongodb_aws.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export class MongoDBAWS extends AuthProvider {
109109
}
110110

111111
const body = 'Action=GetCallerIdentity&Version=2011-06-15';
112-
const options = aws4Sign(
112+
const signed = aws4Sign(
113113
{
114114
method: 'POST',
115115
host,
@@ -128,8 +128,8 @@ export class MongoDBAWS extends AuthProvider {
128128
);
129129

130130
const payload: AWSSaslContinuePayload = {
131-
a: options.headers.Authorization,
132-
d: options.headers['X-Amz-Date']
131+
a: signed.headers.Authorization,
132+
d: signed.headers['X-Amz-Date']
133133
};
134134

135135
if (sessionToken) {
Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ describe('AwsSigV4', function () {
3030
'X-MongoDB-Server-Nonce': 'fakenonce',
3131
'X-MongoDB-GS2-CB-Flag': 'n'
3232
};
33-
const options = aws4Sign(
33+
const signed = aws4Sign(
3434
{
3535
method: 'POST',
3636
host,
@@ -43,8 +43,8 @@ describe('AwsSigV4', function () {
4343
credentials
4444
);
4545

46-
const authorization = options.headers.Authorization;
47-
const xAmzDate = options.headers['X-Amz-Date'];
46+
const authorization = signed.headers.Authorization;
47+
const xAmzDate = signed.headers['X-Amz-Date'];
4848

4949
const fetchHeaders = new Headers();
5050
for (const [key, value] of Object.entries(headers)) {
@@ -68,6 +68,23 @@ describe('AwsSigV4', function () {
6868
);
6969
};
7070

71+
describe('AWS4 signs requests with missing AWS env vars', function () {
72+
before(function () {
73+
if (
74+
process.env.AWS_ACCESS_KEY_ID ||
75+
process.env.AWS_SECRET_ACCESS_KEY ||
76+
process.env.AWS_SESSION_TOKEN
77+
) {
78+
this.skipReason = 'Skipping missing credentials test because AWS credentials are set';
79+
this.skip();
80+
}
81+
});
82+
83+
it('AWS4 signs requests with missing aws env vars', async () => {
84+
await testSigning(undefined);
85+
});
86+
});
87+
7188
describe('AWS4 signs requests with AWS permanent env vars', function () {
7289
before(function () {
7390
if (process.env.AWS_SESSION_TOKEN) {
@@ -94,12 +111,12 @@ describe('AwsSigV4', function () {
94111
});
95112

96113
it('AWS4 signs requests with AWS session env vars', async () => {
97-
const awsCredentials = {
114+
const awsSesssionCredentials = {
98115
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
99116
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
100117
sessionToken: process.env.AWS_SESSION_TOKEN
101118
};
102-
await testSigning(awsCredentials);
119+
await testSigning(awsSesssionCredentials);
103120
});
104121
});
105122
});

test/unit/aws4.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { expect } from 'chai';
2+
3+
import { aws4Sign, type Options } from '../../src/aws4';
4+
5+
describe('Verify AWS4 signature generation', () => {
6+
const date = new Date('2025-12-15T12:34:56Z');
7+
const awsCredentials = {
8+
accessKeyId: 'AKIDEXAMPLE',
9+
secretAccessKey: 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY'
10+
};
11+
const awsSessionCredentials = {
12+
accessKeyId: 'AKIDEXAMPLE',
13+
secretAccessKey: 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYexamplekey',
14+
sessionToken: 'AQoDYXdzEJ'
15+
};
16+
const host = 'sts.amazonaws.com';
17+
const body = 'Action=GetCallerIdentity&Version=2011-06-15';
18+
const request: Options = {
19+
method: 'POST',
20+
host,
21+
path: '/',
22+
region: 'us-east-1',
23+
service: 'sts',
24+
headers: {
25+
'Content-Type': 'application/x-www-form-urlencoded',
26+
'Content-Length': body.length,
27+
'X-MongoDB-Server-Nonce': 'fakenonce',
28+
'X-MongoDB-GS2-CB-Flag': 'n'
29+
},
30+
body,
31+
date
32+
};
33+
34+
it('should generate correct credentials for missing credentials', () => {
35+
const signed = aws4Sign(request, undefined);
36+
37+
expect(signed.headers['X-Amz-Date']).to.exist;
38+
expect(signed.headers['X-Amz-Date']).to.equal('20251215T123456Z');
39+
expect(signed.headers['Authorization']).to.exist;
40+
expect(signed.headers['Authorization']).to.equal(
41+
'AWS4-HMAC-SHA256 Credential=undefined/20251215/us-east-1/sts/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-mongodb-gs2-cb-flag;x-mongodb-server-nonce, Signature=8854aaaeec4bf1f820435b60e216b610e92fa53cbfca71b269f2c334e02c1c45'
42+
);
43+
44+
// Uncomment the following lines if you want to compare with the old aws4 library.
45+
// Remember to import aws4 at the top of the file, like this: import * as aws4sign from 'aws4';
46+
47+
// const oldSigned = aws4sign.sign(request, undefined);
48+
// expect(oldSigned.headers['X-Amz-Date']).to.exist;
49+
// expect(oldSigned.headers['X-Amz-Date']).to.equal(signed.headers['X-Amz-Date']);
50+
// expect(oldSigned.headers['Authorization']).to.exist;
51+
// expect(oldSigned.headers['Authorization']).to.equal(signed.headers['Authorization']);
52+
});
53+
54+
it('should generate correct credentials for permanent credentials', () => {
55+
const signed = aws4Sign(request, awsCredentials);
56+
57+
expect(signed.headers['X-Amz-Date']).to.exist;
58+
expect(signed.headers['X-Amz-Date']).to.equal('20251215T123456Z');
59+
expect(signed.headers['Authorization']).to.exist;
60+
expect(signed.headers['Authorization']).to.equal(
61+
'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20251215/us-east-1/sts/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-mongodb-gs2-cb-flag;x-mongodb-server-nonce, Signature=48a66f9fc76829002a7a7ac5b92e4089395d9b88ea7d417ab146949b90eeab08'
62+
);
63+
64+
// Uncomment the following lines if you want to compare with the old aws4 library.
65+
// Remember to import aws4 at the top of the file, like this: import * as aws4sign from 'aws4';
66+
67+
// const oldSigned = aws4sign.sign(request, awsCredentials);
68+
// expect(oldSigned.headers['X-Amz-Date']).to.exist;
69+
// expect(oldSigned.headers['X-Amz-Date']).to.equal(signed.headers['X-Amz-Date']);
70+
// expect(oldSigned.headers['Authorization']).to.exist;
71+
// expect(oldSigned.headers['Authorization']).to.equal(signed.headers['Authorization']);
72+
});
73+
74+
it('should generate correct credentials for session credentials', () => {
75+
const signed = aws4Sign(request, awsSessionCredentials);
76+
77+
expect(signed.headers['X-Amz-Date']).to.exist;
78+
expect(signed.headers['X-Amz-Date']).to.equal('20251215T123456Z');
79+
expect(signed.headers['Authorization']).to.exist;
80+
expect(signed.headers['Authorization']).to.equal(
81+
'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20251215/us-east-1/sts/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-amz-security-token;x-mongodb-gs2-cb-flag;x-mongodb-server-nonce, Signature=bbcb06e2feb8651dced329789743ba283f92ef1302d34a7398cb1d35808a1a66'
82+
);
83+
84+
// Uncomment the following lines if you want to compare with the old aws4 library.
85+
// Remember to import aws4 at the top of the file, like this: import * as aws4sign from 'aws4';
86+
87+
// const oldSigned = aws4sign.sign(request, awsSessionCredentials);
88+
// expect(oldSigned.headers['X-Amz-Date']).to.exist;
89+
// expect(oldSigned.headers['X-Amz-Date']).to.equal(signed.headers['X-Amz-Date']);
90+
// expect(oldSigned.headers['Authorization']).to.exist;
91+
// expect(oldSigned.headers['Authorization']).to.equal(signed.headers['Authorization']);
92+
});
93+
});

0 commit comments

Comments
 (0)