Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7cc1156
sigv4 implementation
PavelSafronov Dec 11, 2025
811d453
fix issues and add a test, and a simple way to run it
PavelSafronov Dec 12, 2025
a0ba1ec
added unit tests for the signing logic, added comments about how to c…
PavelSafronov Dec 15, 2025
449d677
Merge branch 'main' into NODE-5393
PavelSafronov Dec 15, 2025
a44f3b4
added test for undefined credentials
PavelSafronov Dec 15, 2025
221044d
pr feedback:
PavelSafronov Dec 17, 2025
72ab61d
removed extraneous integ test and moved its logic into an existing aw…
PavelSafronov Dec 17, 2025
037bcf8
minor fixes
PavelSafronov Dec 17, 2025
fe3c90b
use webcrypto for new code
PavelSafronov Dec 19, 2025
021f9de
use ByteUtils.toHex
PavelSafronov Dec 19, 2025
d7966a3
use ByteUtils.encodeUTF8Into
PavelSafronov Dec 19, 2025
3a2a0ee
pr feedback
PavelSafronov Jan 5, 2026
9178f66
Update src/cmap/auth/aws4.ts
PavelSafronov Jan 6, 2026
5a8380f
pr feedback
PavelSafronov Jan 6, 2026
a3c06e4
minor fix
PavelSafronov Jan 6, 2026
26fecf5
Merge branch 'main' into NODE-5393
PavelSafronov Jan 6, 2026
a625dc5
Merge branch 'main' into NODE-5393
PavelSafronov Jan 7, 2026
2e69f64
removing unnecessary bit of code
PavelSafronov Jan 7, 2026
31f49e7
Merge branch 'main' into NODE-5393
PavelSafronov Jan 7, 2026
59f3e26
update aws4 test
PavelSafronov Jan 7, 2026
41a18ab
Merge branch 'main' into NODE-5393
PavelSafronov Jan 7, 2026
178b90a
pr feedback
PavelSafronov Jan 8, 2026
4e88199
add aws4 as dev dependency and verify our code generates the same sig…
PavelSafronov Jan 8, 2026
a4d722a
make aws4 a dev dependency
PavelSafronov Jan 8, 2026
6fffef6
Merge branch 'main' into NODE-5393
PavelSafronov Jan 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .evergreen/run-mongodb-aws-ecs-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,3 @@ source ./.evergreen/prepare-shell.sh # should not run git clone

# load node.js
source $DRIVERS_TOOLS/.evergreen/init-node-and-npm-env.sh

# run the tests
npm install aws4
2 changes: 0 additions & 2 deletions .evergreen/setup-mongodb-aws-auth-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,5 @@ cd $DRIVERS_TOOLS/.evergreen/auth_aws

cd $BEFORE

npm install --no-save aws4

# revert to show test output
set -x
24 changes: 24 additions & 0 deletions etc/aws-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash

cd $DRIVERS_TOOLS/.evergreen/auth_aws

. ./activate-authawsvenv.sh

# Test with permanent credentials
. aws_setup.sh env-creds
unset MONGODB_URI
echo "AWS_SESSION_TOKEN is set to '${AWS_SESSION_TOKEN-NOT SET}'"
npm run check:test -- --grep "AwsSigV4"

# Test with session credentials
. aws_setup.sh session-creds
unset MONGODB_URI
echo "AWS_SESSION_TOKEN is set to '${AWS_SESSION_TOKEN-NOT SET}'"
npm run check:test -- --grep "AwsSigV4"

# Test with missing credentials
unset MONGODB_URI
unset AWS_ACCESS_KEY_ID
unset AWS_SECRET_ACCESS_KEY
unset AWS_SESSION_TOKEN
npm run check:test -- --grep "AwsSigV4"
141 changes: 141 additions & 0 deletions src/aws4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import * as crypto from 'node:crypto';

export type Options = {
path: '/';
body: string;
host: string;
method: 'POST';
headers: {
'Content-Type': 'application/x-www-form-urlencoded';
'Content-Length': number;
'X-MongoDB-Server-Nonce': string;
'X-MongoDB-GS2-CB-Flag': 'n';
};
service: string;
region: string;
date?: Date;
};

export type AwsSessionCredentials = {
accessKeyId: string;
secretAccessKey: string;
sessionToken: string;
};

export type AwsLongtermCredentials = {
accessKeyId: string;
secretAccessKey: string;
};

export type SignedHeaders = {
headers: {
Authorization: string;
'X-Amz-Date': string;
};
};

export interface AWS4 {
/**
* Created these inline types to better assert future usage of this API
* @param options - options for request
* @param credentials - AWS credential details, sessionToken should be omitted entirely if its false-y
*/
sign(
this: void,
options: Options,
credentials: AwsSessionCredentials | AwsLongtermCredentials | undefined
): SignedHeaders;
}

const getHash = (str: string): string => {
return crypto.createHash('sha256').update(str, 'utf8').digest('hex');
};
const getHmacArray = (key: string | Uint8Array, str: string): Uint8Array => {
return crypto.createHmac('sha256', key).update(str, 'utf8').digest();
};
const getHmacString = (key: Uint8Array, str: string): string => {
return crypto.createHmac('sha256', key).update(str, 'utf8').digest('hex');
};

const getEnvCredentials = () => {
const env = process.env;
return {
accessKeyId: env.AWS_ACCESS_KEY_ID || env.AWS_ACCESS_KEY,
secretAccessKey: env.AWS_SECRET_ACCESS_KEY || env.AWS_SECRET_KEY,
sessionToken: env.AWS_SESSION_TOKEN
};
};

