Skip to content

Commit

Permalink
fix(NODE-3487): check for nullish aws mechanism property (#2951)
Browse files Browse the repository at this point in the history
  • Loading branch information
nbbeeken committed Aug 27, 2021
1 parent c9a962f commit 78ec0dd
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 49 deletions.
1 change: 0 additions & 1 deletion package.json
Expand Up @@ -40,7 +40,6 @@
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@microsoft/api-extractor": "^7.18.6",
"@microsoft/tsdoc-config": "^0.15.2",
"@types/aws4": "^1.5.1",
"@types/chai": "^4.2.14",
"@types/chai-subset": "^1.3.3",
"@types/kerberos": "^1.1.0",
Expand Down
17 changes: 14 additions & 3 deletions src/cmap/auth/mongo_credentials.ts
Expand Up @@ -25,14 +25,22 @@ function getDefaultAuthMechanism(ismaster?: Document): AuthMechanism {
return AuthMechanism.MONGODB_CR;
}

/** @public */
export interface AuthMechanismProperties extends Document {
SERVICE_NAME?: string;
SERVICE_REALM?: string;
CANONICALIZE_HOST_NAME?: boolean;
AWS_SESSION_TOKEN?: string;
}

/** @public */
export interface MongoCredentialsOptions {
username: string;
password: string;
source: string;
db?: string;
mechanism?: AuthMechanism;
mechanismProperties: Document;
mechanismProperties: AuthMechanismProperties;
}

/**
Expand All @@ -49,7 +57,7 @@ export class MongoCredentials {
/** The method used to authenticate */
readonly mechanism: AuthMechanism;
/** Special properties used by some types of auth mechanisms */
readonly mechanismProperties: Document;
readonly mechanismProperties: AuthMechanismProperties;

constructor(options: MongoCredentialsOptions) {
this.username = options.username;
Expand All @@ -70,7 +78,10 @@ export class MongoCredentials {
this.password = process.env.AWS_SECRET_ACCESS_KEY;
}

if (!this.mechanismProperties.AWS_SESSION_TOKEN && process.env.AWS_SESSION_TOKEN) {
if (
this.mechanismProperties.AWS_SESSION_TOKEN == null &&
process.env.AWS_SESSION_TOKEN != null
) {
this.mechanismProperties = {
...this.mechanismProperties,
AWS_SESSION_TOKEN: process.env.AWS_SESSION_TOKEN
Expand Down
63 changes: 32 additions & 31 deletions src/cmap/auth/mongodb_aws.ts
Expand Up @@ -14,6 +14,7 @@ import type { BSONSerializeOptions } from '../../bson';

import { aws4 } from '../../deps';
import { AuthMechanism } from './defaultAuthProviders';
import type { Binary } from 'bson';

const ASCII_N = 110;
const AWS_RELATIVE_URI = 'http://169.254.170.2';
Expand Down Expand Up @@ -64,10 +65,19 @@ export class MongoDBAWS extends AuthProvider {
return;
}

const username = credentials.username;
const password = credentials.password;
const accessKeyId = credentials.username;
const secretAccessKey = credentials.password;
const sessionToken = credentials.mechanismProperties.AWS_SESSION_TOKEN;

// If all three defined, include sessionToken, else include username and pass, else no credentials
const awsCredentials =
accessKeyId && secretAccessKey && sessionToken
? { accessKeyId, secretAccessKey, sessionToken }
: accessKeyId && secretAccessKey
? { accessKeyId, secretAccessKey }
: undefined;

const db = credentials.source;
const token = credentials.mechanismProperties.AWS_SESSION_TOKEN;
crypto.randomBytes(32, (err, nonce) => {
if (err) {
callback(err);
Expand All @@ -83,7 +93,10 @@ export class MongoDBAWS extends AuthProvider {
connection.command(ns(`${db}.$cmd`), saslStart, undefined, (err, res) => {
if (err) return callback(err);

const serverResponse = BSON.deserialize(res.payload.buffer, bsonOptions);
const serverResponse = BSON.deserialize(res.payload.buffer, bsonOptions) as {
s: Binary;
h: string;
};
const host = serverResponse.h;
const serverNonce = serverResponse.s.buffer;
if (serverNonce.length !== 64) {
Expand Down Expand Up @@ -123,18 +136,15 @@ export class MongoDBAWS extends AuthProvider {
path: '/',
body
},
{
accessKeyId: username,
secretAccessKey: password,
token
}
awsCredentials
);

const authorization = options.headers.Authorization;
const date = options.headers['X-Amz-Date'];
const payload: AWSSaslContinuePayload = { a: authorization, d: date };
if (token) {
payload.t = token;
const payload: AWSSaslContinuePayload = {
a: options.headers.Authorization,
d: options.headers['X-Amz-Date']
};
if (sessionToken) {
payload.t = sessionToken;
}

const saslContinue = {
Expand All @@ -149,14 +159,16 @@ export class MongoDBAWS extends AuthProvider {
}
}

interface AWSCredentials {
interface AWSTempCredentials {
AccessKeyId?: string;
SecretAccessKey?: string;
Token?: string;
RoleArn?: string;
Expiration?: Date;
}

function makeTempCredentials(credentials: MongoCredentials, callback: Callback<MongoCredentials>) {
function done(creds: AWSCredentials) {
function done(creds: AWSTempCredentials) {
if (!creds.AccessKeyId || !creds.SecretAccessKey || !creds.Token) {
callback(
new MongoMissingCredentialsError('Could not obtain temporary MONGODB-AWS credentials')
Expand All @@ -183,6 +195,7 @@ function makeTempCredentials(credentials: MongoCredentials, callback: Callback<M
if (process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) {
request(
`${AWS_RELATIVE_URI}${process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}`,
undefined,
(err, res) => {
if (err) return callback(err);
done(res);
Expand Down Expand Up @@ -239,27 +252,15 @@ interface RequestOptions {
headers?: http.OutgoingHttpHeaders;
}

function request(uri: string, callback: Callback): void;
function request(uri: string, options: RequestOptions, callback: Callback): void;
function request(uri: string, _options: RequestOptions | Callback, _callback?: Callback) {
let options = _options as RequestOptions;
if ('function' === typeof _options) {
options = {};
}

let callback: Callback = _options as Callback;
if (_callback) {
callback = _callback;
}

options = Object.assign(
function request(uri: string, _options: RequestOptions | undefined, callback: Callback) {
const options = Object.assign(
{
method: 'GET',
timeout: 10000,
json: true
},
url.parse(uri),
options
_options
);

const req = http.request(options, res => {
Expand Down
51 changes: 45 additions & 6 deletions src/deps.ts
Expand Up @@ -109,12 +109,51 @@ try {
saslprep = require('saslprep');
} catch {} // eslint-disable-line

export let aws4: typeof import('aws4') | { kModuleError: MongoMissingDependencyError } =
makeErrorModule(
new MongoMissingDependencyError(
'Optional module `aws4` not found. Please install it to enable AWS authentication'
)
);
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(
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 let aws4: AWS4 | { kModuleError: MongoMissingDependencyError } = makeErrorModule(
new MongoMissingDependencyError(
'Optional module `aws4` not found. Please install it to enable AWS authentication'
)
);

try {
// Ensure you always wrap an optional require in the try block NODE-3199
Expand Down
6 changes: 5 additions & 1 deletion src/index.ts
Expand Up @@ -160,7 +160,11 @@ export type {
OperationTime,
ResumeOptions
} from './change_stream';
export type { MongoCredentials, MongoCredentialsOptions } from './cmap/auth/mongo_credentials';
export type {
MongoCredentials,
AuthMechanismProperties,
MongoCredentialsOptions
} from './cmap/auth/mongo_credentials';
export type {
WriteProtocolMessageType,
Query,
Expand Down
9 changes: 2 additions & 7 deletions src/mongo_client.ts
Expand Up @@ -27,7 +27,7 @@ import type { AuthMechanism } from './cmap/auth/defaultAuthProviders';
import type { Topology, TopologyEvents } from './sdam/topology';
import type { ClientSession, ClientSessionOptions } from './sessions';
import type { TagSet } from './sdam/server_description';
import type { MongoCredentials } from './cmap/auth/mongo_credentials';
import type { AuthMechanismProperties, MongoCredentials } from './cmap/auth/mongo_credentials';
import { parseOptions } from './connection_string';
import type { CompressorName } from './cmap/wire_protocol/compression';
import type { TLSSocketOptions, ConnectionOptions as TLSConnectionOptions } from 'tls';
Expand Down Expand Up @@ -157,12 +157,7 @@ export interface MongoClientOptions extends BSONSerializeOptions, SupportedNodeC
/** Specify the authentication mechanism that MongoDB will use to authenticate the connection. */
authMechanism?: AuthMechanism;
/** Specify properties for the specified authMechanism as a comma-separated list of colon-separated key-value pairs. */
authMechanismProperties?: {
SERVICE_NAME?: string;
CANONICALIZE_HOST_NAME?: boolean;
SERVICE_REALM?: string;
[key: string]: any;
};
authMechanismProperties?: AuthMechanismProperties;
/** The size (in milliseconds) of the latency window for selecting among multiple suitable MongoDB instances. */
localThresholdMS?: number;
/** Specifies how long (in milliseconds) to block for server selection before throwing an exception. */
Expand Down
9 changes: 9 additions & 0 deletions test/functional/mongodb_aws.test.js
Expand Up @@ -40,4 +40,13 @@ describe('MONGODB-AWS', function () {
});
});
});

it('should allow empty string in authMechanismProperties.AWS_SESSION_TOKEN to override AWS_SESSION_TOKEN environment variable', function () {
const client = this.configuration.newClient(this.configuration.url(), {
authMechanismProperties: { AWS_SESSION_TOKEN: '' }
});
expect(client)
.to.have.nested.property('options.credentials.mechanismProperties.AWS_SESSION_TOKEN')
.that.equals('');
});
});

0 comments on commit 78ec0dd

Please sign in to comment.