Skip to content

Commit

Permalink
feat(NODE-4929): Add OIDC Azure workflow (#3670)
Browse files Browse the repository at this point in the history
  • Loading branch information
durran authored and malikj2000 committed Jun 29, 2023
1 parent 0e5917b commit 448148b
Show file tree
Hide file tree
Showing 30 changed files with 763 additions and 105 deletions.
2 changes: 2 additions & 0 deletions .evergreen/ci_matrix_constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const DEFAULT_OS = 'rhel80-large';
const WINDOWS_OS = 'windows-vsCurrent-large';
const MACOS_OS = 'macos-1100';
const UBUNTU_OS = 'ubuntu1804-large';
const UBUNTU_20_OS = 'ubuntu2004-small'
const DEBIAN_OS = 'debian11-small';

module.exports = {
Expand All @@ -32,5 +33,6 @@ module.exports = {
WINDOWS_OS,
MACOS_OS,
UBUNTU_OS,
UBUNTU_20_OS,
DEBIAN_OS
};
42 changes: 42 additions & 0 deletions .evergreen/config.in.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,20 @@ tasks:
args:
- src/.evergreen/run-azure-kms-tests.sh

- name: "oidc-auth-test-azure-latest"
commands:
- func: "install dependencies"
- command: subprocess.exec
params:
working_dir: src
binary: bash
env:
DRIVERS_TOOLS: ${DRIVERS_TOOLS}
PROJECT_DIRECTORY: ${PROJECT_DIRECTORY}
AZUREOIDC_CLIENTID: ${testazureoidc_clientid}
PROVIDER_NAME: azure
args:
- .evergreen/run-oidc-tests-azure.sh

task_groups:
- name: serverless_task_group
Expand Down Expand Up @@ -1348,6 +1362,34 @@ task_groups:
tasks:
- test-azurekms-task

- name: testazureoidc_task_group
setup_group:
- func: fetch source
- command: shell.exec
params:
shell: bash
script: |-
set -o errexit
${PREPARE_SHELL}
export AZUREOIDC_CLIENTID="${testazureoidc_clientid}"
export AZUREOIDC_TENANTID="${testazureoic_tenantid}"
export AZUREOIDC_SECRET="${testazureoidc_secret}"
export AZUREOIDC_KEYVAULT=${testazureoidc_keyvault}
export AZUREOIDC_DRIVERS_TOOLS="$DRIVERS_TOOLS"
export AZUREOIDC_VMNAME_PREFIX="NODE_DRIVER"
$DRIVERS_TOOLS/.evergreen/auth_oidc/azure/create-and-setup-vm.sh
teardown_group:
- command: shell.exec
params:
shell: bash
script: |-
${PREPARE_SHELL}
$DRIVERS_TOOLS/.evergreen/auth_oidc/azure/delete-vm.sh
setup_group_can_fail_task: true
setup_group_timeout_secs: 1800
tasks:
- oidc-auth-test-azure-latest

pre:
- func: "fetch source"
- func: "windows fix"
Expand Down
47 changes: 47 additions & 0 deletions .evergreen/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,20 @@ tasks:
EXPECTED_AZUREKMS_OUTCOME: failure
args:
- src/.evergreen/run-azure-kms-tests.sh
- name: oidc-auth-test-azure-latest
commands:
- func: install dependencies
- command: subprocess.exec
params:
working_dir: src
binary: bash
env:
DRIVERS_TOOLS: ${DRIVERS_TOOLS}
PROJECT_DIRECTORY: ${PROJECT_DIRECTORY}
AZUREOIDC_CLIENTID: ${testazureoidc_clientid}
PROVIDER_NAME: azure
args:
- .evergreen/run-oidc-tests-azure.sh
- name: test-latest-server
tags:
- latest
Expand Down Expand Up @@ -3420,6 +3434,33 @@ task_groups:
- ${DRIVERS_TOOLS}/.evergreen/csfle/azurekms/delete-vm.sh
tasks:
- test-azurekms-task
- name: testazureoidc_task_group
setup_group:
- func: fetch source
- command: shell.exec
params:
shell: bash
script: |-
set -o errexit
${PREPARE_SHELL}
export AZUREOIDC_CLIENTID="${testazureoidc_clientid}"
export AZUREOIDC_TENANTID="${testazureoic_tenantid}"
export AZUREOIDC_SECRET="${testazureoidc_secret}"
export AZUREOIDC_KEYVAULT=${testazureoidc_keyvault}
export AZUREOIDC_DRIVERS_TOOLS="$DRIVERS_TOOLS"
export AZUREOIDC_VMNAME_PREFIX="NODE_DRIVER"
$DRIVERS_TOOLS/.evergreen/auth_oidc/azure/create-and-setup-vm.sh
teardown_group:
- command: shell.exec
params:
shell: bash
script: |-
${PREPARE_SHELL}
$DRIVERS_TOOLS/.evergreen/auth_oidc/azure/delete-vm.sh
setup_group_can_fail_task: true
setup_group_timeout_secs: 1800
tasks:
- oidc-auth-test-azure-latest
pre:
- func: fetch source
- func: windows fix
Expand Down Expand Up @@ -3999,6 +4040,12 @@ buildvariants:
tasks:
- test_azurekms_task_group
- test-azurekms-fail-task
- name: ubuntu20-test-azure-oidc
display_name: Azure OIDC
run_on: ubuntu2004-small
batchtime: 20160
tasks:
- testazureoidc_task_group
- name: rhel8-no-auth-tests
display_name: No Auth Tests
run_on: rhel80-large
Expand Down
9 changes: 9 additions & 0 deletions .evergreen/generate_evergreen_tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const {
WINDOWS_OS,
MACOS_OS,
UBUNTU_OS,
UBUNTU_20_OS,
DEBIAN_OS
} = require('./ci_matrix_constants');

Expand Down Expand Up @@ -754,6 +755,14 @@ BUILD_VARIANTS.push({
tasks: ['test_azurekms_task_group', 'test-azurekms-fail-task']
});

BUILD_VARIANTS.push({
name: 'ubuntu20-test-azure-oidc',
display_name: 'Azure OIDC',
run_on: UBUNTU_20_OS,
batchtime: 20160,
tasks: ['testazureoidc_task_group']
});

BUILD_VARIANTS.push({
name: 'rhel8-no-auth-tests',
display_name: 'No Auth Tests',
Expand Down
11 changes: 11 additions & 0 deletions .evergreen/run-oidc-tests-azure.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/bash
set -o xtrace # Write all commands first to stderr
set -o errexit # Exit the script with error if any of the commands fail

export AZUREOIDC_DRIVERS_TAR_FILE=/tmp/node-mongodb-native.tgz
tar czf $AZUREOIDC_DRIVERS_TAR_FILE .
export AZUREOIDC_TEST_CMD="source ./env.sh && PROVIDER_NAME=azure ./.evergreen/run-oidc-tests.sh"
export AZUREOIDC_CLIENTID=$AZUREOIDC_CLIENTID
export PROJECT_DIRECTORY=$PROJECT_DIRECTORY
export PROVIDER_NAME=$PROVIDER_NAME
bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh
30 changes: 25 additions & 5 deletions .evergreen/run-oidc-tests.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,34 @@
set -o errexit # Exit the script with error if any of the commands fail
set -o xtrace # Write all commands first to stderr

PROVIDER_NAME=${PROVIDER_NAME:-"aws"}
PROJECT_DIRECTORY=${PROJECT_DIRECTORY:-"."}
source "${PROJECT_DIRECTORY}/.evergreen/init-node-and-npm-env.sh"

MONGODB_URI=${MONGODB_URI:-"mongodb://127.0.0.1:27017"}
MONGODB_URI_SINGLE="${MONGODB_URI}/?authMechanism=MONGODB-OIDC&authMechanismProperties=DEVICE_NAME:aws"

echo $MONGODB_URI_SINGLE

export MONGODB_URI="$MONGODB_URI_SINGLE"
export OIDC_TOKEN_DIR=${OIDC_TOKEN_DIR}

npm run check:oidc
export MONGODB_URI=${MONGODB_URI:-"mongodb://localhost"}

if [ "$PROVIDER_NAME" = "aws" ]; then
export MONGODB_URI_SINGLE="${MONGODB_URI}/?authMechanism=MONGODB-OIDC"
export MONGODB_URI_MULTIPLE="${MONGODB_URI}:27018/?authMechanism=MONGODB-OIDC&directConnection=true"

if [ -z "${OIDC_TOKEN_DIR}" ]; then
echo "Must specify OIDC_TOKEN_DIR"
exit 1
fi
npm run check:oidc
elif [ "$PROVIDER_NAME" = "azure" ]; then
if [ -z "${AZUREOIDC_CLIENTID}" ]; then
echo "Must specify an AZUREOIDC_CLIENTID"
exit 1
fi
MONGODB_URI="${MONGODB_URI}/?authMechanism=MONGODB-OIDC"
MONGODB_URI="${MONGODB_URI}&authMechanismProperties=PROVIDER_NAME:azure"
export MONGODB_URI="${MONGODB_URI},TOKEN_AUDIENCE:api%3A%2F%2F${AZUREOIDC_CLIENTID}"
npm run check:oidc-azure
else
npm run check:oidc
fi
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
"check:adl": "mocha --config test/mocha_mongodb.json test/manual/atlas-data-lake-testing",
"check:aws": "nyc mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_aws.test.ts",
"check:oidc": "mocha --config test/mocha_mongodb.json test/manual/mongodb_oidc.prose.test.ts",
"check:oidc-azure": "mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_oidc_azure.prose.test.ts",
"check:ocsp": "mocha --config test/manual/mocharc.json test/manual/ocsp_support.test.js",
"check:kerberos": "nyc mocha --config test/manual/mocharc.json test/manual/kerberos.test.ts",
"check:tls": "mocha --config test/manual/mocharc.json test/manual/tls_support.test.js",
Expand Down
24 changes: 21 additions & 3 deletions src/cmap/auth/mongo_credentials.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Resolves the default auth mechanism according to
// Resolves the default auth mechanism according to
import type { Document } from '../../bson';
import {
MongoAPIError,
MongoAzureError,
MongoInvalidArgumentError,
MongoMissingCredentialsError
} from '../../error';
Expand Down Expand Up @@ -30,6 +32,7 @@ function getDefaultAuthMechanism(hello?: Document): AuthMechanism {
return AuthMechanism.MONGODB_CR;
}

const ALLOWED_PROVIDER_NAMES: AuthMechanismProperties['PROVIDER_NAME'][] = ['aws', 'azure'];
const ALLOWED_HOSTS_ERROR = 'Auth mechanism property ALLOWED_HOSTS must be an array of strings.';

/** @internal */
Expand All @@ -42,6 +45,10 @@ export const DEFAULT_ALLOWED_HOSTS = [
'::1'
];

/** Error for when the token audience is missing in the environment. */
const TOKEN_AUDIENCE_MISSING_ERROR =
'TOKEN_AUDIENCE must be set in the auth mechanism properties when PROVIDER_NAME is azure.';

/** @public */
export interface AuthMechanismProperties extends Document {
SERVICE_HOST?: string;
Expand All @@ -54,9 +61,11 @@ export interface AuthMechanismProperties extends Document {
/** @experimental */
REFRESH_TOKEN_CALLBACK?: OIDCRefreshFunction;
/** @experimental */
PROVIDER_NAME?: 'aws';
PROVIDER_NAME?: 'aws' | 'azure';
/** @experimental */
ALLOWED_HOSTS?: string[];
/** @experimental */
TOKEN_AUDIENCE?: string;
}

/** @public */
Expand Down Expand Up @@ -176,12 +185,21 @@ export class MongoCredentials {
);
}

if (
this.mechanismProperties.PROVIDER_NAME === 'azure' &&
!this.mechanismProperties.TOKEN_AUDIENCE
) {
throw new MongoAzureError(TOKEN_AUDIENCE_MISSING_ERROR);
}

if (
this.mechanismProperties.PROVIDER_NAME &&
this.mechanismProperties.PROVIDER_NAME !== 'aws'
!ALLOWED_PROVIDER_NAMES.includes(this.mechanismProperties.PROVIDER_NAME)
) {
throw new MongoInvalidArgumentError(
`Currently only a PROVIDER_NAME of 'aws' is supported for mechanism '${this.mechanism}'.`
`Currently only a PROVIDER_NAME in ${ALLOWED_PROVIDER_NAMES.join(
','
)} is supported for mechanism '${this.mechanism}'.`
);
}

Expand Down
62 changes: 1 addition & 61 deletions src/cmap/auth/mongodb_aws.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import * as crypto from 'crypto';
import * as http from 'http';
import * as url from 'url';
import { promisify } from 'util';

import type { Binary, BSONSerializeOptions } from '../../bson';
Expand All @@ -12,7 +10,7 @@ import {
MongoMissingCredentialsError,
MongoRuntimeError
} from '../../error';
import { ByteUtils, maxWireVersion, ns } from '../../utils';
import { ByteUtils, maxWireVersion, ns, request } from '../../utils';
import { type AuthContext, AuthProvider } from './auth_provider';
import { MongoCredentials } from './mongo_credentials';
import { AuthMechanism } from './providers';
Expand Down Expand Up @@ -253,61 +251,3 @@ function deriveRegion(host: string) {

return parts[1];
}

interface RequestOptions {
json?: boolean;
method?: string;
timeout?: number;
headers?: http.OutgoingHttpHeaders;
}

async function request(uri: string): Promise<Record<string, any>>;
async function request(
uri: string,
options?: { json?: true } & RequestOptions
): Promise<Record<string, any>>;
async function request(uri: string, options?: { json: false } & RequestOptions): Promise<string>;
async function request(
uri: string,
options: RequestOptions = {}
): Promise<string | Record<string, any>> {
return new Promise<string | Record<string, any>>((resolve, reject) => {
const requestOptions = {
method: 'GET',
timeout: 10000,
json: true,
...url.parse(uri),
...options
};

const req = http.request(requestOptions, res => {
res.setEncoding('utf8');

let data = '';
res.on('data', d => {
data += d;
});

res.once('end', () => {
if (options.json === false) {
resolve(data);
return;
}

try {
const parsed = JSON.parse(data);
resolve(parsed);
} catch {
// TODO(NODE-3483)
reject(new MongoRuntimeError(`Invalid JSON response: "${data}"`));
}
});
});

req.once('timeout', () =>
req.destroy(new MongoAWSError(`AWS request to ${uri} timed out after ${options.timeout} ms`))
);
req.once('error', error => reject(error));
req.end();
});
}
4 changes: 3 additions & 1 deletion src/cmap/auth/mongodb_oidc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { Connection } from '../connection';
import { type AuthContext, AuthProvider } from './auth_provider';
import type { MongoCredentials } from './mongo_credentials';
import { AwsServiceWorkflow } from './mongodb_oidc/aws_service_workflow';
import { AzureServiceWorkflow } from './mongodb_oidc/azure_service_workflow';
import { CallbackWorkflow } from './mongodb_oidc/callback_workflow';

/** Error when credentials are missing. */
Expand Down Expand Up @@ -60,7 +61,7 @@ export type OIDCRefreshFunction = (
context: OIDCCallbackContext
) => Promise<IdPServerResponse>;

type ProviderName = 'aws' | 'callback';
type ProviderName = 'aws' | 'azure' | 'callback';

export interface Workflow {
/**
Expand All @@ -84,6 +85,7 @@ export interface Workflow {
export const OIDC_WORKFLOWS: Map<ProviderName, Workflow> = new Map();
OIDC_WORKFLOWS.set('callback', new CallbackWorkflow());
OIDC_WORKFLOWS.set('aws', new AwsServiceWorkflow());
OIDC_WORKFLOWS.set('azure', new AzureServiceWorkflow());

/**
* OIDC auth provider.
Expand Down

0 comments on commit 448148b

Please sign in to comment.