const convertHeaderValue = (value: string | number) => {
return value.toString().trim().replace(/\s+/g, ' ');
};

export function aws4Sign(
this: void,
options: Options,
credentials: AwsSessionCredentials | AwsLongtermCredentials | undefined
): SignedHeaders {
const method = options.method;
const canonicalUri = options.path;
const canonicalQuerystring = '';
const creds = credentials || getEnvCredentials();

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

const headers: string[] = [
`content-length:${convertHeaderValue(options.headers['Content-Length'])}`,
`content-type:${convertHeaderValue(options.headers['Content-Type'])}`,
`host:${convertHeaderValue(options.host)}`,
`x-amz-date:${convertHeaderValue(requestDateTime)}`,
`x-mongodb-gs2-cb-flag:${convertHeaderValue(options.headers['X-MongoDB-GS2-CB-Flag'])}`,
`x-mongodb-server-nonce:${convertHeaderValue(options.headers['X-MongoDB-Server-Nonce'])}`
];
if ('sessionToken' in creds && creds.sessionToken) {
headers.push(`x-amz-security-token:${convertHeaderValue(creds.sessionToken)}`);
}
const canonicalHeaders = headers.sort().join('\n');
const canonicalHeaderNames = headers.map(header => header.split(':', 2)[0].toLowerCase());
const signedHeaders = canonicalHeaderNames.sort().join(';');

const hashedPayload = getHash(options.body || '');

const canonicalRequest = [
method,
canonicalUri,
canonicalQuerystring,
canonicalHeaders + '\n',
signedHeaders,
hashedPayload
].join('\n');

const canonicalRequestHash = getHash(canonicalRequest);
const credentialScope = `${requestDate}/${options.region}/${options.service}/aws4_request`;

const stringToSign = [
'AWS4-HMAC-SHA256',
requestDateTime,
credentialScope,
canonicalRequestHash
].join('\n');

const dateKey = getHmacArray('AWS4' + creds.secretAccessKey, requestDate);
const dateRegionKey = getHmacArray(dateKey, options.region);
const dateRegionServiceKey = getHmacArray(dateRegionKey, options.service);
const signingKey = getHmacArray(dateRegionServiceKey, 'aws4_request');
const signature = getHmacString(signingKey, stringToSign);

const authorizationHeader = [
'AWS4-HMAC-SHA256 Credential=' + creds.accessKeyId + '/' + credentialScope,
'SignedHeaders=' + signedHeaders,
'Signature=' + signature
].join(', ');

return {
headers: {
Authorization: authorizationHeader,
'X-Amz-Date': requestDateTime
}
};
}
13 changes: 4 additions & 9 deletions src/cmap/auth/mongodb_aws.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { aws4Sign } from '../../aws4';
import type { Binary, BSONSerializeOptions } from '../../bson';
import * as BSON from '../../bson';
import { aws4 } from '../../deps';
import {
MongoCompatibilityError,
MongoMissingCredentialsError,
Expand Down Expand Up @@ -45,11 +45,6 @@ export class MongoDBAWS extends AuthProvider {
throw new MongoMissingCredentialsError('AuthContext must provide credentials.');
}

if ('kModuleError' in aws4) {
throw aws4['kModuleError'];
}
const { sign } = aws4;

if (maxWireVersion(connection) < 9) {
throw new MongoCompatibilityError(
'MONGODB-AWS authentication requires MongoDB version 4.4 or later'
Expand Down Expand Up @@ -114,7 +109,7 @@ export class MongoDBAWS extends AuthProvider {
}

const body = 'Action=GetCallerIdentity&Version=2011-06-15';
const options = sign(
const signed = aws4Sign(
{
method: 'POST',
host,
Expand All @@ -133,8 +128,8 @@ export class MongoDBAWS extends AuthProvider {
);

const payload: AWSSaslContinuePayload = {
a: options.headers.Authorization,
d: options.headers['X-Amz-Date']
a: signed.headers.Authorization,
d: signed.headers['X-Amz-Date']
};

if (sessionToken) {
Expand Down
60 changes: 0 additions & 60 deletions src/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,66 +203,6 @@ export function getSocks(): SocksLib | { kModuleError: MongoMissingDependencyErr
}
}

interface AWS4 {
/**
* Created these inline types to better assert future usage of this API
* @param options - options for request
* @param credentials - AWS credential details, sessionToken should be omitted entirely if its false-y
*/
sign(
this: void,
options: {
path: '/';
body: string;
host: string;
method: 'POST';
headers: {
'Content-Type': 'application/x-www-form-urlencoded';
'Content-Length': number;
'X-MongoDB-Server-Nonce': string;
'X-MongoDB-GS2-CB-Flag': 'n';
};
service: string;
region: string;
},
credentials:
| {
accessKeyId: string;
secretAccessKey: string;
sessionToken: string;
}
| {
accessKeyId: string;
secretAccessKey: string;
}
| undefined
): {
headers: {
Authorization: string;
'X-Amz-Date': string;
};
};
}

export const aws4: AWS4 | { kModuleError: MongoMissingDependencyError } = loadAws4();

function loadAws4() {
let aws4: AWS4 | { kModuleError: MongoMissingDependencyError };
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
aws4 = require('aws4');
} catch (error) {
aws4 = makeErrorModule(
new MongoMissingDependencyError(
'Optional module `aws4` not found. Please install it to enable AWS authentication',
{ cause: error, dependencyName: 'aws4' }
)
);
}

return aws4;
}

/** A utility function to get the instance of mongodb-client-encryption, if it exists. */
export function getMongoDBClientEncryption():
| typeof import('mongodb-client-encryption')
Expand Down
Loading