From 240af4c5059cd53a68d27a27361b589468a9dda8 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Fri, 19 Apr 2024 11:39:19 -0600 Subject: [PATCH 01/25] Add remaining environments (azure, gcp), evergreen testing, API naming updates --- .evergreen/.evg.yml | 149 +++++- .evergreen/run-mongodb-oidc-test.sh | 49 ++ .../main/com/mongodb/ConnectionString.java | 2 +- .../src/main/com/mongodb/MongoCredential.java | 20 +- .../authentication/AzureCredentialHelper.java | 61 ++- .../authentication/CredentialInfo.java | 40 ++ .../authentication/GcpCredentialHelper.java | 16 +- .../connection/OidcAuthenticator.java | 132 +++-- .../auth/legacy/connection-string.json | 93 +++- .../auth/mongodb-oidc-no-retry.json | 41 +- .../com/mongodb/client/unified/Entities.java | 75 +-- .../unified/RunOnRequirementsMatcher.java | 5 +- .../OidcAuthenticationProseTests.java | 490 +++++++++++++----- 13 files changed, 888 insertions(+), 285 deletions(-) create mode 100755 .evergreen/run-mongodb-oidc-test.sh create mode 100644 driver-core/src/main/com/mongodb/internal/authentication/CredentialInfo.java diff --git a/.evergreen/.evg.yml b/.evergreen/.evg.yml index d35c01fd89f..57775871856 100644 --- a/.evergreen/.evg.yml +++ b/.evergreen/.evg.yml @@ -12,9 +12,8 @@ stepback: true # Actual testing tasks are marked with `type: test` command_type: system -# Protect ourself against rogue test case, or curl gone wild, that runs forever -# 12 minutes is the longest we'll ever run -exec_timeout_secs: 3600 # 12 minutes is the longest we'll ever run +# Protect ourselves against rogue test case, or curl gone wild, that runs forever +exec_timeout_secs: 7200 # What to do when evergreen hits the timeout (`post:` tasks are run automatically) timeout: @@ -968,6 +967,58 @@ tasks: - func: "run load-balancer" - func: "run load-balancer tests" + - name: "oidc-auth-test-latest" + commands: + - command: subprocess.exec + type: test + params: + working_dir: "src" + binary: bash + include_expansions_in_env: ["DRIVERS_TOOLS", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"] + args: + - .evergreen/run-mongodb-oidc-test.sh + + - name: "oidc-auth-test-azure-latest" + commands: + - command: shell.exec + params: + shell: bash + env: + JAVA_HOME: ${JAVA_HOME} + script: |- + set -o errexit + ${PREPARE_SHELL} + cd src + git add . + git commit -m "add files" + # uncompressed tar used to allow appending .git folder + export AZUREOIDC_DRIVERS_TAR_FILE=/tmp/mongo-java-driver.tar + git archive -o $AZUREOIDC_DRIVERS_TAR_FILE HEAD + tar -rf $AZUREOIDC_DRIVERS_TAR_FILE .git + export AZUREOIDC_TEST_CMD="OIDC_ENV=azure ./.evergreen/run-mongodb-oidc-test.sh" + bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh + + - name: "oidc-auth-test-gcp-latest" + commands: + - command: shell.exec + params: + shell: bash + script: |- + set -o errexit + ${PREPARE_SHELL} + cd src + git add . + git commit -m "add files" + # uncompressed tar used to allow appending .git folder + export GCPOIDC_DRIVERS_TAR_FILE=/tmp/mongo-java-driver.tar + git archive -o $GCPOIDC_DRIVERS_TAR_FILE HEAD + tar -rf $GCPOIDC_DRIVERS_TAR_FILE .git + # Define the command to run on the VM. + # Ensure that we source the environment file created for us, set up any other variables we need, + # and then run our test suite on the vm. + export GCPOIDC_TEST_CMD="OIDC_ENV=gcp ./.evergreen/run-mongodb-oidc-test.sh" + bash $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/run-driver-test.sh + - name: serverless-test commands: - func: "run serverless" @@ -2065,6 +2116,77 @@ task_groups: tasks: - test-aws-lambda-deployed + - name: testoidc_task_group + setup_group: + - func: fetch source + - func: prepare resources + - func: fix absolute paths + - command: ec2.assume_role + params: + role_arn: ${aws_test_secrets_role} + - command: subprocess.exec + params: + binary: bash + include_expansions_in_env: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"] + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/setup.sh + teardown_task: + - command: subprocess.exec + params: + binary: bash + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/teardown.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + tasks: + - oidc-auth-test-latest + + - name: testazureoidc_task_group + setup_group: + - func: fetch source + - func: prepare resources + - func: fix absolute paths + - command: subprocess.exec + params: + binary: bash + env: + AZUREOIDC_VMNAME_PREFIX: "JAVA_DRIVER" + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/azure/create-and-setup-vm.sh + teardown_task: + - command: subprocess.exec + params: + binary: bash + args: + - ${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 + + - name: testgcpoidc_task_group + setup_group: + - func: fetch source + - func: prepare resources + - func: fix absolute paths + - command: subprocess.exec + params: + binary: bash + env: + GCPOIDC_VMNAME_PREFIX: "JAVA_DRIVER" + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/gcp/setup.sh + teardown_task: + - command: subprocess.exec + params: + binary: bash + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/gcp/teardown.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + tasks: + - oidc-auth-test-gcp-latest + buildvariants: # Test packaging and other release related routines @@ -2216,6 +2338,27 @@ buildvariants: tasks: - name: "test_atlas_task_group_search_indexes" +- name: "oidc-auth-test" + display_name: "OIDC Auth" + run_on: ubuntu2204-small + tasks: + - name: testoidc_task_group + batchtime: 20160 # 14 days + +- name: testazureoidc-variant + display_name: "OIDC Auth Azure" + run_on: ubuntu2204-small + tasks: + - name: testazureoidc_task_group + batchtime: 20160 # 14 days + +- name: testgcpoidc-variant + display_name: "OIDC Auth GCP" + run_on: ubuntu2204-small + tasks: + - name: testgcpoidc_task_group + batchtime: 20160 # 14 days + - matrix_name: "aws-auth-test" matrix_spec: { ssl: "nossl", jdk: ["jdk8", "jdk17", "jdk21"], version: ["4.4", "5.0", "6.0", "7.0", "latest"], os: "ubuntu", aws-credential-provider: "*" } diff --git a/.evergreen/run-mongodb-oidc-test.sh b/.evergreen/run-mongodb-oidc-test.sh new file mode 100755 index 00000000000..101a4754c84 --- /dev/null +++ b/.evergreen/run-mongodb-oidc-test.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +set +x # Disable debug trace +set -eu + +echo "Running MONGODB-OIDC authentication tests" + +OIDC_ENV=${OIDC_ENV:-"test"} + +echo "OIDC_ENV $OIDC_ENV" + +if [ $OIDC_ENV == "test" ]; then + if [ -z "$DRIVERS_TOOLS" ]; then + echo "Must specify DRIVERS_TOOLS" + exit 1 + fi + source ${DRIVERS_TOOLS}/.evergreen/auth_oidc/secrets-export.sh + # java will not need to be installed, but we need to config + RELATIVE_DIR_PATH="$(dirname "${BASH_SOURCE:-$0}")" + source "${RELATIVE_DIR_PATH}/javaConfig.bash" +elif [ $OIDC_ENV == "azure" ]; then + source ./env.sh +elif [ $OIDC_ENV == "gcp" ]; then + source ./secrets-export.sh +else + echo "Unrecognized OIDC_ENV $OIDC_ENV" + exit 1 +fi + + +if ! which java ; then + echo "Installing java..." + sudo apt install openjdk-17-jdk -y + echo "Installed java." +fi + +which java +export OIDC_TESTS_ENABLED=true +export OIDC_ENV="$OIDC_ENV" # read by tests + +# use admin credentials for tests +TO_REPLACE="mongodb://" +REPLACEMENT="mongodb://$OIDC_ADMIN_USER:$OIDC_ADMIN_PWD@" +ADMIN_URI=${MONGODB_URI/$TO_REPLACE/$REPLACEMENT} + +./gradlew -Dorg.mongodb.test.uri="$ADMIN_URI" \ + --stacktrace --debug --info --no-build-cache driver-core:cleanTest \ + driver-sync:test --tests OidcAuthenticationProseTests --tests UnifiedAuthTest \ + driver-reactive-streams:test --tests OidcAuthenticationAsyncProseTests \ diff --git a/driver-core/src/main/com/mongodb/ConnectionString.java b/driver-core/src/main/com/mongodb/ConnectionString.java index 8bb802e9e70..375fa160ab3 100644 --- a/driver-core/src/main/com/mongodb/ConnectionString.java +++ b/driver-core/src/main/com/mongodb/ConnectionString.java @@ -916,7 +916,7 @@ private MongoCredential createCredentials(final Map> option if (credential != null && authMechanismProperties != null) { for (String part : authMechanismProperties.split(",")) { - String[] mechanismPropertyKeyValue = part.split(":"); + String[] mechanismPropertyKeyValue = part.split(":", 2); if (mechanismPropertyKeyValue.length != 2) { throw new IllegalArgumentException(format("The connection string contains invalid authentication properties. " + "'%s' is not a key value pair", part)); diff --git a/driver-core/src/main/com/mongodb/MongoCredential.java b/driver-core/src/main/com/mongodb/MongoCredential.java index 295803e55a4..30003ad43cf 100644 --- a/driver-core/src/main/com/mongodb/MongoCredential.java +++ b/driver-core/src/main/com/mongodb/MongoCredential.java @@ -37,6 +37,7 @@ import static com.mongodb.AuthenticationMechanism.SCRAM_SHA_1; import static com.mongodb.AuthenticationMechanism.SCRAM_SHA_256; import static com.mongodb.assertions.Assertions.notNull; +import static com.mongodb.internal.connection.OidcAuthenticator.OidcValidator.validateCreateOidcCredential; import static com.mongodb.internal.connection.OidcAuthenticator.OidcValidator.validateOidcCredentialConstruction; /** @@ -185,7 +186,7 @@ public final class MongoCredential { public static final String AWS_CREDENTIAL_PROVIDER_KEY = "AWS_CREDENTIAL_PROVIDER"; /** - * The provider name. The value must be a string. + * The environment. The value must be a string. *

* If this is provided, * {@link MongoCredential#OIDC_CALLBACK_KEY} and @@ -195,7 +196,7 @@ public final class MongoCredential { * @see #createOidcCredential(String) * @since 4.10 */ - public static final String PROVIDER_NAME_KEY = "PROVIDER_NAME"; + public static final String ENVIRONMENT_KEY = "ENVIRONMENT"; /** * This callback is invoked when the OIDC-based authenticator requests @@ -204,7 +205,7 @@ public final class MongoCredential { * and a {@linkplain OidcCallbackResult#getRefreshToken() refresh token} * must not be returned by the callback. *

- * If this is provided, {@link MongoCredential#PROVIDER_NAME_KEY} + * If this is provided, {@link MongoCredential#ENVIRONMENT_KEY} * and {@link MongoCredential#OIDC_HUMAN_CALLBACK_KEY} * must not be provided. * @@ -219,7 +220,7 @@ public final class MongoCredential { * from the MongoDB server. The type of the value must be * {@link OidcCallback}. *

- * If this is provided, {@link MongoCredential#PROVIDER_NAME_KEY} + * If this is provided, {@link MongoCredential#ENVIRONMENT_KEY} * and {@link MongoCredential#OIDC_CALLBACK_KEY} * must not be provided. * @@ -253,6 +254,13 @@ public final class MongoCredential { public static final List DEFAULT_ALLOWED_HOSTS = Collections.unmodifiableList(Arrays.asList( "*.mongodb.net", "*.mongodb-qa.net", "*.mongodb-dev.net", "*.mongodbgov.net", "localhost", "127.0.0.1", "::1")); + /** + * The token resource. + * + * @since TODO-OIDC update all + */ + public static final String TOKEN_RESOURCE_KEY = "TOKEN_RESOURCE"; + /** * Creates a MongoCredential instance with an unspecified mechanism. The client will negotiate the best mechanism based on the * version of the server that the client is authenticating to. @@ -408,7 +416,7 @@ public static MongoCredential createAwsCredential(@Nullable final String userNam * @return the credential * @since 4.10 * @see #withMechanismProperty(String, Object) - * @see #PROVIDER_NAME_KEY + * @see #ENVIRONMENT_KEY * @see #OIDC_CALLBACK_KEY * @see #OIDC_HUMAN_CALLBACK_KEY * @see #ALLOWED_HOSTS_KEY @@ -463,6 +471,7 @@ public MongoCredential withMechanism(final AuthenticationMechanism mechanism) { if (mechanism == MONGODB_OIDC) { validateOidcCredentialConstruction(source, mechanismProperties); + validateCreateOidcCredential(password); } if (userName == null && !Arrays.asList(MONGODB_X509, MONGODB_AWS, MONGODB_OIDC).contains(mechanism)) { @@ -697,6 +706,7 @@ public interface IdpInfo { /** * @return Unique client ID for this OIDC client. */ + @Nullable String getClientId(); /** diff --git a/driver-core/src/main/com/mongodb/internal/authentication/AzureCredentialHelper.java b/driver-core/src/main/com/mongodb/internal/authentication/AzureCredentialHelper.java index 7c75e397d2a..6b1c2d21020 100644 --- a/driver-core/src/main/com/mongodb/internal/authentication/AzureCredentialHelper.java +++ b/driver-core/src/main/com/mongodb/internal/authentication/AzureCredentialHelper.java @@ -18,6 +18,7 @@ import com.mongodb.MongoClientException; import com.mongodb.internal.ExpirableValue; +import com.mongodb.lang.Nullable; import org.bson.BsonDocument; import org.bson.BsonString; import org.bson.json.JsonParseException; @@ -55,33 +56,11 @@ public static BsonDocument obtainFromEnvironment() { if (cachedValue.isPresent()) { accessToken = cachedValue.get(); } else { - String endpoint = "http://" + "169.254.169.254:80" - + "/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://vault.azure.net"; - - Map headers = new HashMap<>(); - headers.put("Metadata", "true"); - headers.put("Accept", "application/json"); - long startNanoTime = System.nanoTime(); - BsonDocument responseDocument; - try { - responseDocument = BsonDocument.parse(getHttpContents("GET", endpoint, headers)); - } catch (JsonParseException e) { - throw new MongoClientException("Exception parsing JSON from Azure IMDS metadata response.", e); - } - - if (!responseDocument.isString(ACCESS_TOKEN_FIELD)) { - throw new MongoClientException(String.format( - "The %s field from Azure IMDS metadata response is missing or is not a string", ACCESS_TOKEN_FIELD)); - } - if (!responseDocument.isString(EXPIRES_IN_FIELD)) { - throw new MongoClientException(String.format( - "The %s field from Azure IMDS metadata response is missing or is not a string", EXPIRES_IN_FIELD)); - } - accessToken = responseDocument.getString(ACCESS_TOKEN_FIELD).getValue(); - int expiresInSeconds = Integer.parseInt(responseDocument.getString(EXPIRES_IN_FIELD).getValue()); - cachedAccessToken = ExpirableValue.expirable(accessToken, Duration.ofSeconds(expiresInSeconds).minus(Duration.ofMinutes(1)), - startNanoTime); + CredentialInfo response = fetchAzureCredentialInfo("https://vault.azure.net", null); + accessToken = response.getAccessToken(); + Duration duration = response.getExpiresIn().minus(Duration.ofMinutes(1)); + cachedAccessToken = ExpirableValue.expirable(accessToken, duration, startNanoTime); } } finally { CACHED_ACCESS_TOKEN_LOCK.unlock(); @@ -90,6 +69,36 @@ public static BsonDocument obtainFromEnvironment() { return new BsonDocument("accessToken", new BsonString(accessToken)); } + public static CredentialInfo fetchAzureCredentialInfo(final String resource, @Nullable final String objectId) { + String endpoint = "http://169.254.169.254:80" + + "/metadata/identity/oauth2/token?api-version=2018-02-01" + + "&resource=" + resource + + (objectId == null ? "" : "&object_id=" + objectId); + + Map headers = new HashMap<>(); + headers.put("Metadata", "true"); + headers.put("Accept", "application/json"); + + BsonDocument responseDocument; + try { + responseDocument = BsonDocument.parse(getHttpContents("GET", endpoint, headers)); + } catch (JsonParseException e) { + throw new MongoClientException("Exception parsing JSON from Azure IMDS metadata response.", e); + } + + if (!responseDocument.isString(ACCESS_TOKEN_FIELD)) { + throw new MongoClientException(String.format( + "The %s field from Azure IMDS metadata response is missing or is not a string", ACCESS_TOKEN_FIELD)); + } + if (!responseDocument.isString(EXPIRES_IN_FIELD)) { + throw new MongoClientException(String.format( + "The %s field from Azure IMDS metadata response is missing or is not a string", EXPIRES_IN_FIELD)); + } + String accessToken = responseDocument.getString(ACCESS_TOKEN_FIELD).getValue(); + int expiresInSeconds = Integer.parseInt(responseDocument.getString(EXPIRES_IN_FIELD).getValue()); + return new CredentialInfo(accessToken, Duration.ofSeconds(expiresInSeconds)); + } + private AzureCredentialHelper() { } } diff --git a/driver-core/src/main/com/mongodb/internal/authentication/CredentialInfo.java b/driver-core/src/main/com/mongodb/internal/authentication/CredentialInfo.java new file mode 100644 index 00000000000..d4b45f557e3 --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/authentication/CredentialInfo.java @@ -0,0 +1,40 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.internal.authentication; + +import java.time.Duration; + +/** + *

This class is not part of the public API and may be removed or changed at any time

+ */ +public final class CredentialInfo { + private final String accessToken; + private final Duration expiresIn; + + public CredentialInfo(final String accessToken, final Duration expiresIn) { + this.accessToken = accessToken; + this.expiresIn = expiresIn; + } + + public String getAccessToken() { + return accessToken; + } + + public Duration getExpiresIn() { + return expiresIn; + } +} diff --git a/driver-core/src/main/com/mongodb/internal/authentication/GcpCredentialHelper.java b/driver-core/src/main/com/mongodb/internal/authentication/GcpCredentialHelper.java index 92b3fdd6040..9d9f1983c5e 100644 --- a/driver-core/src/main/com/mongodb/internal/authentication/GcpCredentialHelper.java +++ b/driver-core/src/main/com/mongodb/internal/authentication/GcpCredentialHelper.java @@ -18,7 +18,9 @@ import com.mongodb.MongoClientException; import org.bson.BsonDocument; +import org.bson.BsonString; +import java.time.Duration; import java.util.HashMap; import java.util.Map; @@ -30,15 +32,27 @@ *

This class is not part of the public API and may be removed or changed at any time

*/ public final class GcpCredentialHelper { + + public static CredentialInfo fetchGcpCredentialInfo(final String resource) { + String endpoint = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?" + resource; + return new CredentialInfo( + getBsonDocument(endpoint).getValue(), + Duration.ZERO); + } + public static BsonDocument obtainFromEnvironment() { String endpoint = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"; + return new BsonDocument("accessToken", getBsonDocument(endpoint)); + } + private static BsonString getBsonDocument(final String endpoint) { Map header = new HashMap<>(); header.put("Metadata-Flavor", "Google"); + header.put("Accept", "application/json"); String response = getHttpContents("GET", endpoint, header); BsonDocument responseDocument = BsonDocument.parse(response); if (responseDocument.containsKey("access_token")) { - return new BsonDocument("accessToken", responseDocument.get("access_token")); + return responseDocument.get("access_token").asString(); } else { throw new MongoClientException("access_token is missing from GCE metadata response. Full response is ''" + response); } diff --git a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java index 6b2362cbc1f..ec2ec7855bb 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java +++ b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java @@ -31,6 +31,9 @@ import com.mongodb.internal.Locks; import com.mongodb.internal.VisibleForTesting; import com.mongodb.internal.async.SingleResultCallback; +import com.mongodb.internal.authentication.AzureCredentialHelper; +import com.mongodb.internal.authentication.CredentialInfo; +import com.mongodb.internal.authentication.GcpCredentialHelper; import com.mongodb.lang.Nullable; import org.bson.BsonDocument; import org.bson.BsonString; @@ -50,12 +53,13 @@ import static com.mongodb.AuthenticationMechanism.MONGODB_OIDC; import static com.mongodb.MongoCredential.ALLOWED_HOSTS_KEY; +import static com.mongodb.MongoCredential.TOKEN_RESOURCE_KEY; import static com.mongodb.MongoCredential.DEFAULT_ALLOWED_HOSTS; import static com.mongodb.MongoCredential.IdpInfo; import static com.mongodb.MongoCredential.OIDC_HUMAN_CALLBACK_KEY; import static com.mongodb.MongoCredential.OidcCallback; import static com.mongodb.MongoCredential.OidcCallbackContext; -import static com.mongodb.MongoCredential.PROVIDER_NAME_KEY; +import static com.mongodb.MongoCredential.ENVIRONMENT_KEY; import static com.mongodb.MongoCredential.OIDC_CALLBACK_KEY; import static com.mongodb.assertions.Assertions.assertFalse; import static com.mongodb.assertions.Assertions.assertNotNull; @@ -69,11 +73,18 @@ */ public final class OidcAuthenticator extends SaslAuthenticator { - private static final List SUPPORTED_PROVIDERS = Arrays.asList("aws"); + private static final String TEST_ENVIRONMENT = "test"; + private static final String AZURE_ENVIRONMENT = "azure"; + private static final String GCP_ENVIRONMENT = "gcp"; + private static final List SUPPORTED_ENVIRONMENTS = Arrays.asList( + AZURE_ENVIRONMENT, GCP_ENVIRONMENT, TEST_ENVIRONMENT); + private static final List SUPPORTS_TOKEN_RESOURCE = Arrays.asList( + AZURE_ENVIRONMENT, GCP_ENVIRONMENT); private static final Duration CALLBACK_TIMEOUT = Duration.ofMinutes(5); - public static final String AWS_WEB_IDENTITY_TOKEN_FILE = "AWS_WEB_IDENTITY_TOKEN_FILE"; + public static final String OIDC_TOKEN_FILE = "OIDC_TOKEN_FILE"; + private static final int CALLBACK_API_VERSION_NUMBER = 1; @Nullable @@ -113,9 +124,6 @@ protected SaslClient createSaslClient(final ServerAddress serverAddress) { @Nullable public BsonDocument createSpeculativeAuthenticateCommand(final InternalConnection connection) { try { - if (isAutomaticAuthentication()) { - return wrapInSpeculative(prepareAwsTokenFromFileAsJwt()); - } String cachedAccessToken = getCachedAccessToken(); if (cachedAccessToken != null) { return wrapInSpeculative(prepareTokenAsJwt(cachedAccessToken)); @@ -152,11 +160,8 @@ public void setSpeculativeAuthenticateResponse(@Nullable final BsonDocument resp speculativeAuthenticateResponse = response; } - private boolean isAutomaticAuthentication() { - return getOidcCallbackMechanismProperty(PROVIDER_NAME_KEY) == null; - } - private boolean isHumanCallback() { + // built-in providers (aws, azure...) are considered machine callbacks return getOidcCallbackMechanismProperty(OIDC_HUMAN_CALLBACK_KEY) != null; } @@ -167,10 +172,47 @@ private OidcCallback getOidcCallbackMechanismProperty(final String key) { .getMechanismProperty(key, null); } - @Nullable private OidcCallback getRequestCallback() { - OidcCallback machine = getOidcCallbackMechanismProperty(OIDC_CALLBACK_KEY); - return machine != null ? machine : getOidcCallbackMechanismProperty(OIDC_HUMAN_CALLBACK_KEY); + String environment = getEnvironmentName(getMongoCredential()); + OidcCallback machine; + if (TEST_ENVIRONMENT.equals(environment)) { + machine = getTestCallback(); + } else if (AZURE_ENVIRONMENT.equals(environment)) { + machine = getAzureCallback(getMongoCredential()); + } else if (GCP_ENVIRONMENT.equals(environment)) { + machine = getGcpCallback(getMongoCredential()); + } else { + machine = getOidcCallbackMechanismProperty(OIDC_CALLBACK_KEY); + } + OidcCallback human = getOidcCallbackMechanismProperty(OIDC_HUMAN_CALLBACK_KEY); + return machine != null ? machine : assertNotNull(human); + } + + @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) + public static OidcCallback getTestCallback() { + return (context) -> { + String accessToken = readTestTokenFromFile(); + return new OidcCallbackResult(accessToken, Duration.ZERO); + }; + } + + @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) + public static OidcCallback getAzureCallback(final MongoCredential credential) { + return (context) -> { + String resource = assertNotNull(credential.getMechanismProperty(TOKEN_RESOURCE_KEY, null)); + String objectId = credential.getUserName(); + CredentialInfo response = AzureCredentialHelper.fetchAzureCredentialInfo(resource, objectId); + return new OidcCallbackResult(response.getAccessToken(), response.getExpiresIn()); + }; + } + + @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) + public static OidcCallback getGcpCallback(final MongoCredential credential) { + return (context) -> { + String resource = assertNotNull(credential.getMechanismProperty(TOKEN_RESOURCE_KEY, null)); + CredentialInfo response = GcpCredentialHelper.fetchGcpCredentialInfo(resource); + return new OidcCallbackResult(response.getAccessToken(), response.getExpiresIn()); + }; } @Override @@ -239,16 +281,13 @@ private void authenticationLoopAsync(final InternalConnection connection, final } private byte[] evaluate(final byte[] challenge) { - if (isAutomaticAuthentication()) { - return prepareAwsTokenFromFileAsJwt(); - } byte[][] jwt = new byte[1][]; Locks.withLock(getMongoCredentialWithCache().getOidcLock(), () -> { OidcCacheEntry oidcCacheEntry = getMongoCredentialWithCache().getOidcCacheEntry(); String cachedRefreshToken = oidcCacheEntry.getRefreshToken(); IdpInfo cachedIdpInfo = oidcCacheEntry.getIdpInfo(); String cachedAccessToken = validatedCachedAccessToken(); - OidcCallback requestCallback = assertNotNull(getRequestCallback()); + OidcCallback requestCallback = getRequestCallback(); boolean isHuman = isHumanCallback(); if (cachedAccessToken != null) { @@ -443,18 +482,18 @@ public boolean isComplete() { } - private static String readAwsTokenFromFile() { - String path = System.getenv(AWS_WEB_IDENTITY_TOKEN_FILE); + private static String readTestTokenFromFile() { + String path = System.getenv(OIDC_TOKEN_FILE); if (path == null) { throw new MongoClientException( - format("Environment variable must be specified: %s", AWS_WEB_IDENTITY_TOKEN_FILE)); + format("Environment variable must be specified: %s", OIDC_TOKEN_FILE)); } try { return new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8); } catch (IOException e) { throw new MongoClientException(format( "Could not read file specified by environment variable: %s at path: %s", - AWS_WEB_IDENTITY_TOKEN_FILE, path), e); + OIDC_TOKEN_FILE, path), e); } } @@ -483,14 +522,13 @@ private IdpInfo toIdpInfo(final byte[] challenge) { validateAllowedHosts(getMongoCredential()); BsonDocument c = new RawBsonDocument(challenge); String issuer = c.getString("issuer").getValue(); - String clientId = c.getString("clientId").getValue(); + String clientId = !c.containsKey("clientId") ? null : c.getString("clientId").getValue(); return new IdpInfoImpl( issuer, clientId, getStringArray(c, "requestScopes")); } - @Nullable private static List getStringArray(final BsonDocument document, final String key) { if (!document.isArray(key)) { @@ -529,11 +567,6 @@ private byte[] prepareTokenAsJwt(final String accessToken) { return toJwtDocument(accessToken); } - private static byte[] prepareAwsTokenFromFileAsJwt() { - String accessToken = readAwsTokenFromFile(); - return toJwtDocument(accessToken); - } - private static byte[] toJwtDocument(final String accessToken) { return toBson(new BsonDocument().append("jwt", new BsonString(accessToken))); } @@ -553,10 +586,10 @@ public static void validateOidcCredentialConstruction( throw new IllegalArgumentException("source must be '$external'"); } - Object providerName = mechanismProperties.get(PROVIDER_NAME_KEY.toLowerCase()); + String providerName = getEnvironmentName(mechanismProperties); if (providerName != null) { - if (!(providerName instanceof String) || !SUPPORTED_PROVIDERS.contains(providerName)) { - throw new IllegalArgumentException(PROVIDER_NAME_KEY + " must be one of: " + SUPPORTED_PROVIDERS); + if (!SUPPORTED_ENVIRONMENTS.contains(providerName)) { + throw new IllegalArgumentException(ENVIRONMENT_KEY + " must be one of: " + SUPPORTED_ENVIRONMENTS); } } } @@ -571,13 +604,13 @@ public static void validateCreateOidcCredential(@Nullable final char[] password) @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) public static void validateBeforeUse(final MongoCredential credential) { String userName = credential.getUserName(); - Object providerName = credential.getMechanismProperty(PROVIDER_NAME_KEY, null); + Object providerName = credential.getMechanismProperty(ENVIRONMENT_KEY, null); Object machineCallback = credential.getMechanismProperty(OIDC_CALLBACK_KEY, null); Object humanCallback = credential.getMechanismProperty(OIDC_HUMAN_CALLBACK_KEY, null); if (providerName == null) { // callback if (machineCallback == null && humanCallback == null) { - throw new IllegalArgumentException("Either " + PROVIDER_NAME_KEY + throw new IllegalArgumentException("Either " + ENVIRONMENT_KEY + " or " + OIDC_CALLBACK_KEY + " or " + OIDC_HUMAN_CALLBACK_KEY + " must be specified"); @@ -589,18 +622,39 @@ public static void validateBeforeUse(final MongoCredential credential) { } } else { if (userName != null) { - throw new IllegalArgumentException("user name must not be specified when " + PROVIDER_NAME_KEY + " is specified"); + throw new IllegalArgumentException("user name must not be specified when " + ENVIRONMENT_KEY + " is specified"); } if (machineCallback != null) { - throw new IllegalArgumentException(OIDC_CALLBACK_KEY + " must not be specified when " + PROVIDER_NAME_KEY + " is specified"); + throw new IllegalArgumentException(OIDC_CALLBACK_KEY + " must not be specified when " + ENVIRONMENT_KEY + " is specified"); } if (humanCallback != null) { - throw new IllegalArgumentException(OIDC_HUMAN_CALLBACK_KEY + " must not be specified when " + PROVIDER_NAME_KEY + " is specified"); + throw new IllegalArgumentException(OIDC_HUMAN_CALLBACK_KEY + " must not be specified when " + ENVIRONMENT_KEY + " is specified"); + } + String tokenResource = credential.getMechanismProperty(TOKEN_RESOURCE_KEY, null); + boolean hasTokenResourceProperty = tokenResource != null; + boolean tokenResourceSupported = SUPPORTS_TOKEN_RESOURCE.contains(providerName); + if (hasTokenResourceProperty != tokenResourceSupported) { + throw new IllegalArgumentException(TOKEN_RESOURCE_KEY + + " must be provided if and only if " + ENVIRONMENT_KEY + + " " + providerName + " " + + " is one of: " + SUPPORTS_TOKEN_RESOURCE + + ". " + TOKEN_RESOURCE_KEY + ": " + tokenResource); } } } } + @Nullable + private static String getEnvironmentName(final Map mechanismProperties) { + Object o = mechanismProperties.get(ENVIRONMENT_KEY.toLowerCase()); + return o instanceof String ? (String) o : null; + } + + @Nullable + private static String getEnvironmentName(final MongoCredential credential) { + Object o = credential.getMechanismProperty(ENVIRONMENT_KEY, null); + return o instanceof String ? (String) o : null; + } @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) static class OidcCallbackContextImpl implements OidcCallbackContext { @@ -646,12 +700,13 @@ public String getRefreshToken() { @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) static final class IdpInfoImpl implements IdpInfo { private final String issuer; + @Nullable private final String clientId; private final List requestScopes; - IdpInfoImpl(final String issuer, final String clientId, @Nullable final List requestScopes) { + IdpInfoImpl(final String issuer, @Nullable final String clientId, @Nullable final List requestScopes) { this.issuer = assertNotNull(issuer); - this.clientId = assertNotNull(clientId); + this.clientId = clientId; this.requestScopes = requestScopes == null ? Collections.emptyList() : Collections.unmodifiableList(requestScopes); @@ -663,6 +718,7 @@ public String getIssuer() { } @Override + @Nullable public String getClientId() { return clientId; } diff --git a/driver-core/src/test/resources/auth/legacy/connection-string.json b/driver-core/src/test/resources/auth/legacy/connection-string.json index f8521be9d19..d4baa89b6eb 100644 --- a/driver-core/src/test/resources/auth/legacy/connection-string.json +++ b/driver-core/src/test/resources/auth/legacy/connection-string.json @@ -446,8 +446,8 @@ } }, { - "description": "should recognise the mechanism with aws provider (MONGODB-OIDC)", - "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws", + "description": "should recognise the mechanism with test environment (MONGODB-OIDC)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test", "valid": true, "credential": { "username": null, @@ -455,13 +455,13 @@ "source": "$external", "mechanism": "MONGODB-OIDC", "mechanism_properties": { - "PROVIDER_NAME": "aws" + "ENVIRONMENT": "test" } } }, { - "description": "should recognise the mechanism when auth source is explicitly specified and with provider (MONGODB-OIDC)", - "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external&authMechanismProperties=PROVIDER_NAME:aws", + "description": "should recognise the mechanism when auth source is explicitly specified and with environment (MONGODB-OIDC)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external&authMechanismProperties=ENVIRONMENT:test", "valid": true, "credential": { "username": null, @@ -469,30 +469,30 @@ "source": "$external", "mechanism": "MONGODB-OIDC", "mechanism_properties": { - "PROVIDER_NAME": "aws" + "ENVIRONMENT": "test" } } }, { "description": "should throw an exception if supplied a password (MONGODB-OIDC)", - "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws", + "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test", "valid": false, "credential": null }, { - "description": "should throw an exception if username is specified for aws (MONGODB-OIDC)", - "uri": "mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&PROVIDER_NAME:aws", + "description": "should throw an exception if username is specified for test (MONGODB-OIDC)", + "uri": "mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&ENVIRONMENT:test", "valid": false, "credential": null }, { - "description": "should throw an exception if specified provider is not supported (MONGODB-OIDC)", - "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:invalid", + "description": "should throw an exception if specified environment is not supported (MONGODB-OIDC)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:invalid", "valid": false, "credential": null }, { - "description": "should throw an exception if neither provider nor callbacks specified (MONGODB-OIDC)", + "description": "should throw an exception if neither environment nor callbacks specified (MONGODB-OIDC)", "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC", "valid": false, "credential": null @@ -502,6 +502,75 @@ "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=UnsupportedProperty:unexisted", "valid": false, "credential": null + }, + { + "description": "should recognise the mechanism with azure provider (MONGODB-OIDC)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo", + "valid": true, + "credential": { + "username": null, + "password": null, + "source": "$external", + "mechanism": "MONGODB-OIDC", + "mechanism_properties": { + "ENVIRONMENT": "azure", + "TOKEN_RESOURCE": "foo" + } + } + }, + { + "description": "should accept a username with azure provider (MONGODB-OIDC)", + "uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo", + "valid": true, + "credential": { + "username": "user", + "password": null, + "source": "$external", + "mechanism": "MONGODB-OIDC", + "mechanism_properties": { + "ENVIRONMENT": "azure", + "TOKEN_RESOURCE": "foo" + } + } + }, + { + "description": "should accept a username and throw an error for a password with azure provider (MONGODB-OIDC)", + "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo", + "valid": false, + "credential": null + }, + { + "description": "should throw an exception if no token audience is given for azure provider (MONGODB-OIDC)", + "uri": "mongodb://username@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure", + "valid": false, + "credential": null + }, + { + "description": "should recognise the mechanism with gcp provider (MONGODB-OIDC)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:foo", + "valid": true, + "credential": { + "username": null, + "password": null, + "source": "$external", + "mechanism": "MONGODB-OIDC", + "mechanism_properties": { + "ENVIRONMENT": "gcp", + "TOKEN_RESOURCE": "foo" + } + } + }, + { + "description": "should throw an error for a username and password with gcp provider (MONGODB-OIDC)", + "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:foo", + "valid": false, + "credential": null + }, + { + "description": "should throw an error if not TOKEN_RESOURCE with gcp provider (MONGODB-OIDC)", + "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp", + "valid": false, + "credential": null } ] } diff --git a/driver-core/src/test/resources/unified-test-format/auth/mongodb-oidc-no-retry.json b/driver-core/src/test/resources/unified-test-format/auth/mongodb-oidc-no-retry.json index 7287c2486f0..83065f492ae 100644 --- a/driver-core/src/test/resources/unified-test-format/auth/mongodb-oidc-no-retry.json +++ b/driver-core/src/test/resources/unified-test-format/auth/mongodb-oidc-no-retry.json @@ -52,9 +52,7 @@ { "collectionName": "collName", "databaseName": "test", - "documents": [ - - ] + "documents": [] } ], "tests": [ @@ -65,12 +63,9 @@ "name": "find", "object": "collection0", "arguments": { - "filter": { - } + "filter": {} }, - "expectResult": [ - - ] + "expectResult": [] } ], "expectEvents": [ @@ -81,8 +76,7 @@ "commandStartedEvent": { "command": { "find": "collName", - "filter": { - } + "filter": {} } } }, @@ -161,12 +155,9 @@ "name": "find", "object": "collection0", "arguments": { - "filter": { - } + "filter": {} }, - "expectResult": [ - - ] + "expectResult": [] } ], "expectEvents": [ @@ -177,8 +168,7 @@ "commandStartedEvent": { "command": { "find": "collName", - "filter": { - } + "filter": {} } } }, @@ -191,8 +181,7 @@ "commandStartedEvent": { "command": { "find": "collName", - "filter": { - } + "filter": {} } } }, @@ -324,12 +313,14 @@ "client": "failPointClient", "failPoint": { "configureFailPoint": "failCommand", - "mode": "alwaysOn", + "mode": { + "times": 1 + }, "data": { "failCommands": [ "saslStart" ], - "errorCode": 20 + "errorCode": 18 } } } @@ -399,12 +390,14 @@ "client": "failPointClient", "failPoint": { "configureFailPoint": "failCommand", - "mode": "alwaysOn", + "mode": { + "times": 1 + }, "data": { "failCommands": [ "saslStart" ], - "errorCode": 20 + "errorCode": 18 } } } @@ -419,7 +412,7 @@ } }, "expectError": { - "errorCode": 20 + "errorCode": 18 } } ] diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java index 7e83f802279..a535f00e5ec 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java @@ -16,9 +16,9 @@ package com.mongodb.client.unified; -import com.mongodb.AuthenticationMechanism; import com.mongodb.ClientEncryptionSettings; import com.mongodb.ClientSessionOptions; +import com.mongodb.ConnectionString; import com.mongodb.MongoClientSettings; import com.mongodb.MongoCredential; import com.mongodb.ReadConcern; @@ -26,11 +26,6 @@ import com.mongodb.ReadPreference; import com.mongodb.ServerApi; import com.mongodb.ServerApiVersion; -import com.mongodb.internal.connection.OidcAuthenticator; -import com.mongodb.event.TestServerMonitorListener; -import com.mongodb.internal.connection.ServerMonitoringModeUtil; -import com.mongodb.internal.connection.TestClusterListener; -import com.mongodb.logging.TestLoggingInterceptor; import com.mongodb.TransactionOptions; import com.mongodb.WriteConcern; import com.mongodb.assertions.Assertions; @@ -62,11 +57,15 @@ import com.mongodb.event.ConnectionPoolListener; import com.mongodb.event.ConnectionPoolReadyEvent; import com.mongodb.event.ConnectionReadyEvent; +import com.mongodb.event.TestServerMonitorListener; +import com.mongodb.internal.connection.ServerMonitoringModeUtil; +import com.mongodb.internal.connection.TestClusterListener; import com.mongodb.internal.connection.TestCommandListener; import com.mongodb.internal.connection.TestConnectionPoolListener; import com.mongodb.internal.connection.TestServerListener; import com.mongodb.internal.logging.LogMessage; import com.mongodb.lang.NonNull; +import com.mongodb.logging.TestLoggingInterceptor; import org.bson.BsonArray; import org.bson.BsonBoolean; import org.bson.BsonDocument; @@ -76,12 +75,6 @@ import org.bson.BsonString; import org.bson.BsonValue; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -96,6 +89,7 @@ import java.util.function.Function; import java.util.stream.Collectors; +import static com.mongodb.AuthenticationMechanism.MONGODB_OIDC; import static com.mongodb.ClusterFixture.getMultiMongosConnectionString; import static com.mongodb.ClusterFixture.isLoadBalanced; import static com.mongodb.ClusterFixture.isSharded; @@ -529,30 +523,47 @@ private void initClient(final BsonDocument entity, final String id, ServerMonitoringModeUtil.fromString(value.asString().getValue()))); break; case "authMechanism": - if (value.equals(new BsonString(AuthenticationMechanism.MONGODB_OIDC.getMechanismName()))) { - clientSettingsBuilder.credential(MongoCredential.createOidcCredential(null)); + if (value.equals(new BsonString(MONGODB_OIDC.getMechanismName()))) { + // authMechanismProperties depends on authMechanism + BsonDocument authMechanismProperties = entity + .getDocument("uriOptions") + .getDocument("authMechanismProperties"); + boolean hasPlaceholder = authMechanismProperties.equals( + new BsonDocument("$$placeholder", new BsonInt32(1))); + if (!hasPlaceholder) { + throw new UnsupportedOperationException("Unsupported authMechanism: " + value); + } + + // override the org.mongodb.test.uri connection string + String uri = getenv("MONGODB_URI"); + ConnectionString cs = new ConnectionString(uri); + clientSettingsBuilder.applyConnectionString(cs); + + String env = getenv("OIDC_ENV"); + if (env == null) { + env = "test"; + } + MongoCredential oidcCredential = MongoCredential + .createOidcCredential(null) + .withMechanismProperty("ENVIRONMENT", env); + if (env.equals("azure")) { + oidcCredential = oidcCredential + .withMechanismProperty("TOKEN_RESOURCE", getenv("AZUREOIDC_RESOURCE")); + } else if (env.equals("gcp")) { + oidcCredential = oidcCredential + .withMechanismProperty("TOKEN_RESOURCE", getenv("GCPOIDC_RESOURCE")); + } + clientSettingsBuilder.credential(oidcCredential); break; } throw new UnsupportedOperationException("Unsupported authMechanism: " + value); case "authMechanismProperties": - MongoCredential credential = clientSettingsBuilder.build().getCredential(); - boolean isOidc = credential != null - && credential.getAuthenticationMechanism() == AuthenticationMechanism.MONGODB_OIDC; - boolean hasPlaceholder = value.equals(new BsonDocument("$$placeholder", new BsonInt32(1))); - if (isOidc && hasPlaceholder) { - clientSettingsBuilder.credential(credential.withMechanismProperty( - MongoCredential.OIDC_CALLBACK_KEY, - (MongoCredential.OidcCallback) context -> { - Path path = Paths.get(getenv(OidcAuthenticator.AWS_WEB_IDENTITY_TOKEN_FILE)); - String accessToken; - try { - accessToken = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); - } catch (IOException e) { - throw new RuntimeException(e); - } - return new MongoCredential.OidcCallbackResult(accessToken, Duration.ZERO); - })); - break; + // authMechanismProperties are handled as part of authMechanism, above + BsonValue authMechanism = entity + .getDocument("uriOptions") + .get("authMechanism"); + if (authMechanism.equals(new BsonString(MONGODB_OIDC.getMechanismName()))) { + break; // only OIDC supports authMechanismProperties } throw new UnsupportedOperationException("Failure to apply authMechanismProperties: " + value); default: diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/RunOnRequirementsMatcher.java b/driver-sync/src/test/functional/com/mongodb/client/unified/RunOnRequirementsMatcher.java index aa7a3f80a53..60553c73f96 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/RunOnRequirementsMatcher.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/RunOnRequirementsMatcher.java @@ -69,7 +69,10 @@ public static boolean runOnRequirementsMet(final BsonArray runOnRequirements, fi } break; case "auth": - if (curRequirement.getValue().asBoolean().getValue() == (clientSettings.getCredential() == null)) { + boolean authRequired = curRequirement.getValue().asBoolean().getValue(); + boolean credentialPresent = clientSettings.getCredential() != null; + + if (authRequired != credentialPresent) { requirementMet = false; break requirementLoop; } diff --git a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java index b5a87a51cef..3b92ed1be84 100644 --- a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java +++ b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java @@ -33,7 +33,6 @@ import org.bson.BsonDocument; import org.bson.BsonInt32; import org.bson.BsonString; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -60,14 +59,16 @@ import java.util.stream.Collectors; import static com.mongodb.MongoCredential.ALLOWED_HOSTS_KEY; +import static com.mongodb.MongoCredential.OIDC_CALLBACK_KEY; import static com.mongodb.MongoCredential.OIDC_HUMAN_CALLBACK_KEY; -import static com.mongodb.MongoCredential.OidcCallbackResult; import static com.mongodb.MongoCredential.OidcCallback; import static com.mongodb.MongoCredential.OidcCallbackContext; -import static com.mongodb.MongoCredential.OIDC_CALLBACK_KEY; +import static com.mongodb.MongoCredential.OidcCallbackResult; +import static com.mongodb.assertions.Assertions.assertNotNull; import static java.lang.System.getenv; import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -80,42 +81,51 @@ */ public class OidcAuthenticationProseTests { + private String appName; + public static boolean oidcTestsEnabled() { return Boolean.parseBoolean(getenv().get("OIDC_TESTS_ENABLED")); } - private String appName; + private void assumeTestEnvironment() { + assumeTrue(getenv("OIDC_TOKEN_DIR") != null); + } protected static String getOidcUri() { - ConnectionString cs = new ConnectionString(getenv("OIDC_ATLAS_URI_SINGLE")); - // remove any username and password - return "mongodb+srv://" + cs.getHosts().get(0) + "/?authMechanism=MONGODB-OIDC"; + return getenv("MONGODB_URI_SINGLE"); } - protected static String getOidcUri(final String username) { - ConnectionString cs = new ConnectionString(getenv("OIDC_ATLAS_URI_SINGLE")); - // set username - return "mongodb+srv://" + username + "@" + cs.getHosts().get(0) + "/?authMechanism=MONGODB-OIDC"; + private static String getOidcUriMulti() { + return getenv("MONGODB_URI_MULTI"); } - protected static String getOidcUriMulti(@Nullable final String username) { - ConnectionString cs = new ConnectionString(getenv("OIDC_ATLAS_URI_MULTI")); - // set username - String userPart = username == null ? "" : username + "@"; - return "mongodb+srv://" + userPart + cs.getHosts().get(0) + "/?authMechanism=MONGODB-OIDC"; + private static String getAdminOidcUri() { + String oidcUri = getOidcUri(); + if (!oidcUri.contains("ENVIRONMENT:")) { + oidcUri += "&authMechanismProperties=ENVIRONMENT:" + getOidcEnv(); + } + return oidcUri; } - private static String getAwsOidcUri() { - return getOidcUri() + "&authMechanismProperties=PROVIDER_NAME:aws"; + private static String getOidcEnv() { + return getenv("OIDC_ENV"); + } + + @Nullable + private static String getUserWithDomain(@Nullable final String user) { + return user == null ? null : user + "@" + getenv("OIDC_DOMAIN"); } - @NotNull private static String oidcTokenDirectory() { - return getenv("OIDC_TOKEN_DIR"); + String dir = getenv("OIDC_TOKEN_DIR"); + if (!dir.endsWith("/")) { + dir = dir + "/"; + } + return dir; } - private static String getAwsTokenFilePath() { - return getenv(OidcAuthenticator.AWS_WEB_IDENTITY_TOKEN_FILE); + private static String getTestTokenFilePath() { + return getenv(OidcAuthenticator.OIDC_TOKEN_FILE); } protected MongoClient createMongoClient(final MongoClientSettings settings) { @@ -137,17 +147,17 @@ public void afterEach() { @Test public void test1p1CallbackIsCalledDuringAuth() { // #. Create a ``MongoClient`` configured with an OIDC callback... - TestCallback onRequest = createCallback(); - MongoClientSettings clientSettings = createSettings(getOidcUri(), onRequest, null); + TestCallback callback = createCallback(); + MongoClientSettings clientSettings = createSettings(callback); // #. Perform a find operation that succeeds performFind(clientSettings); - assertEquals(1, onRequest.invocations.get()); + assertEquals(1, callback.invocations.get()); } @Test public void test1p2CallbackCalledOnceForMultipleConnections() { - TestCallback onRequest = createCallback(); - MongoClientSettings clientSettings = createSettings(getOidcUri(), onRequest, null); + TestCallback callback = createCallback(); + MongoClientSettings clientSettings = createSettings(callback); try (MongoClient mongoClient = createMongoClient(clientSettings)) { List threads = new ArrayList<>(); for (int i = 0; i < 10; i++) { @@ -164,47 +174,47 @@ public void test1p2CallbackCalledOnceForMultipleConnections() { } } } - assertEquals(1, onRequest.invocations.get()); + assertEquals(1, callback.invocations.get()); } @Test public void test2p1ValidCallbackInputs() { - String connectionString = getOidcUri(); Duration expectedSeconds = Duration.ofMinutes(5); - TestCallback onRequest = createCallback(); + TestCallback callback1 = createCallback(); // #. Verify that the request callback was called with the appropriate // inputs, including the timeout parameter if possible. - OidcCallback onRequest2 = (context) -> { + OidcCallback callback2 = (context) -> { assertEquals(expectedSeconds, context.getTimeout()); - return onRequest.onRequest(context); + return callback1.onRequest(context); }; - MongoClientSettings clientSettings = createSettings(connectionString, onRequest2); + MongoClientSettings clientSettings = createSettings(callback2); try (MongoClient mongoClient = createMongoClient(clientSettings)) { performFind(mongoClient); // callback was called - assertEquals(1, onRequest.getInvocations()); + assertEquals(1, callback1.getInvocations()); } } @Test public void test2p2RequestCallbackReturnsNull() { //noinspection ConstantConditions - OidcCallback onRequest = (context) -> null; - MongoClientSettings settings = this.createSettings(getOidcUri(), onRequest, null); - performFind(settings, MongoConfigurationException.class, "Result of callback must not be null"); + OidcCallback callback = (context) -> null; + MongoClientSettings clientSettings = this.createSettings(callback); + performFind(clientSettings, MongoConfigurationException.class, + "Result of callback must not be null"); } @Test public void test2p3CallbackReturnsMissingData() { // #. Create a client with a request callback that returns data not // conforming to the OIDCRequestTokenResult with missing field(s). - OidcCallback onRequest = (context) -> { + OidcCallback callback = (context) -> { //noinspection ConstantConditions return new OidcCallbackResult(null, Duration.ZERO); }; // we ensure that the error is propagated - MongoClientSettings clientSettings = createSettings(getOidcUri(), onRequest, null); + MongoClientSettings clientSettings = createSettings(callback); try (MongoClient mongoClient = createMongoClient(clientSettings)) { try { performFind(mongoClient); @@ -217,24 +227,25 @@ public void test2p3CallbackReturnsMissingData() { @Test public void test2p4InvalidClientConfigurationWithCallback() { - String awsOidcUri = getAwsOidcUri(); + String uri = getOidcUri() + "&authMechanismProperties=ENVIRONMENT:" + getOidcEnv(); MongoClientSettings settings = createSettings( - awsOidcUri, createCallback(), null); + uri, createCallback(), null, OIDC_CALLBACK_KEY); try { performFind(settings); fail(); } catch (Exception e) { assertCause(IllegalArgumentException.class, - "OIDC_CALLBACK must not be specified when PROVIDER_NAME is specified", e); + "OIDC_CALLBACK must not be specified when ENVIRONMENT is specified", e); } } @Test public void test3p1AuthFailsWithCachedToken() throws ExecutionException, InterruptedException, NoSuchFieldException, IllegalAccessException { - TestCallback onRequestWrapped = createCallback(); + TestCallback callbackWrapped = createCallback(); + // reference to the token to poison CompletableFuture poisonToken = new CompletableFuture<>(); - OidcCallback onRequest = (context) -> { - OidcCallbackResult result = onRequestWrapped.onRequest(context); + OidcCallback callback = (context) -> { + OidcCallbackResult result = callbackWrapped.onRequest(context); String accessToken = result.getAccessToken(); if (!poisonToken.isDone()) { poisonToken.complete(accessToken); @@ -242,11 +253,11 @@ public void test3p1AuthFailsWithCachedToken() throws ExecutionException, Interru return result; }; - MongoClientSettings clientSettings = createSettings(getOidcUri(), onRequest, null); + MongoClientSettings clientSettings = createSettings(callback); try (MongoClient mongoClient = createMongoClient(clientSettings)) { // populate cache performFind(mongoClient); - assertEquals(1, onRequestWrapped.invocations.get()); + assertEquals(1, callbackWrapped.invocations.get()); // Poison the *Client Cache* with an invalid access token. // uses reflection String poisonString = poisonToken.get(); @@ -256,19 +267,20 @@ public void test3p1AuthFailsWithCachedToken() throws ExecutionException, Interru poisonChars[0] = '~'; poisonChars[1] = '~'; - assertEquals(1, onRequestWrapped.invocations.get()); + assertEquals(1, callbackWrapped.invocations.get()); // cause another connection to be opened - delayNextFind(); // cause both callbacks to be called + delayNextFind(); executeAll(2, () -> performFind(mongoClient)); } - assertEquals(2, onRequestWrapped.invocations.get()); + assertEquals(2, callbackWrapped.invocations.get()); } @Test public void test3p2AuthFailsWithoutCachedToken() { - MongoClientSettings clientSettings = createSettings(getOidcUri(), - (x) -> new OidcCallbackResult("invalid_token", Duration.ZERO), null); + OidcCallback callback = + (x) -> new OidcCallbackResult("invalid_token", Duration.ZERO); + MongoClientSettings clientSettings = createSettings(callback); try (MongoClient mongoClient = createMongoClient(clientSettings)) { try { performFind(mongoClient); @@ -280,26 +292,81 @@ public void test3p2AuthFailsWithoutCachedToken() { } } + @Test + public void test3p3UnexpectedErrorDoesNotClearCache() { + assumeTestEnvironment(); + + TestListener listener = new TestListener(); + TestCommandListener commandListener = new TestCommandListener(listener); + + TestCallback callback = createCallback(); + MongoClientSettings clientSettings = createSettings(getOidcUri(), callback, commandListener); + + try (MongoClient mongoClient = createMongoClient(clientSettings)) { + failCommand(20, 1, "saslStart"); + assertCause(MongoCommandException.class, + "Command failed with error 20", + () -> performFind(mongoClient)); + + assertEquals(Arrays.asList( + "isMaster started", + "isMaster succeeded", + "saslStart started", + "saslStart failed" + ), listener.getEventStrings()); + + assertEquals(1, callback.getInvocations()); + performFind(mongoClient); + assertEquals(1, callback.getInvocations()); + } + } + + // TODO-OIDC reinstate 2 broken(?) tests in mongodb-oidc-no-retry.json + @Test public void test4p1Reauthentication() { - TestCallback onRequest = createCallback(); - MongoClientSettings clientSettings = createSettings(getOidcUri(), onRequest); + TestCallback callback = createCallback(); + MongoClientSettings clientSettings = createSettings(callback); try (MongoClient mongoClient = createMongoClient(clientSettings)) { failCommand(391, 1, "find"); // #. Perform a find operation that succeeds. performFind(mongoClient); } - assertEquals(2, onRequest.invocations.get()); + assertEquals(2, callback.invocations.get()); + } + + @Test + public void test5p1Azure() { + assumeTrue(getOidcEnv().equals("azure")); + String oidcUri = getOidcUri(); + assertFalse(oidcUri.contains("@")); + MongoClientSettings clientSettings = createSettings(oidcUri, createCallback(), null); + try (MongoClient mongoClient = createMongoClient(clientSettings)) { + // #. Perform a find operation that succeeds. + performFind(mongoClient); + } + } + + @Test + public void test5p1AzureFails() { + assumeTrue(getOidcEnv().equals("azure")); + String oidcUri = getOidcUri(); + oidcUri = oidcUri.replace("://", "://bad@"); + MongoClientSettings clientSettings = createSettings(oidcUri, createCallback(), null); + try (MongoClient mongoClient = createMongoClient(clientSettings)) { + // #. Perform a find operation that succeeds. + performFind(mongoClient); + } } // Tests for human authentication ("testh", to preserve ordering) @Test public void testh1p1SinglePrincipalImplicitUsername() { + assumeTestEnvironment(); // #. Create default OIDC client with authMechanism=MONGODB-OIDC. - String oidcUri = getOidcUri(); TestCallback callback = createHumanCallback(); - MongoClientSettings clientSettings = createHumanSettings(oidcUri, callback, null); + MongoClientSettings clientSettings = createHumanSettings(callback, null); // #. Perform a find operation that succeeds performFind(clientSettings); assertEquals(1, callback.invocations.get()); @@ -307,67 +374,61 @@ public void testh1p1SinglePrincipalImplicitUsername() { @Test public void testh1p2SinglePrincipalExplicitUsername() { + assumeTestEnvironment(); // #. Create a client with MONGODB_URI_SINGLE, a username of test_user1, // authMechanism=MONGODB-OIDC, and the OIDC human callback. - String oidcUri = getOidcUri("test_user1"); TestCallback callback = createHumanCallback(); - MongoClientSettings clientSettings = createHumanSettings(oidcUri, callback, null); + MongoClientSettings clientSettings = createSettingsHuman(getUserWithDomain("test_user1"), callback, getOidcUri()); // #. Perform a find operation that succeeds performFind(clientSettings); } @Test public void testh1p3MultiplePrincipalUser1() { + assumeTestEnvironment(); // #. Create a client with MONGODB_URI_MULTI, a username of test_user1, // authMechanism=MONGODB-OIDC, and the OIDC human callback. - String oidcUri = getOidcUriMulti("test_user1"); - TestCallback callback = createHumanCallback(); - MongoClientSettings clientSettings = createHumanSettings(oidcUri, callback, null); + MongoClientSettings clientSettings = createSettingsMulti(getUserWithDomain("test_user1"), createHumanCallback()); // #. Perform a find operation that succeeds performFind(clientSettings); } @Test public void testh1p4MultiplePrincipalUser2() { + assumeTestEnvironment(); //- Create a human callback that reads in the generated ``test_user2`` token file. //- Create a client with ``MONGODB_URI_MULTI``, a username of ``test_user2``, // ``authMechanism=MONGODB-OIDC``, and the OIDC human callback. - String oidcUri = getOidcUriMulti("test_user2"); - TestCallback callback = createHumanCallback() - .setPathSupplier(() -> tokenQueue("test_user2").remove()); - MongoClientSettings clientSettings = createHumanSettings(oidcUri, callback, null); - // #. Perform a find operation that succeeds + MongoClientSettings clientSettings = createSettingsMulti(getUserWithDomain("test_user2"), createHumanCallback() + .setPathSupplier(() -> tokenQueue("test_user2").remove())); performFind(clientSettings); } @Test public void testh1p5MultiplePrincipalNoUser() { - //- Create a client with ``MONGODB_URI_MULTI``, no username, - // ``authMechanism=MONGODB-OIDC``, and the OIDC human callback. - String oidcUri = getOidcUriMulti(null); - TestCallback callback = createHumanCallback(); - MongoClientSettings clientSettings = createHumanSettings(oidcUri, callback, null); - // #. Perform a find operation that succeeds + assumeTestEnvironment(); + // Create an OIDC configured client with `MONGODB_URI_MULTI` and no username. + MongoClientSettings clientSettings = createSettingsMulti(null, createHumanCallback()); + // Assert that a `find` operation fails. performFind(clientSettings, MongoCommandException.class, "Authentication failed"); } @Test public void testh1p6AllowedHostsBlocked() { + assumeTestEnvironment(); //- Create a default OIDC client, with an ``ALLOWED_HOSTS`` that is an empty list. //- Assert that a ``find`` operation fails with a client-side error. - MongoClientSettings settings1 = createSettings( - getOidcUri(), + MongoClientSettings clientSettings1 = createSettings(getOidcUri(), createHumanCallback(), null, OIDC_HUMAN_CALLBACK_KEY, Collections.emptyList()); - performFind(settings1, MongoSecurityException.class, "not permitted by ALLOWED_HOSTS"); + performFind(clientSettings1, MongoSecurityException.class, "not permitted by ALLOWED_HOSTS"); //- Create a client that uses the URL // ``mongodb://localhost/?authMechanism=MONGODB-OIDC&ignored=example.com``, a // human callback, and an ``ALLOWED_HOSTS`` that contains ``["example.com"]``. //- Assert that a ``find`` operation fails with a client-side error. - MongoClientSettings settings2 = createSettings( - getOidcUri() + "&ignored=example.com", + MongoClientSettings clientSettings2 = createSettings(getOidcUri() + "&ignored=example.com", createHumanCallback(), null, OIDC_HUMAN_CALLBACK_KEY, Arrays.asList("example.com")); - performFind(settings2, MongoSecurityException.class, "not permitted by ALLOWED_HOSTS"); + performFind(clientSettings2, MongoSecurityException.class, "not permitted by ALLOWED_HOSTS"); } // Not a prose test @@ -379,68 +440,118 @@ public void testAllowedHostsDisallowedInConnectionString() { () -> new ConnectionString(string)); } + @Test + public void testh1p7() { + // example.com changed to localhost + String string = "mongodb+srv://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ALLOWED_HOSTS:%5B%22localhost%22%5D"; + assertCause(IllegalArgumentException.class, + "connection string contains disallowed mechanism properties", + () -> new ConnectionString(string)); + } + + @Test + public void testh1p8MachineIdpWithHumanCallback() { + assumeTrue(getenv("OIDC_IS_LOCAL") != null); + + TestCallback callback = createHumanCallback() + .setPathSupplier(() -> oidcTokenDirectory() + "test_machine"); + MongoClientSettings clientSettings = createSettingsHuman( + "test_machine", callback, getOidcUri()); + performFind(clientSettings); + } + @Test public void testh2p1ValidCallbackInputs() { - TestCallback onRequest = createHumanCallback(); - OidcCallback onRequest2 = (context) -> { - assertTrue(context.getIdpInfo().getClientId().startsWith("0oad")); - assertTrue(context.getIdpInfo().getIssuer().endsWith("mock-identity-config-oidc")); - assertEquals(Arrays.asList("fizz", "buzz"), context.getIdpInfo().getRequestScopes()); + assumeTestEnvironment(); + TestCallback callback1 = createHumanCallback(); + OidcCallback callback2 = (context) -> { + MongoCredential.IdpInfo idpInfo = assertNotNull(context.getIdpInfo()); + assertTrue(assertNotNull(idpInfo.getClientId()).startsWith("0oad")); + assertTrue(idpInfo.getIssuer().endsWith("mock-identity-config-oidc")); + assertEquals(Arrays.asList("fizz", "buzz"), idpInfo.getRequestScopes()); assertEquals(Duration.ofMinutes(5), context.getTimeout()); - return onRequest.onRequest(context); + return callback1.onRequest(context); }; - MongoClientSettings clientSettings = createHumanSettings(getOidcUri(), onRequest2, null); + MongoClientSettings clientSettings = createHumanSettings(callback2, null); try (MongoClient mongoClient = createMongoClient(clientSettings)) { performFind(mongoClient); // Ensure that callback was called - assertEquals(1, onRequest.getInvocations()); + assertEquals(1, callback1.getInvocations()); } } @Test public void testh2p2HumanCallbackReturnsMissingData() { + assumeTestEnvironment(); //noinspection ConstantConditions - OidcCallback onRequestNull = (context) -> null; - performFind(createHumanSettings(getOidcUri(), onRequestNull, null), + OidcCallback callbackNull = (context) -> null; + performFind(createHumanSettings(callbackNull, null), MongoConfigurationException.class, "Result of callback must not be null"); //noinspection ConstantConditions - OidcCallback onRequest = (context) -> new OidcCallbackResult(null, Duration.ZERO); - performFind(createHumanSettings(getOidcUri(), onRequest, null), + OidcCallback callback = + (context) -> new OidcCallbackResult(null, Duration.ZERO); + performFind(createHumanSettings(callback, null), IllegalArgumentException.class, "accessToken can not be null"); + } + // not a prose test + @Test + public void testRefreshTokenAbsent() { // additionally, check validation for refresh in machine workflow: - OidcCallback onRequestMachineRefresh = (context) -> new OidcCallbackResult("access", Duration.ZERO, "exists"); - performFind(createSettings(getOidcUri(), onRequestMachineRefresh, null), + OidcCallback callbackMachineRefresh = + (context) -> new OidcCallbackResult("access", Duration.ZERO, "exists"); + performFind(createSettings(callbackMachineRefresh), MongoConfigurationException.class, "Refresh token must only be provided in human workflow"); } @Test - public void testh3p1UsesSpecAuthIfCachedToken() { - failCommandAndCloseConnection("find", 1); - MongoClientSettings settings = createHumanSettings(getOidcUri(), createHumanCallback(), null); + public void testh2p3RefreshTokenPassed() { + assumeTestEnvironment(); + AtomicInteger refreshTokensProvided = new AtomicInteger(); + TestCallback callback1 = createHumanCallback(); + OidcCallback callback2 = (context) -> { + if (context.getRefreshToken() != null) { + refreshTokensProvided.incrementAndGet(); + } + return callback1.onRequest(context); + }; + MongoClientSettings clientSettings = createHumanSettings(callback2, null); + try (MongoClient mongoClient = createMongoClient(clientSettings)) { + performFind(mongoClient); + failCommand(391, 1, "find"); + performFind(mongoClient); + assertEquals(2, callback1.getInvocations()); + assertEquals(1, refreshTokensProvided.get()); + } + } - try (MongoClient mongoClient = createMongoClient(settings)) { + @Test + public void testh3p1UsesSpecAuthIfCachedToken() { + assumeTestEnvironment(); + MongoClientSettings clientSettings = createHumanSettings(createHumanCallback(), null); + try (MongoClient mongoClient = createMongoClient(clientSettings)) { + failCommandAndCloseConnection("find", 1); assertCause(MongoSocketException.class, "Prematurely reached end of stream", () -> performFind(mongoClient)); - failCommand(20, 99, "saslStart"); - + failCommand(18, 1, "saslStart"); performFind(mongoClient); } } @Test public void testh3p2NoSpecAuthIfNoCachedToken() { - failCommand(20, 99, "saslStart"); + assumeTestEnvironment(); + failCommand(18, 1, "saslStart"); TestListener listener = new TestListener(); TestCommandListener commandListener = new TestCommandListener(listener); - performFind(createHumanSettings(getOidcUri(), createHumanCallback(), commandListener), + performFind(createHumanSettings(createHumanCallback(), commandListener), MongoCommandException.class, - "Command failed with error 20"); + "Command failed with error 18"); assertEquals(Arrays.asList( "isMaster started", "isMaster succeeded", @@ -451,18 +562,19 @@ public void testh3p2NoSpecAuthIfNoCachedToken() { } @Test - public void testh4p1Succeeds() { + public void testh4p1ReauthenticationSucceeds() { + assumeTestEnvironment(); TestListener listener = new TestListener(); TestCommandListener commandListener = new TestCommandListener(listener); TestCallback callback = createHumanCallback() .setEventListener(listener); - MongoClientSettings settings = createHumanSettings(getOidcUri(), callback, commandListener); - try (MongoClient mongoClient = createMongoClient(settings)) { + MongoClientSettings clientSettings = createHumanSettings(callback, commandListener); + try (MongoClient mongoClient = createMongoClient(clientSettings)) { performFind(mongoClient); listener.clear(); assertEquals(1, callback.getInvocations()); - failCommand(391, 1, "find"); + // Perform another find operation that succeeds. performFind(mongoClient); assertEquals(Arrays.asList( // first find fails: @@ -482,27 +594,66 @@ public void testh4p1Succeeds() { @Test public void testh4p2SucceedsNoRefresh() { - TestListener listener = new TestListener(); - TestCommandListener commandListener = new TestCommandListener(listener); - TestCallback callback = createHumanCallback().setEventListener(listener); - MongoClientSettings settings = createHumanSettings(getOidcUri(), callback, commandListener); - try (MongoClient mongoClient = createMongoClient(settings)) { - + assumeTestEnvironment(); + TestCallback callback = createHumanCallback(); + MongoClientSettings clientSettings = createHumanSettings(callback, null); + try (MongoClient mongoClient = createMongoClient(clientSettings)) { performFind(mongoClient); - listener.clear(); assertEquals(1, callback.getInvocations()); failCommand(391, 1, "find"); performFind(mongoClient); + assertEquals(2, callback.getInvocations()); } } - // TODO-OIDC awaiting spec updates, add 4.3 and 4.4 + @Test + public void testh4p3SucceedsAfterRefreshFails() { + assumeTestEnvironment(); + TestCallback callback1 = createHumanCallback(); + OidcCallback callback2 = (context) -> { + OidcCallbackResult oidcCallbackResult = callback1.onRequest(context); + return new OidcCallbackResult(oidcCallbackResult.getAccessToken(), Duration.ofMinutes(5), "BAD_REFRESH"); + }; + MongoClientSettings clientSettings = createHumanSettings(callback2, null); + try (MongoClient mongoClient = createMongoClient(clientSettings)) { + performFind(mongoClient); + failCommand(391, 1, "find"); + performFind(mongoClient); + assertEquals(2, callback1.getInvocations()); + } + } + + @Test + public void testh4p4Fails() { + assumeTestEnvironment(); + ConcurrentLinkedQueue tokens = tokenQueue( + "test_user1", + "test_user1_expires", + "test_user1_expires"); + TestCallback callback1 = createHumanCallback() + .setPathSupplier(() -> tokens.remove()); + OidcCallback callback2 = (context) -> { + OidcCallbackResult oidcCallbackResult = callback1.onRequest(context); + return new OidcCallbackResult(oidcCallbackResult.getAccessToken(), Duration.ofMinutes(5), "BAD_REFRESH"); + }; + MongoClientSettings clientSettings = createHumanSettings(callback2, null); + try (MongoClient mongoClient = createMongoClient(clientSettings)) { + performFind(mongoClient); + assertEquals(1, callback1.getInvocations()); + failCommand(391, 1, "find"); + assertCause(MongoCommandException.class, + "Command failed with error 18", + () -> performFind(mongoClient)); + assertEquals(3, callback1.getInvocations()); + } + } // Not a prose test @Test public void testErrorClearsCache() { + assumeTestEnvironment(); // #. Create a new client with a valid request callback that // gives credentials that expire within 5 minutes and // a refresh callback that gives invalid credentials. @@ -512,14 +663,14 @@ public void testErrorClearsCache() { "test_user1_expires", "test_user1_expires", "test_user1_1"); - TestCallback onRequest = createHumanCallback() + TestCallback callback = createHumanCallback() .setRefreshToken("refresh") .setPathSupplier(() -> tokens.remove()) .setEventListener(listener); TestCommandListener commandListener = new TestCommandListener(listener); - MongoClientSettings clientSettings = createHumanSettings(getOidcUri(), onRequest, commandListener); + MongoClientSettings clientSettings = createHumanSettings(callback, commandListener); try (MongoClient mongoClient = createMongoClient(clientSettings)) { // #. Ensure that a find operation adds a new entry to the cache. performFind(mongoClient); @@ -585,17 +736,31 @@ public void testErrorClearsCache() { } } + + private MongoClientSettings createSettings(final OidcCallback callback) { + return createSettings(getOidcUri(), callback, null); + } + public MongoClientSettings createSettings( final String connectionString, - @Nullable final OidcCallback onRequest) { - return createSettings(connectionString, onRequest, null); + @Nullable final TestCallback callback) { + return createSettings(connectionString, callback, null); } private MongoClientSettings createSettings( final String connectionString, @Nullable final OidcCallback callback, @Nullable final CommandListener commandListener) { - return createSettings(connectionString, callback, commandListener, OIDC_CALLBACK_KEY); + String cleanedConnectionString = callback == null ? connectionString : connectionString + .replace("ENVIRONMENT:azure,", "") + .replace("ENVIRONMENT:gcp,", "") + .replace("ENVIRONMENT:test,", ""); + return createSettings(cleanedConnectionString, callback, commandListener, OIDC_CALLBACK_KEY); + } + + private MongoClientSettings createHumanSettings( + final OidcCallback callback, @Nullable final TestCommandListener commandListener) { + return createHumanSettings(getOidcUri(), callback, commandListener); } private MongoClientSettings createHumanSettings( @@ -605,15 +770,16 @@ private MongoClientSettings createHumanSettings( return createSettings(connectionString, callback, commandListener, OIDC_HUMAN_CALLBACK_KEY); } - @NotNull private MongoClientSettings createSettings( final String connectionString, - @Nullable final OidcCallback onRequest, + final @Nullable OidcCallback callback, @Nullable final CommandListener commandListener, final String oidcCallbackKey) { ConnectionString cs = new ConnectionString(connectionString); - MongoCredential credential = cs.getCredential() - .withMechanismProperty(oidcCallbackKey, onRequest); + MongoCredential credential = assertNotNull(cs.getCredential()); + if (callback != null) { + credential = credential.withMechanismProperty(oidcCallbackKey, callback); + } MongoClientSettings.Builder builder = MongoClientSettings.builder() .applicationName(appName) .applyConnectionString(cs) @@ -627,13 +793,13 @@ private MongoClientSettings createSettings( private MongoClientSettings createSettings( final String connectionString, - @Nullable final OidcCallback onRequest, + @Nullable final OidcCallback callback, @Nullable final CommandListener commandListener, final String oidcCallbackKey, @Nullable final List allowedHosts) { ConnectionString cs = new ConnectionString(connectionString); MongoCredential credential = cs.getCredential() - .withMechanismProperty(oidcCallbackKey, onRequest) + .withMechanismProperty(oidcCallbackKey, callback) .withMechanismProperty(ALLOWED_HOSTS_KEY, allowedHosts); MongoClientSettings.Builder builder = MongoClientSettings.builder() .applicationName(appName) @@ -645,6 +811,22 @@ private MongoClientSettings createSettings( return builder.build(); } + private MongoClientSettings createSettingsMulti(@Nullable final String user, final OidcCallback callback) { + return createSettingsHuman(user, callback, getOidcUriMulti()); + } + + private MongoClientSettings createSettingsHuman(@Nullable final String user, final OidcCallback callback, final String oidcUri) { + ConnectionString cs = new ConnectionString(oidcUri); + MongoCredential credential = MongoCredential.createOidcCredential(user) + .withMechanismProperty(OIDC_HUMAN_CALLBACK_KEY, callback); + return MongoClientSettings.builder() + .applicationName(appName) + .applyConnectionString(cs) + .retryReads(false) + .credential(credential) + .build(); + } + private void performFind(final MongoClientSettings settings) { try (MongoClient mongoClient = createMongoClient(settings)) { performFind(mongoClient); @@ -681,7 +863,7 @@ private static void assertCause( cause = cause.getCause(); } if (!expectedCause.isInstance(cause)) { - throw new AssertionFailedError("Unexpected cause", actualException); + throw new AssertionFailedError("Unexpected cause: " + actualException.getClass(), actualException); } if (!cause.getMessage().contains(expectedMessageFragment)) { throw new AssertionFailedError("Unexpected message", actualException); @@ -690,7 +872,7 @@ private static void assertCause( protected void delayNextFind() { try (MongoClient client = createMongoClient(createSettings( - getAwsOidcUri(), null, null))) { + getAdminOidcUri(), null, null, OIDC_CALLBACK_KEY))) { BsonDocument failPointDocument = new BsonDocument("configureFailPoint", new BsonString("failCommand")) .append("mode", new BsonDocument("times", new BsonInt32(1))) .append("data", new BsonDocument() @@ -704,7 +886,7 @@ protected void delayNextFind() { protected void failCommand(final int code, final int times, final String... commands) { try (MongoClient mongoClient = createMongoClient(createSettings( - getAwsOidcUri(), null, null))) { + getAdminOidcUri(), null, null, OIDC_CALLBACK_KEY))) { List list = Arrays.stream(commands).map(c -> new BsonString(c)).collect(Collectors.toList()); BsonDocument failPointDocument = new BsonDocument("configureFailPoint", new BsonString("failCommand")) .append("mode", new BsonDocument("times", new BsonInt32(times))) @@ -718,7 +900,7 @@ protected void failCommand(final int code, final int times, final String... comm private void failCommandAndCloseConnection(final String command, final int times) { try (MongoClient mongoClient = createMongoClient(createSettings( - getAwsOidcUri(), null, null))) { + getAdminOidcUri(), null, null, OIDC_CALLBACK_KEY))) { BsonDocument failPointDocument = new BsonDocument("configureFailPoint", new BsonString("failCommand")) .append("mode", new BsonDocument("times", new BsonInt32(times))) .append("data", new BsonDocument() @@ -772,11 +954,10 @@ public OidcCallbackResult onRequest(final OidcCallbackContext context) { + " - IdpInfo: " + (context.getIdpInfo() == null ? "none" : "present") + ")"); } - return callback(); + return callback(context); } - @NotNull - private OidcCallbackResult callback() { + private OidcCallbackResult callback(final OidcCallbackContext context) { if (concurrentTracker != null) { if (concurrentTracker.get() > 0) { throw new RuntimeException("Callbacks should not be invoked by multiple threads."); @@ -785,20 +966,28 @@ private OidcCallbackResult callback() { } try { invocations.incrementAndGet(); - Path path = Paths.get(pathSupplier == null - ? getAwsTokenFilePath() - : pathSupplier.get()); - String accessToken; try { simulateDelay(); - accessToken = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); - } catch (IOException | InterruptedException e) { + } catch (InterruptedException e) { throw new RuntimeException(e); } - if (testListener != null) { - testListener.add("read access token: " + path.getFileName()); + MongoCredential credential = assertNotNull(new ConnectionString(getOidcUri()).getCredential()); + String oidcEnv = getOidcEnv(); + OidcCallback c; + if (oidcEnv.contains("azure")) { + c = OidcAuthenticator.getAzureCallback(credential); + } else if (oidcEnv.contains("gcp")) { + c = OidcAuthenticator.getGcpCallback(credential); + } else if (oidcEnv.contains("test")) { + c = null; + } else { + c = null; + } + if (c == null) { + c = getProseTestCallback(); } - return new OidcCallbackResult(accessToken, Duration.ZERO, refreshToken); + return c.onRequest(context); + } finally { if (concurrentTracker != null) { concurrentTracker.decrementAndGet(); @@ -806,6 +995,23 @@ private OidcCallbackResult callback() { } } + private OidcCallback getProseTestCallback() { + return (x) -> { + try { + Path path = Paths.get(pathSupplier == null + ? getTestTokenFilePath() + : pathSupplier.get()); + String accessToken = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + if (testListener != null) { + testListener.add("read access token: " + path.getFileName()); + } + return new OidcCallbackResult(accessToken, Duration.ZERO, refreshToken); + } catch (IOException e) { + throw new RuntimeException(e); + } + }; + } + private void simulateDelay() throws InterruptedException { if (delayInMilliseconds != null) { Thread.sleep(delayInMilliseconds); @@ -847,6 +1053,7 @@ public TestCallback setPathSupplier(final Supplier pathSupplier) { this.testListener, pathSupplier); } + public TestCallback setRefreshToken(final String token) { return new TestCallback( token, @@ -857,7 +1064,6 @@ public TestCallback setRefreshToken(final String token) { } } - @NotNull private ConcurrentLinkedQueue tokenQueue(final String... queue) { String tokenPath = oidcTokenDirectory(); return java.util.stream.Stream From b84ca992208fd73ed2568145280d2db0ae6da005 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Mon, 22 Apr 2024 09:39:51 -0600 Subject: [PATCH 02/25] Add remaining tests, refactor, increase GCP test machine --- .evergreen/.evg.yml | 1 + .../authentication/GcpCredentialHelper.java | 27 ++-- .../OidcAuthenticationProseTests.java | 116 +++++++++++------- 3 files changed, 90 insertions(+), 54 deletions(-) diff --git a/.evergreen/.evg.yml b/.evergreen/.evg.yml index 57775871856..673eda64ba8 100644 --- a/.evergreen/.evg.yml +++ b/.evergreen/.evg.yml @@ -2174,6 +2174,7 @@ task_groups: binary: bash env: GCPOIDC_VMNAME_PREFIX: "JAVA_DRIVER" + GCPKMS_MACHINETYPE: "e2-medium" # comparable elapsed time to Azure; default was starved, caused timeouts args: - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/gcp/setup.sh teardown_task: diff --git a/driver-core/src/main/com/mongodb/internal/authentication/GcpCredentialHelper.java b/driver-core/src/main/com/mongodb/internal/authentication/GcpCredentialHelper.java index 9d9f1983c5e..dfd3b6a30e7 100644 --- a/driver-core/src/main/com/mongodb/internal/authentication/GcpCredentialHelper.java +++ b/driver-core/src/main/com/mongodb/internal/authentication/GcpCredentialHelper.java @@ -33,31 +33,32 @@ */ public final class GcpCredentialHelper { - public static CredentialInfo fetchGcpCredentialInfo(final String resource) { - String endpoint = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?" + resource; - return new CredentialInfo( - getBsonDocument(endpoint).getValue(), - Duration.ZERO); - } - public static BsonDocument obtainFromEnvironment() { String endpoint = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"; - return new BsonDocument("accessToken", getBsonDocument(endpoint)); - } - private static BsonString getBsonDocument(final String endpoint) { Map header = new HashMap<>(); header.put("Metadata-Flavor", "Google"); - header.put("Accept", "application/json"); String response = getHttpContents("GET", endpoint, header); BsonDocument responseDocument = BsonDocument.parse(response); if (responseDocument.containsKey("access_token")) { - return responseDocument.get("access_token").asString(); + return new BsonDocument("accessToken", responseDocument.get("access_token")); } else { - throw new MongoClientException("access_token is missing from GCE metadata response. Full response is ''" + response); + throw new MongoClientException("access_token is missing from GCE metadata response. Full response is ''" + + response); } } + public static CredentialInfo fetchGcpCredentialInfo(final String resource) { + String endpoint = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=" + + resource; + Map header = new HashMap<>(); + header.put("Metadata-Flavor", "Google");; + String response = getHttpContents("GET", endpoint, header); + return new CredentialInfo( + new BsonString(response).getValue(), + Duration.ZERO); + } + private GcpCredentialHelper() { } } diff --git a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java index 3b92ed1be84..7b40de83363 100644 --- a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java +++ b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java @@ -33,6 +33,7 @@ import org.bson.BsonDocument; import org.bson.BsonInt32; import org.bson.BsonString; +import org.bson.Document; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -71,7 +72,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assumptions.assumeTrue; import static util.ThreadTestHelpers.executeAll; @@ -201,7 +201,7 @@ public void test2p2RequestCallbackReturnsNull() { //noinspection ConstantConditions OidcCallback callback = (context) -> null; MongoClientSettings clientSettings = this.createSettings(callback); - performFind(clientSettings, MongoConfigurationException.class, + assertFindFails(clientSettings, MongoConfigurationException.class, "Result of callback must not be null"); } @@ -216,12 +216,9 @@ public void test2p3CallbackReturnsMissingData() { // we ensure that the error is propagated MongoClientSettings clientSettings = createSettings(callback); try (MongoClient mongoClient = createMongoClient(clientSettings)) { - try { - performFind(mongoClient); - fail(); - } catch (Exception e) { - assertCause(IllegalArgumentException.class, "accessToken can not be null", e); - } + assertCause(IllegalArgumentException.class, + "accessToken can not be null", + () -> performFind(mongoClient)); } } @@ -230,13 +227,9 @@ public void test2p4InvalidClientConfigurationWithCallback() { String uri = getOidcUri() + "&authMechanismProperties=ENVIRONMENT:" + getOidcEnv(); MongoClientSettings settings = createSettings( uri, createCallback(), null, OIDC_CALLBACK_KEY); - try { - performFind(settings); - fail(); - } catch (Exception e) { - assertCause(IllegalArgumentException.class, - "OIDC_CALLBACK must not be specified when ENVIRONMENT is specified", e); - } + assertCause(IllegalArgumentException.class, + "OIDC_CALLBACK must not be specified when ENVIRONMENT is specified", + () -> performFind(settings)); } @Test @@ -282,13 +275,9 @@ public void test3p2AuthFailsWithoutCachedToken() { (x) -> new OidcCallbackResult("invalid_token", Duration.ZERO); MongoClientSettings clientSettings = createSettings(callback); try (MongoClient mongoClient = createMongoClient(clientSettings)) { - try { - performFind(mongoClient); - fail(); - } catch (Exception e) { - assertCause(MongoCommandException.class, - "Command failed with error 18 (AuthenticationFailed):", e); - } + assertCause(MongoCommandException.class, + "Command failed with error 18 (AuthenticationFailed):", + () -> performFind(mongoClient)); } } @@ -321,8 +310,6 @@ public void test3p3UnexpectedErrorDoesNotClearCache() { } } - // TODO-OIDC reinstate 2 broken(?) tests in mongodb-oidc-no-retry.json - @Test public void test4p1Reauthentication() { TestCallback callback = createCallback(); @@ -335,6 +322,59 @@ public void test4p1Reauthentication() { assertEquals(2, callback.invocations.get()); } + @Test + public void test4p2ReadCommandsFailIfReauthenticationFails() { + // Create a `MongoClient` whose OIDC callback returns one good token + // and then bad tokens after the first call. + TestCallback wrappedCallback = createCallback(); + OidcCallback callback = (context) -> { + OidcCallbackResult result1 = wrappedCallback.callback(context); + return new OidcCallbackResult( + wrappedCallback.getInvocations() > 1 ? "bad" : result1.getAccessToken(), + Duration.ZERO, + null); + }; + MongoClientSettings clientSettings = createSettings(callback); + try (MongoClient mongoClient = createMongoClient(clientSettings)) { + performFind(mongoClient); + failCommand(391, 1, "find"); + assertCause(MongoCommandException.class, + "Command failed with error 18", + () -> performFind(mongoClient)); + } + assertEquals(2, wrappedCallback.invocations.get()); + } + + @Test + public void test4p3WriteCommandsFailIfReauthenticationFails() { + // Create a `MongoClient` whose OIDC callback returns one good token + // and then bad tokens after the first call. + TestCallback wrappedCallback = createCallback(); + OidcCallback callback = (context) -> { + OidcCallbackResult result1 = wrappedCallback.callback(context); + return new OidcCallbackResult( + wrappedCallback.getInvocations() > 1 ? "bad" : result1.getAccessToken(), + Duration.ZERO, + null); + }; + MongoClientSettings clientSettings = createSettings(callback); + try (MongoClient mongoClient = createMongoClient(clientSettings)) { + performInsert(mongoClient); + failCommand(391, 1, "insert"); + assertCause(MongoCommandException.class, + "Command failed with error 18", + () -> performInsert(mongoClient)); + } + assertEquals(2, wrappedCallback.invocations.get()); + } + + private static void performInsert(final MongoClient mongoClient) { + mongoClient + .getDatabase("test") + .getCollection("test") + .insertOne(Document.parse("{ x: 1 }")); + } + @Test public void test5p1Azure() { assumeTrue(getOidcEnv().equals("azure")); @@ -410,7 +450,7 @@ public void testh1p5MultiplePrincipalNoUser() { // Create an OIDC configured client with `MONGODB_URI_MULTI` and no username. MongoClientSettings clientSettings = createSettingsMulti(null, createHumanCallback()); // Assert that a `find` operation fails. - performFind(clientSettings, MongoCommandException.class, "Authentication failed"); + assertFindFails(clientSettings, MongoCommandException.class, "Authentication failed"); } @Test @@ -420,7 +460,7 @@ public void testh1p6AllowedHostsBlocked() { //- Assert that a ``find`` operation fails with a client-side error. MongoClientSettings clientSettings1 = createSettings(getOidcUri(), createHumanCallback(), null, OIDC_HUMAN_CALLBACK_KEY, Collections.emptyList()); - performFind(clientSettings1, MongoSecurityException.class, "not permitted by ALLOWED_HOSTS"); + assertFindFails(clientSettings1, MongoSecurityException.class, "not permitted by ALLOWED_HOSTS"); //- Create a client that uses the URL // ``mongodb://localhost/?authMechanism=MONGODB-OIDC&ignored=example.com``, a @@ -428,7 +468,7 @@ public void testh1p6AllowedHostsBlocked() { //- Assert that a ``find`` operation fails with a client-side error. MongoClientSettings clientSettings2 = createSettings(getOidcUri() + "&ignored=example.com", createHumanCallback(), null, OIDC_HUMAN_CALLBACK_KEY, Arrays.asList("example.com")); - performFind(clientSettings2, MongoSecurityException.class, "not permitted by ALLOWED_HOSTS"); + assertFindFails(clientSettings2, MongoSecurityException.class, "not permitted by ALLOWED_HOSTS"); } // Not a prose test @@ -485,14 +525,14 @@ public void testh2p2HumanCallbackReturnsMissingData() { assumeTestEnvironment(); //noinspection ConstantConditions OidcCallback callbackNull = (context) -> null; - performFind(createHumanSettings(callbackNull, null), + assertFindFails(createHumanSettings(callbackNull, null), MongoConfigurationException.class, "Result of callback must not be null"); //noinspection ConstantConditions OidcCallback callback = (context) -> new OidcCallbackResult(null, Duration.ZERO); - performFind(createHumanSettings(callback, null), + assertFindFails(createHumanSettings(callback, null), IllegalArgumentException.class, "accessToken can not be null"); } @@ -503,7 +543,7 @@ public void testRefreshTokenAbsent() { // additionally, check validation for refresh in machine workflow: OidcCallback callbackMachineRefresh = (context) -> new OidcCallbackResult("access", Duration.ZERO, "exists"); - performFind(createSettings(callbackMachineRefresh), + assertFindFails(createSettings(callbackMachineRefresh), MongoConfigurationException.class, "Refresh token must only be provided in human workflow"); } @@ -549,7 +589,7 @@ public void testh3p2NoSpecAuthIfNoCachedToken() { failCommand(18, 1, "saslStart"); TestListener listener = new TestListener(); TestCommandListener commandListener = new TestCommandListener(listener); - performFind(createHumanSettings(createHumanCallback(), commandListener), + assertFindFails(createHumanSettings(createHumanCallback(), commandListener), MongoCommandException.class, "Command failed with error 18"); assertEquals(Arrays.asList( @@ -833,7 +873,7 @@ private void performFind(final MongoClientSettings settings) { } } - private void performFind( + private void assertFindFails( final MongoClientSettings settings, final Class expectedExceptionOrCause, final String expectedMessage) { @@ -852,21 +892,15 @@ private void performFind(final MongoClient mongoClient) { private static void assertCause( final Class expectedCause, final String expectedMessageFragment, final Executable e) { - Throwable actualException = assertThrows(Throwable.class, e); - assertCause(expectedCause, expectedMessageFragment, actualException); - } - - private static void assertCause( - final Class expectedCause, final String expectedMessageFragment, final Throwable actualException) { - Throwable cause = actualException; + Throwable cause = assertThrows(Throwable.class, e); while (cause.getCause() != null) { cause = cause.getCause(); } if (!expectedCause.isInstance(cause)) { - throw new AssertionFailedError("Unexpected cause: " + actualException.getClass(), actualException); + throw new AssertionFailedError("Unexpected cause: " + assertThrows(Throwable.class, e).getClass(), assertThrows(Throwable.class, e)); } if (!cause.getMessage().contains(expectedMessageFragment)) { - throw new AssertionFailedError("Unexpected message", actualException); + throw new AssertionFailedError("Unexpected message", assertThrows(Throwable.class, e)); } } From df3ef8de2a84140f317f8681aeecdcee312398f9 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Tue, 23 Apr 2024 10:13:13 -0600 Subject: [PATCH 03/25] Cleanup, update since annotations, updates to match spec API --- .../src/main/com/mongodb/MongoCredential.java | 53 +++++++++++++------ .../connection/OidcAuthenticator.java | 50 ++++++++++------- .../OidcAuthenticationProseTests.java | 41 +++++++------- 3 files changed, 88 insertions(+), 56 deletions(-) diff --git a/driver-core/src/main/com/mongodb/MongoCredential.java b/driver-core/src/main/com/mongodb/MongoCredential.java index 30003ad43cf..db7c3763e7e 100644 --- a/driver-core/src/main/com/mongodb/MongoCredential.java +++ b/driver-core/src/main/com/mongodb/MongoCredential.java @@ -194,7 +194,7 @@ public final class MongoCredential { * must not be provided. * * @see #createOidcCredential(String) - * @since 4.10 + * @since 5.1 */ public static final String ENVIRONMENT_KEY = "ENVIRONMENT"; @@ -202,7 +202,7 @@ public final class MongoCredential { * This callback is invoked when the OIDC-based authenticator requests * a token. The type of the value must be {@link OidcCallback}. * {@link IdpInfo} will not be supplied to the callback, - * and a {@linkplain OidcCallbackResult#getRefreshToken() refresh token} + * and a {@linkplain com.mongodb.MongoCredential.OidcTokens#getRefreshToken() refresh token} * must not be returned by the callback. *

* If this is provided, {@link MongoCredential#ENVIRONMENT_KEY} @@ -210,7 +210,7 @@ public final class MongoCredential { * must not be provided. * * @see #createOidcCredential(String) - * @since 4.10 + * @since 5.1 */ public static final String OIDC_CALLBACK_KEY = "OIDC_CALLBACK"; @@ -225,7 +225,7 @@ public final class MongoCredential { * must not be provided. * * @see #createOidcCredential(String) - * @since 4.10 + * @since 5.1 */ public static final String OIDC_HUMAN_CALLBACK_KEY = "OIDC_HUMAN_CALLBACK"; @@ -238,7 +238,7 @@ public final class MongoCredential { * * @see MongoCredential#DEFAULT_ALLOWED_HOSTS * @see #createOidcCredential(String) - * @since 4.10 + * @since 5.1 */ public static final String ALLOWED_HOSTS_KEY = "ALLOWED_HOSTS"; @@ -249,7 +249,7 @@ public final class MongoCredential { * {@code "*.mongodb.net", "*.mongodb-qa.net", "*.mongodb-dev.net", "*.mongodbgov.net", "localhost", "127.0.0.1", "::1"} * * @see #createOidcCredential(String) - * @since 4.10 + * @since 5.1 */ public static final List DEFAULT_ALLOWED_HOSTS = Collections.unmodifiableList(Arrays.asList( "*.mongodb.net", "*.mongodb-qa.net", "*.mongodb-dev.net", "*.mongodbgov.net", "localhost", "127.0.0.1", "::1")); @@ -257,7 +257,7 @@ public final class MongoCredential { /** * The token resource. * - * @since TODO-OIDC update all + * @since 5.1 */ public static final String TOKEN_RESOURCE_KEY = "TOKEN_RESOURCE"; @@ -414,7 +414,7 @@ public static MongoCredential createAwsCredential(@Nullable final String userNam * * @param userName the user name, which may be null. This is the OIDC principal name. * @return the credential - * @since 4.10 + * @since 5.1 * @see #withMechanismProperty(String, Object) * @see #ENVIRONMENT_KEY * @see #OIDC_CALLBACK_KEY @@ -650,14 +650,16 @@ public String toString() { /** * The context for the {@link OidcCallback#onRequest(OidcCallbackContext) OIDC request callback}. + * + * @since 5.1 */ @Evolving public interface OidcCallbackContext { /** - * @return The OIDC Identity Provider's configuration that can be used to acquire an Access Token. + * @return Convenience method to obtain the username. */ @Nullable - IdpInfo getIdpInfo(); + String getUserName(); /** * @return The timeout that this callback must complete within. @@ -669,6 +671,12 @@ public interface OidcCallbackContext { */ int getVersion(); + /** + * @return The OIDC Identity Provider's configuration that can be used to acquire an Access Token. + */ + @Nullable + IdpInfo getIdpInfo(); + /** * @return The OIDC Refresh token supplied by a prior callback invocation. */ @@ -682,17 +690,21 @@ public interface OidcCallbackContext { *

* It does not have to be thread-safe, unless it is provided to multiple * MongoClients. + * + * @since 5.1 */ public interface OidcCallback { /** * @param context The context. * @return The response produced by an OIDC Identity Provider */ - OidcCallbackResult onRequest(OidcCallbackContext context); + OidcTokens onRequest(OidcCallbackContext context); } /** * The OIDC Identity Provider's configuration that can be used to acquire an Access Token. + * + * @since 5.1 */ @Evolving public interface IdpInfo { @@ -716,9 +728,11 @@ public interface IdpInfo { } /** - * The response produced by an OIDC Identity Provider. + * The OIDC credential information. + * + * @since 5.1 */ - public static final class OidcCallbackResult { + public static final class OidcTokens { private final String accessToken; @@ -727,13 +741,22 @@ public static final class OidcCallbackResult { @Nullable private final String refreshToken; + + /** + * An access token that does not expire. + * @param accessToken The OIDC access token. + */ + public OidcTokens(final String accessToken) { + this(accessToken, Duration.ZERO, null); + } + /** * @param accessToken The OIDC access token. * @param expiresIn Time until the access token expires. * A {@linkplain Duration#isZero() zero-length} duration * means that the access token does not expire. */ - public OidcCallbackResult(final String accessToken, final Duration expiresIn) { + public OidcTokens(final String accessToken, final Duration expiresIn) { this(accessToken, expiresIn, null); } @@ -744,7 +767,7 @@ public OidcCallbackResult(final String accessToken, final Duration expiresIn) { * means that the access token does not expire. * @param refreshToken The refresh token. If null, refresh will not be attempted. */ - public OidcCallbackResult(final String accessToken, final Duration expiresIn, + public OidcTokens(final String accessToken, final Duration expiresIn, @Nullable final String refreshToken) { notNull("accessToken", accessToken); notNull("expiresIn", expiresIn); diff --git a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java index ec2ec7855bb..ab36e089fb9 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java +++ b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java @@ -21,7 +21,7 @@ import com.mongodb.MongoCommandException; import com.mongodb.MongoConfigurationException; import com.mongodb.MongoCredential; -import com.mongodb.MongoCredential.OidcCallbackResult; +import com.mongodb.MongoCredential.OidcTokens; import com.mongodb.MongoException; import com.mongodb.MongoSecurityException; import com.mongodb.ServerAddress; @@ -192,7 +192,7 @@ private OidcCallback getRequestCallback() { public static OidcCallback getTestCallback() { return (context) -> { String accessToken = readTestTokenFromFile(); - return new OidcCallbackResult(accessToken, Duration.ZERO); + return new OidcTokens(accessToken); }; } @@ -202,7 +202,7 @@ public static OidcCallback getAzureCallback(final MongoCredential credential) { String resource = assertNotNull(credential.getMechanismProperty(TOKEN_RESOURCE_KEY, null)); String objectId = credential.getUserName(); CredentialInfo response = AzureCredentialHelper.fetchAzureCredentialInfo(resource, objectId); - return new OidcCallbackResult(response.getAccessToken(), response.getExpiresIn()); + return new OidcTokens(response.getAccessToken(), response.getExpiresIn()); }; } @@ -211,7 +211,7 @@ public static OidcCallback getGcpCallback(final MongoCredential credential) { return (context) -> { String resource = assertNotNull(credential.getMechanismProperty(TOKEN_RESOURCE_KEY, null)); CredentialInfo response = GcpCredentialHelper.fetchGcpCredentialInfo(resource); - return new OidcCallbackResult(response.getAccessToken(), response.getExpiresIn()); + return new OidcTokens(response.getAccessToken(), response.getExpiresIn()); }; } @@ -289,6 +289,7 @@ private byte[] evaluate(final byte[] challenge) { String cachedAccessToken = validatedCachedAccessToken(); OidcCallback requestCallback = getRequestCallback(); boolean isHuman = isHumanCallback(); + String userName = getMongoCredentialWithCache().getCredential().getUserName(); if (cachedAccessToken != null) { fallbackState = FallbackState.PHASE_1_CACHED_TOKEN; @@ -299,8 +300,8 @@ private byte[] evaluate(final byte[] challenge) { assertNotNull(cachedIdpInfo); // Invoke Callback using cached Refresh Token fallbackState = FallbackState.PHASE_2_REFRESH_CALLBACK_TOKEN; - OidcCallbackResult result = requestCallback.onRequest(new OidcCallbackContextImpl( - CALLBACK_TIMEOUT, cachedIdpInfo, cachedRefreshToken)); + OidcTokens result = requestCallback.onRequest(new OidcCallbackContextImpl( + CALLBACK_TIMEOUT, cachedIdpInfo, cachedRefreshToken, userName)); jwt[0] = populateCacheWithCallbackResultAndPrepareJwt(cachedIdpInfo, result); } else { // cache is empty @@ -308,8 +309,8 @@ private byte[] evaluate(final byte[] challenge) { if (!isHuman) { // no principal request fallbackState = FallbackState.PHASE_3B_CALLBACK_TOKEN; - OidcCallbackResult result = requestCallback.onRequest(new OidcCallbackContextImpl( - CALLBACK_TIMEOUT)); + OidcTokens result = requestCallback.onRequest(new OidcCallbackContextImpl( + CALLBACK_TIMEOUT, userName)); jwt[0] = populateCacheWithCallbackResultAndPrepareJwt(null, result); if (result.getRefreshToken() != null) { throw new MongoConfigurationException( @@ -333,13 +334,13 @@ private byte[] evaluate(final byte[] challenge) { if (!alreadyTriedPrincipal && idpInfoNotPresent) { // request for idp info, only in the human workflow fallbackState = FallbackState.PHASE_3A_PRINCIPAL; - jwt[0] = prepareUsername(getMongoCredentialWithCache().getCredential().getUserName()); + jwt[0] = prepareUsername(userName); } else { IdpInfo idpInfo = toIdpInfo(challenge); // there is no cached refresh token fallbackState = FallbackState.PHASE_3B_CALLBACK_TOKEN; - OidcCallbackResult result = requestCallback.onRequest(new OidcCallbackContextImpl( - CALLBACK_TIMEOUT, idpInfo, null)); + OidcTokens result = requestCallback.onRequest(new OidcCallbackContextImpl( + CALLBACK_TIMEOUT, idpInfo, null, userName)); jwt[0] = populateCacheWithCallbackResultAndPrepareJwt(idpInfo, result); } } @@ -499,14 +500,14 @@ private static String readTestTokenFromFile() { private byte[] populateCacheWithCallbackResultAndPrepareJwt( @Nullable final IdpInfo serverInfo, - @Nullable final OidcCallbackResult oidcCallbackResult) { - if (oidcCallbackResult == null) { + @Nullable final OidcTokens oidcTokens) { + if (oidcTokens == null) { throw new MongoConfigurationException("Result of callback must not be null"); } - OidcCacheEntry newEntry = new OidcCacheEntry(oidcCallbackResult.getAccessToken(), - oidcCallbackResult.getRefreshToken(), serverInfo); + OidcCacheEntry newEntry = new OidcCacheEntry(oidcTokens.getAccessToken(), + oidcTokens.getRefreshToken(), serverInfo); getMongoCredentialWithCache().setOidcCacheEntry(newEntry); - return prepareTokenAsJwt(oidcCallbackResult.getAccessToken()); + return prepareTokenAsJwt(oidcTokens.getAccessToken()); } private static byte[] prepareUsername(@Nullable final String username) { @@ -663,20 +664,26 @@ static class OidcCallbackContextImpl implements OidcCallbackContext { private final IdpInfo idpInfo; @Nullable private final String refreshToken; + @Nullable + private final String userName; - OidcCallbackContextImpl(final Duration timeout) { + OidcCallbackContextImpl(final Duration timeout, @Nullable final String userName) { this.timeout = assertNotNull(timeout); this.idpInfo = null; this.refreshToken = null; + this.userName = userName; } - OidcCallbackContextImpl(final Duration timeout, final IdpInfo idpInfo, @Nullable final String refreshToken) { + OidcCallbackContextImpl(final Duration timeout, final IdpInfo idpInfo, + @Nullable final String refreshToken, @Nullable final String userName) { this.timeout = assertNotNull(timeout); this.idpInfo = assertNotNull(idpInfo); this.refreshToken = refreshToken; + this.userName = userName; } @Override + @Nullable public IdpInfo getIdpInfo() { return idpInfo; } @@ -692,9 +699,16 @@ public int getVersion() { } @Override + @Nullable public String getRefreshToken() { return refreshToken; } + + @Override + @Nullable + public String getUserName() { + return userName; + } } @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) diff --git a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java index 7b40de83363..255dada52ba 100644 --- a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java +++ b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java @@ -64,7 +64,7 @@ import static com.mongodb.MongoCredential.OIDC_HUMAN_CALLBACK_KEY; import static com.mongodb.MongoCredential.OidcCallback; import static com.mongodb.MongoCredential.OidcCallbackContext; -import static com.mongodb.MongoCredential.OidcCallbackResult; +import static com.mongodb.MongoCredential.OidcTokens; import static com.mongodb.assertions.Assertions.assertNotNull; import static java.lang.System.getenv; import static java.util.Arrays.asList; @@ -211,7 +211,7 @@ public void test2p3CallbackReturnsMissingData() { // conforming to the OIDCRequestTokenResult with missing field(s). OidcCallback callback = (context) -> { //noinspection ConstantConditions - return new OidcCallbackResult(null, Duration.ZERO); + return new OidcTokens(null); }; // we ensure that the error is propagated MongoClientSettings clientSettings = createSettings(callback); @@ -238,7 +238,7 @@ public void test3p1AuthFailsWithCachedToken() throws ExecutionException, Interru // reference to the token to poison CompletableFuture poisonToken = new CompletableFuture<>(); OidcCallback callback = (context) -> { - OidcCallbackResult result = callbackWrapped.onRequest(context); + OidcTokens result = callbackWrapped.onRequest(context); String accessToken = result.getAccessToken(); if (!poisonToken.isDone()) { poisonToken.complete(accessToken); @@ -272,7 +272,7 @@ public void test3p1AuthFailsWithCachedToken() throws ExecutionException, Interru @Test public void test3p2AuthFailsWithoutCachedToken() { OidcCallback callback = - (x) -> new OidcCallbackResult("invalid_token", Duration.ZERO); + (x) -> new OidcTokens("invalid_token"); MongoClientSettings clientSettings = createSettings(callback); try (MongoClient mongoClient = createMongoClient(clientSettings)) { assertCause(MongoCommandException.class, @@ -328,11 +328,8 @@ public void test4p2ReadCommandsFailIfReauthenticationFails() { // and then bad tokens after the first call. TestCallback wrappedCallback = createCallback(); OidcCallback callback = (context) -> { - OidcCallbackResult result1 = wrappedCallback.callback(context); - return new OidcCallbackResult( - wrappedCallback.getInvocations() > 1 ? "bad" : result1.getAccessToken(), - Duration.ZERO, - null); + OidcTokens result1 = wrappedCallback.callback(context); + return new OidcTokens(wrappedCallback.getInvocations() > 1 ? "bad" : result1.getAccessToken()); }; MongoClientSettings clientSettings = createSettings(callback); try (MongoClient mongoClient = createMongoClient(clientSettings)) { @@ -351,11 +348,9 @@ public void test4p3WriteCommandsFailIfReauthenticationFails() { // and then bad tokens after the first call. TestCallback wrappedCallback = createCallback(); OidcCallback callback = (context) -> { - OidcCallbackResult result1 = wrappedCallback.callback(context); - return new OidcCallbackResult( - wrappedCallback.getInvocations() > 1 ? "bad" : result1.getAccessToken(), - Duration.ZERO, - null); + OidcTokens result1 = wrappedCallback.callback(context); + return new OidcTokens( + wrappedCallback.getInvocations() > 1 ? "bad" : result1.getAccessToken()); }; MongoClientSettings clientSettings = createSettings(callback); try (MongoClient mongoClient = createMongoClient(clientSettings)) { @@ -531,7 +526,7 @@ public void testh2p2HumanCallbackReturnsMissingData() { //noinspection ConstantConditions OidcCallback callback = - (context) -> new OidcCallbackResult(null, Duration.ZERO); + (context) -> new OidcTokens(null); assertFindFails(createHumanSettings(callback, null), IllegalArgumentException.class, "accessToken can not be null"); @@ -542,7 +537,7 @@ public void testh2p2HumanCallbackReturnsMissingData() { public void testRefreshTokenAbsent() { // additionally, check validation for refresh in machine workflow: OidcCallback callbackMachineRefresh = - (context) -> new OidcCallbackResult("access", Duration.ZERO, "exists"); + (context) -> new OidcTokens("access", Duration.ZERO, "exists"); assertFindFails(createSettings(callbackMachineRefresh), MongoConfigurationException.class, "Refresh token must only be provided in human workflow"); @@ -653,8 +648,8 @@ public void testh4p3SucceedsAfterRefreshFails() { assumeTestEnvironment(); TestCallback callback1 = createHumanCallback(); OidcCallback callback2 = (context) -> { - OidcCallbackResult oidcCallbackResult = callback1.onRequest(context); - return new OidcCallbackResult(oidcCallbackResult.getAccessToken(), Duration.ofMinutes(5), "BAD_REFRESH"); + OidcTokens oidcTokens = callback1.onRequest(context); + return new OidcTokens(oidcTokens.getAccessToken(), Duration.ofMinutes(5), "BAD_REFRESH"); }; MongoClientSettings clientSettings = createHumanSettings(callback2, null); try (MongoClient mongoClient = createMongoClient(clientSettings)) { @@ -675,8 +670,8 @@ public void testh4p4Fails() { TestCallback callback1 = createHumanCallback() .setPathSupplier(() -> tokens.remove()); OidcCallback callback2 = (context) -> { - OidcCallbackResult oidcCallbackResult = callback1.onRequest(context); - return new OidcCallbackResult(oidcCallbackResult.getAccessToken(), Duration.ofMinutes(5), "BAD_REFRESH"); + OidcTokens oidcTokens = callback1.onRequest(context); + return new OidcTokens(oidcTokens.getAccessToken(), Duration.ofMinutes(5), "BAD_REFRESH"); }; MongoClientSettings clientSettings = createHumanSettings(callback2, null); try (MongoClient mongoClient = createMongoClient(clientSettings)) { @@ -981,7 +976,7 @@ public int getInvocations() { } @Override - public OidcCallbackResult onRequest(final OidcCallbackContext context) { + public OidcTokens onRequest(final OidcCallbackContext context) { if (testListener != null) { testListener.add("onRequest invoked (" + "Refresh Token: " + (context.getRefreshToken() == null ? "none" : "present") @@ -991,7 +986,7 @@ public OidcCallbackResult onRequest(final OidcCallbackContext context) { return callback(context); } - private OidcCallbackResult callback(final OidcCallbackContext context) { + private OidcTokens callback(final OidcCallbackContext context) { if (concurrentTracker != null) { if (concurrentTracker.get() > 0) { throw new RuntimeException("Callbacks should not be invoked by multiple threads."); @@ -1039,7 +1034,7 @@ private OidcCallback getProseTestCallback() { if (testListener != null) { testListener.add("read access token: " + path.getFileName()); } - return new OidcCallbackResult(accessToken, Duration.ZERO, refreshToken); + return new OidcTokens(accessToken, Duration.ZERO, refreshToken); } catch (IOException e) { throw new RuntimeException(e); } From 581ae2eed6e03b3391a0bc0cbe380696efb166f9 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Tue, 23 Apr 2024 11:24:33 -0600 Subject: [PATCH 04/25] Test fixes --- .../mongodb/internal/authentication/GcpCredentialHelper.java | 2 +- .../com/mongodb/internal/connection/OidcAuthenticator.java | 4 +++- .../unit/com/mongodb/ConnectionStringSpecification.groovy | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/driver-core/src/main/com/mongodb/internal/authentication/GcpCredentialHelper.java b/driver-core/src/main/com/mongodb/internal/authentication/GcpCredentialHelper.java index dfd3b6a30e7..ee3acdddada 100644 --- a/driver-core/src/main/com/mongodb/internal/authentication/GcpCredentialHelper.java +++ b/driver-core/src/main/com/mongodb/internal/authentication/GcpCredentialHelper.java @@ -52,7 +52,7 @@ public static CredentialInfo fetchGcpCredentialInfo(final String resource) { String endpoint = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=" + resource; Map header = new HashMap<>(); - header.put("Metadata-Flavor", "Google");; + header.put("Metadata-Flavor", "Google"); String response = getHttpContents("GET", endpoint, header); return new CredentialInfo( new BsonString(response).getValue(), diff --git a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java index ab36e089fb9..9801bebca8b 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java +++ b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java @@ -80,6 +80,8 @@ public final class OidcAuthenticator extends SaslAuthenticator { AZURE_ENVIRONMENT, GCP_ENVIRONMENT, TEST_ENVIRONMENT); private static final List SUPPORTS_TOKEN_RESOURCE = Arrays.asList( AZURE_ENVIRONMENT, GCP_ENVIRONMENT); + private static final List ALLOWS_USERNAME = Arrays.asList( + AZURE_ENVIRONMENT); private static final Duration CALLBACK_TIMEOUT = Duration.ofMinutes(5); @@ -622,7 +624,7 @@ public static void validateBeforeUse(final MongoCredential credential) { + " must not be specified"); } } else { - if (userName != null) { + if (userName != null && !ALLOWS_USERNAME.contains(providerName)) { throw new IllegalArgumentException("user name must not be specified when " + ENVIRONMENT_KEY + " is specified"); } if (machineCallback != null) { diff --git a/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy b/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy index e8731439a84..d56aa8a9c7c 100644 --- a/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy @@ -601,7 +601,7 @@ class ConnectionStringSpecification extends Specification { new ConnectionString('mongodb://jeff@localhost/?' + 'authMechanism=GSSAPI' + '&authMechanismProperties=' + - 'SERVICE_NAME:foo:bar') + 'SERVICE_NAMEbar') // missing = then: thrown(IllegalArgumentException) From 00e4c154a652948b3e6e9b1cdbb355e0fcc8a398 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Tue, 23 Apr 2024 14:59:11 -0600 Subject: [PATCH 05/25] PR fixes --- .evergreen/.evg.yml | 2 +- .../src/main/com/mongodb/MongoCredential.java | 12 +++--- .../authentication/GcpCredentialHelper.java | 4 +- .../connection/OidcAuthenticator.java | 28 ++++++------- .../OidcAuthenticationProseTests.java | 39 ++++++++----------- 5 files changed, 39 insertions(+), 46 deletions(-) diff --git a/.evergreen/.evg.yml b/.evergreen/.evg.yml index 673eda64ba8..d00422e5c86 100644 --- a/.evergreen/.evg.yml +++ b/.evergreen/.evg.yml @@ -13,7 +13,7 @@ stepback: true command_type: system # Protect ourselves against rogue test case, or curl gone wild, that runs forever -exec_timeout_secs: 7200 +exec_timeout_secs: 3600 # What to do when evergreen hits the timeout (`post:` tasks are run automatically) timeout: diff --git a/driver-core/src/main/com/mongodb/MongoCredential.java b/driver-core/src/main/com/mongodb/MongoCredential.java index db7c3763e7e..7b06e3dbfff 100644 --- a/driver-core/src/main/com/mongodb/MongoCredential.java +++ b/driver-core/src/main/com/mongodb/MongoCredential.java @@ -202,7 +202,7 @@ public final class MongoCredential { * This callback is invoked when the OIDC-based authenticator requests * a token. The type of the value must be {@link OidcCallback}. * {@link IdpInfo} will not be supplied to the callback, - * and a {@linkplain com.mongodb.MongoCredential.OidcTokens#getRefreshToken() refresh token} + * and a {@linkplain com.mongodb.MongoCredential.OidcCallbackResult#getRefreshToken() refresh token} * must not be returned by the callback. *

* If this is provided, {@link MongoCredential#ENVIRONMENT_KEY} @@ -698,7 +698,7 @@ public interface OidcCallback { * @param context The context. * @return The response produced by an OIDC Identity Provider */ - OidcTokens onRequest(OidcCallbackContext context); + OidcCallbackResult onRequest(OidcCallbackContext context); } /** @@ -732,7 +732,7 @@ public interface IdpInfo { * * @since 5.1 */ - public static final class OidcTokens { + public static final class OidcCallbackResult { private final String accessToken; @@ -746,7 +746,7 @@ public static final class OidcTokens { * An access token that does not expire. * @param accessToken The OIDC access token. */ - public OidcTokens(final String accessToken) { + public OidcCallbackResult(final String accessToken) { this(accessToken, Duration.ZERO, null); } @@ -756,7 +756,7 @@ public OidcTokens(final String accessToken) { * A {@linkplain Duration#isZero() zero-length} duration * means that the access token does not expire. */ - public OidcTokens(final String accessToken, final Duration expiresIn) { + public OidcCallbackResult(final String accessToken, final Duration expiresIn) { this(accessToken, expiresIn, null); } @@ -767,7 +767,7 @@ public OidcTokens(final String accessToken, final Duration expiresIn) { * means that the access token does not expire. * @param refreshToken The refresh token. If null, refresh will not be attempted. */ - public OidcTokens(final String accessToken, final Duration expiresIn, + public OidcCallbackResult(final String accessToken, final Duration expiresIn, @Nullable final String refreshToken) { notNull("accessToken", accessToken); notNull("expiresIn", expiresIn); diff --git a/driver-core/src/main/com/mongodb/internal/authentication/GcpCredentialHelper.java b/driver-core/src/main/com/mongodb/internal/authentication/GcpCredentialHelper.java index ee3acdddada..697860a312d 100644 --- a/driver-core/src/main/com/mongodb/internal/authentication/GcpCredentialHelper.java +++ b/driver-core/src/main/com/mongodb/internal/authentication/GcpCredentialHelper.java @@ -32,7 +32,6 @@ *

This class is not part of the public API and may be removed or changed at any time

*/ public final class GcpCredentialHelper { - public static BsonDocument obtainFromEnvironment() { String endpoint = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"; @@ -43,8 +42,7 @@ public static BsonDocument obtainFromEnvironment() { if (responseDocument.containsKey("access_token")) { return new BsonDocument("accessToken", responseDocument.get("access_token")); } else { - throw new MongoClientException("access_token is missing from GCE metadata response. Full response is ''" - + response); + throw new MongoClientException("access_token is missing from GCE metadata response. Full response is ''" + response); } } diff --git a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java index 9801bebca8b..f06a2df5bd8 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java +++ b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java @@ -21,7 +21,7 @@ import com.mongodb.MongoCommandException; import com.mongodb.MongoConfigurationException; import com.mongodb.MongoCredential; -import com.mongodb.MongoCredential.OidcTokens; +import com.mongodb.MongoCredential.OidcCallbackResult; import com.mongodb.MongoException; import com.mongodb.MongoSecurityException; import com.mongodb.ServerAddress; @@ -193,8 +193,8 @@ private OidcCallback getRequestCallback() { @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) public static OidcCallback getTestCallback() { return (context) -> { - String accessToken = readTestTokenFromFile(); - return new OidcTokens(accessToken); + String accessToken = readTokenFromFile(); + return new OidcCallbackResult(accessToken); }; } @@ -204,7 +204,7 @@ public static OidcCallback getAzureCallback(final MongoCredential credential) { String resource = assertNotNull(credential.getMechanismProperty(TOKEN_RESOURCE_KEY, null)); String objectId = credential.getUserName(); CredentialInfo response = AzureCredentialHelper.fetchAzureCredentialInfo(resource, objectId); - return new OidcTokens(response.getAccessToken(), response.getExpiresIn()); + return new OidcCallbackResult(response.getAccessToken(), response.getExpiresIn()); }; } @@ -213,7 +213,7 @@ public static OidcCallback getGcpCallback(final MongoCredential credential) { return (context) -> { String resource = assertNotNull(credential.getMechanismProperty(TOKEN_RESOURCE_KEY, null)); CredentialInfo response = GcpCredentialHelper.fetchGcpCredentialInfo(resource); - return new OidcTokens(response.getAccessToken(), response.getExpiresIn()); + return new OidcCallbackResult(response.getAccessToken(), response.getExpiresIn()); }; } @@ -302,7 +302,7 @@ private byte[] evaluate(final byte[] challenge) { assertNotNull(cachedIdpInfo); // Invoke Callback using cached Refresh Token fallbackState = FallbackState.PHASE_2_REFRESH_CALLBACK_TOKEN; - OidcTokens result = requestCallback.onRequest(new OidcCallbackContextImpl( + OidcCallbackResult result = requestCallback.onRequest(new OidcCallbackContextImpl( CALLBACK_TIMEOUT, cachedIdpInfo, cachedRefreshToken, userName)); jwt[0] = populateCacheWithCallbackResultAndPrepareJwt(cachedIdpInfo, result); } else { @@ -311,7 +311,7 @@ private byte[] evaluate(final byte[] challenge) { if (!isHuman) { // no principal request fallbackState = FallbackState.PHASE_3B_CALLBACK_TOKEN; - OidcTokens result = requestCallback.onRequest(new OidcCallbackContextImpl( + OidcCallbackResult result = requestCallback.onRequest(new OidcCallbackContextImpl( CALLBACK_TIMEOUT, userName)); jwt[0] = populateCacheWithCallbackResultAndPrepareJwt(null, result); if (result.getRefreshToken() != null) { @@ -341,7 +341,7 @@ private byte[] evaluate(final byte[] challenge) { IdpInfo idpInfo = toIdpInfo(challenge); // there is no cached refresh token fallbackState = FallbackState.PHASE_3B_CALLBACK_TOKEN; - OidcTokens result = requestCallback.onRequest(new OidcCallbackContextImpl( + OidcCallbackResult result = requestCallback.onRequest(new OidcCallbackContextImpl( CALLBACK_TIMEOUT, idpInfo, null, userName)); jwt[0] = populateCacheWithCallbackResultAndPrepareJwt(idpInfo, result); } @@ -485,7 +485,7 @@ public boolean isComplete() { } - private static String readTestTokenFromFile() { + private static String readTokenFromFile() { String path = System.getenv(OIDC_TOKEN_FILE); if (path == null) { throw new MongoClientException( @@ -502,14 +502,14 @@ private static String readTestTokenFromFile() { private byte[] populateCacheWithCallbackResultAndPrepareJwt( @Nullable final IdpInfo serverInfo, - @Nullable final OidcTokens oidcTokens) { - if (oidcTokens == null) { + @Nullable final OidcCallbackResult oidcCallbackResult) { + if (oidcCallbackResult == null) { throw new MongoConfigurationException("Result of callback must not be null"); } - OidcCacheEntry newEntry = new OidcCacheEntry(oidcTokens.getAccessToken(), - oidcTokens.getRefreshToken(), serverInfo); + OidcCacheEntry newEntry = new OidcCacheEntry(oidcCallbackResult.getAccessToken(), + oidcCallbackResult.getRefreshToken(), serverInfo); getMongoCredentialWithCache().setOidcCacheEntry(newEntry); - return prepareTokenAsJwt(oidcTokens.getAccessToken()); + return prepareTokenAsJwt(oidcCallbackResult.getAccessToken()); } private static byte[] prepareUsername(@Nullable final String username) { diff --git a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java index 255dada52ba..1b02f4066d5 100644 --- a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java +++ b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java @@ -64,7 +64,7 @@ import static com.mongodb.MongoCredential.OIDC_HUMAN_CALLBACK_KEY; import static com.mongodb.MongoCredential.OidcCallback; import static com.mongodb.MongoCredential.OidcCallbackContext; -import static com.mongodb.MongoCredential.OidcTokens; +import static com.mongodb.MongoCredential.OidcCallbackResult; import static com.mongodb.assertions.Assertions.assertNotNull; import static java.lang.System.getenv; import static java.util.Arrays.asList; @@ -211,7 +211,7 @@ public void test2p3CallbackReturnsMissingData() { // conforming to the OIDCRequestTokenResult with missing field(s). OidcCallback callback = (context) -> { //noinspection ConstantConditions - return new OidcTokens(null); + return new OidcCallbackResult(null); }; // we ensure that the error is propagated MongoClientSettings clientSettings = createSettings(callback); @@ -238,7 +238,7 @@ public void test3p1AuthFailsWithCachedToken() throws ExecutionException, Interru // reference to the token to poison CompletableFuture poisonToken = new CompletableFuture<>(); OidcCallback callback = (context) -> { - OidcTokens result = callbackWrapped.onRequest(context); + OidcCallbackResult result = callbackWrapped.onRequest(context); String accessToken = result.getAccessToken(); if (!poisonToken.isDone()) { poisonToken.complete(accessToken); @@ -272,7 +272,7 @@ public void test3p1AuthFailsWithCachedToken() throws ExecutionException, Interru @Test public void test3p2AuthFailsWithoutCachedToken() { OidcCallback callback = - (x) -> new OidcTokens("invalid_token"); + (x) -> new OidcCallbackResult("invalid_token"); MongoClientSettings clientSettings = createSettings(callback); try (MongoClient mongoClient = createMongoClient(clientSettings)) { assertCause(MongoCommandException.class, @@ -328,8 +328,8 @@ public void test4p2ReadCommandsFailIfReauthenticationFails() { // and then bad tokens after the first call. TestCallback wrappedCallback = createCallback(); OidcCallback callback = (context) -> { - OidcTokens result1 = wrappedCallback.callback(context); - return new OidcTokens(wrappedCallback.getInvocations() > 1 ? "bad" : result1.getAccessToken()); + OidcCallbackResult result1 = wrappedCallback.callback(context); + return new OidcCallbackResult(wrappedCallback.getInvocations() > 1 ? "bad" : result1.getAccessToken()); }; MongoClientSettings clientSettings = createSettings(callback); try (MongoClient mongoClient = createMongoClient(clientSettings)) { @@ -348,8 +348,8 @@ public void test4p3WriteCommandsFailIfReauthenticationFails() { // and then bad tokens after the first call. TestCallback wrappedCallback = createCallback(); OidcCallback callback = (context) -> { - OidcTokens result1 = wrappedCallback.callback(context); - return new OidcTokens( + OidcCallbackResult result1 = wrappedCallback.callback(context); + return new OidcCallbackResult( wrappedCallback.getInvocations() > 1 ? "bad" : result1.getAccessToken()); }; MongoClientSettings clientSettings = createSettings(callback); @@ -526,7 +526,7 @@ public void testh2p2HumanCallbackReturnsMissingData() { //noinspection ConstantConditions OidcCallback callback = - (context) -> new OidcTokens(null); + (context) -> new OidcCallbackResult(null); assertFindFails(createHumanSettings(callback, null), IllegalArgumentException.class, "accessToken can not be null"); @@ -537,7 +537,7 @@ public void testh2p2HumanCallbackReturnsMissingData() { public void testRefreshTokenAbsent() { // additionally, check validation for refresh in machine workflow: OidcCallback callbackMachineRefresh = - (context) -> new OidcTokens("access", Duration.ZERO, "exists"); + (context) -> new OidcCallbackResult("access", Duration.ZERO, "exists"); assertFindFails(createSettings(callbackMachineRefresh), MongoConfigurationException.class, "Refresh token must only be provided in human workflow"); @@ -648,8 +648,8 @@ public void testh4p3SucceedsAfterRefreshFails() { assumeTestEnvironment(); TestCallback callback1 = createHumanCallback(); OidcCallback callback2 = (context) -> { - OidcTokens oidcTokens = callback1.onRequest(context); - return new OidcTokens(oidcTokens.getAccessToken(), Duration.ofMinutes(5), "BAD_REFRESH"); + OidcCallbackResult oidcCallbackResult = callback1.onRequest(context); + return new OidcCallbackResult(oidcCallbackResult.getAccessToken(), Duration.ofMinutes(5), "BAD_REFRESH"); }; MongoClientSettings clientSettings = createHumanSettings(callback2, null); try (MongoClient mongoClient = createMongoClient(clientSettings)) { @@ -670,8 +670,8 @@ public void testh4p4Fails() { TestCallback callback1 = createHumanCallback() .setPathSupplier(() -> tokens.remove()); OidcCallback callback2 = (context) -> { - OidcTokens oidcTokens = callback1.onRequest(context); - return new OidcTokens(oidcTokens.getAccessToken(), Duration.ofMinutes(5), "BAD_REFRESH"); + OidcCallbackResult oidcCallbackResult = callback1.onRequest(context); + return new OidcCallbackResult(oidcCallbackResult.getAccessToken(), Duration.ofMinutes(5), "BAD_REFRESH"); }; MongoClientSettings clientSettings = createHumanSettings(callback2, null); try (MongoClient mongoClient = createMongoClient(clientSettings)) { @@ -976,7 +976,7 @@ public int getInvocations() { } @Override - public OidcTokens onRequest(final OidcCallbackContext context) { + public OidcCallbackResult onRequest(final OidcCallbackContext context) { if (testListener != null) { testListener.add("onRequest invoked (" + "Refresh Token: " + (context.getRefreshToken() == null ? "none" : "present") @@ -986,7 +986,7 @@ public OidcTokens onRequest(final OidcCallbackContext context) { return callback(context); } - private OidcTokens callback(final OidcCallbackContext context) { + private OidcCallbackResult callback(final OidcCallbackContext context) { if (concurrentTracker != null) { if (concurrentTracker.get() > 0) { throw new RuntimeException("Callbacks should not be invoked by multiple threads."); @@ -1007,12 +1007,7 @@ private OidcTokens callback(final OidcCallbackContext context) { c = OidcAuthenticator.getAzureCallback(credential); } else if (oidcEnv.contains("gcp")) { c = OidcAuthenticator.getGcpCallback(credential); - } else if (oidcEnv.contains("test")) { - c = null; } else { - c = null; - } - if (c == null) { c = getProseTestCallback(); } return c.onRequest(context); @@ -1034,7 +1029,7 @@ private OidcCallback getProseTestCallback() { if (testListener != null) { testListener.add("read access token: " + path.getFileName()); } - return new OidcTokens(accessToken, Duration.ZERO, refreshToken); + return new OidcCallbackResult(accessToken, Duration.ZERO, refreshToken); } catch (IOException e) { throw new RuntimeException(e); } From 6898b4fa993ce0d02fab92437d4ee626986b1e90 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Wed, 24 Apr 2024 11:00:34 -0600 Subject: [PATCH 06/25] Remove admin credentials --- .evergreen/run-mongodb-oidc-test.sh | 7 +------ .../functional/com/mongodb/client/unified/Entities.java | 6 ------ 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/.evergreen/run-mongodb-oidc-test.sh b/.evergreen/run-mongodb-oidc-test.sh index 101a4754c84..849e349ea02 100755 --- a/.evergreen/run-mongodb-oidc-test.sh +++ b/.evergreen/run-mongodb-oidc-test.sh @@ -38,12 +38,7 @@ which java export OIDC_TESTS_ENABLED=true export OIDC_ENV="$OIDC_ENV" # read by tests -# use admin credentials for tests -TO_REPLACE="mongodb://" -REPLACEMENT="mongodb://$OIDC_ADMIN_USER:$OIDC_ADMIN_PWD@" -ADMIN_URI=${MONGODB_URI/$TO_REPLACE/$REPLACEMENT} - -./gradlew -Dorg.mongodb.test.uri="$ADMIN_URI" \ +./gradlew -Dorg.mongodb.test.uri="$MONGODB_URI" \ --stacktrace --debug --info --no-build-cache driver-core:cleanTest \ driver-sync:test --tests OidcAuthenticationProseTests --tests UnifiedAuthTest \ driver-reactive-streams:test --tests OidcAuthenticationAsyncProseTests \ diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java index a535f00e5ec..d9e049100fa 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java @@ -18,7 +18,6 @@ import com.mongodb.ClientEncryptionSettings; import com.mongodb.ClientSessionOptions; -import com.mongodb.ConnectionString; import com.mongodb.MongoClientSettings; import com.mongodb.MongoCredential; import com.mongodb.ReadConcern; @@ -534,11 +533,6 @@ private void initClient(final BsonDocument entity, final String id, throw new UnsupportedOperationException("Unsupported authMechanism: " + value); } - // override the org.mongodb.test.uri connection string - String uri = getenv("MONGODB_URI"); - ConnectionString cs = new ConnectionString(uri); - clientSettingsBuilder.applyConnectionString(cs); - String env = getenv("OIDC_ENV"); if (env == null) { env = "test"; From 2621ae8456f98d3ca0e1a7423051f3400da1e6b6 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Thu, 25 Apr 2024 14:32:22 -0600 Subject: [PATCH 07/25] PR fixes --- .evergreen/.evg.yml | 14 +++++++------ .evergreen/run-mongodb-oidc-test.sh | 4 ---- .../authentication/AzureCredentialHelper.java | 4 ++-- .../connection/OidcAuthenticator.java | 4 ++-- .../OidcAuthenticationProseTests.java | 21 ++++++------------- 5 files changed, 18 insertions(+), 29 deletions(-) diff --git a/.evergreen/.evg.yml b/.evergreen/.evg.yml index d00422e5c86..886282b77c4 100644 --- a/.evergreen/.evg.yml +++ b/.evergreen/.evg.yml @@ -967,7 +967,7 @@ tasks: - func: "run load-balancer" - func: "run load-balancer tests" - - name: "oidc-auth-test-latest" + - name: "oidc-auth-test" commands: - command: subprocess.exec type: test @@ -975,10 +975,12 @@ tasks: working_dir: "src" binary: bash include_expansions_in_env: ["DRIVERS_TOOLS", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"] + env: + OIDC_ENV: "test" args: - .evergreen/run-mongodb-oidc-test.sh - - name: "oidc-auth-test-azure-latest" + - name: "oidc-auth-test-azure" commands: - command: shell.exec params: @@ -998,7 +1000,7 @@ tasks: export AZUREOIDC_TEST_CMD="OIDC_ENV=azure ./.evergreen/run-mongodb-oidc-test.sh" bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh - - name: "oidc-auth-test-gcp-latest" + - name: "oidc-auth-test-gcp" commands: - command: shell.exec params: @@ -2139,7 +2141,7 @@ task_groups: setup_group_can_fail_task: true setup_group_timeout_secs: 1800 tasks: - - oidc-auth-test-latest + - oidc-auth-test - name: testazureoidc_task_group setup_group: @@ -2162,7 +2164,7 @@ task_groups: setup_group_can_fail_task: true setup_group_timeout_secs: 1800 tasks: - - oidc-auth-test-azure-latest + - oidc-auth-test-azure - name: testgcpoidc_task_group setup_group: @@ -2186,7 +2188,7 @@ task_groups: setup_group_can_fail_task: true setup_group_timeout_secs: 1800 tasks: - - oidc-auth-test-gcp-latest + - oidc-auth-test-gcp buildvariants: diff --git a/.evergreen/run-mongodb-oidc-test.sh b/.evergreen/run-mongodb-oidc-test.sh index 849e349ea02..1f5c1b310cc 100755 --- a/.evergreen/run-mongodb-oidc-test.sh +++ b/.evergreen/run-mongodb-oidc-test.sh @@ -4,9 +4,6 @@ set +x # Disable debug trace set -eu echo "Running MONGODB-OIDC authentication tests" - -OIDC_ENV=${OIDC_ENV:-"test"} - echo "OIDC_ENV $OIDC_ENV" if [ $OIDC_ENV == "test" ]; then @@ -36,7 +33,6 @@ fi which java export OIDC_TESTS_ENABLED=true -export OIDC_ENV="$OIDC_ENV" # read by tests ./gradlew -Dorg.mongodb.test.uri="$MONGODB_URI" \ --stacktrace --debug --info --no-build-cache driver-core:cleanTest \ diff --git a/driver-core/src/main/com/mongodb/internal/authentication/AzureCredentialHelper.java b/driver-core/src/main/com/mongodb/internal/authentication/AzureCredentialHelper.java index 6b1c2d21020..9f325c6f251 100644 --- a/driver-core/src/main/com/mongodb/internal/authentication/AzureCredentialHelper.java +++ b/driver-core/src/main/com/mongodb/internal/authentication/AzureCredentialHelper.java @@ -69,11 +69,11 @@ public static BsonDocument obtainFromEnvironment() { return new BsonDocument("accessToken", new BsonString(accessToken)); } - public static CredentialInfo fetchAzureCredentialInfo(final String resource, @Nullable final String objectId) { + public static CredentialInfo fetchAzureCredentialInfo(final String resource, @Nullable final String clientId) { String endpoint = "http://169.254.169.254:80" + "/metadata/identity/oauth2/token?api-version=2018-02-01" + "&resource=" + resource - + (objectId == null ? "" : "&object_id=" + objectId); + + (clientId == null ? "" : "&client_id=" + clientId); Map headers = new HashMap<>(); headers.put("Metadata", "true"); diff --git a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java index f06a2df5bd8..705d74a3c2c 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java +++ b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java @@ -202,8 +202,8 @@ public static OidcCallback getTestCallback() { public static OidcCallback getAzureCallback(final MongoCredential credential) { return (context) -> { String resource = assertNotNull(credential.getMechanismProperty(TOKEN_RESOURCE_KEY, null)); - String objectId = credential.getUserName(); - CredentialInfo response = AzureCredentialHelper.fetchAzureCredentialInfo(resource, objectId); + String clientId = credential.getUserName(); + CredentialInfo response = AzureCredentialHelper.fetchAzureCredentialInfo(resource, clientId); return new OidcCallbackResult(response.getAccessToken(), response.getExpiresIn()); }; } diff --git a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java index 1b02f4066d5..76877af199e 100644 --- a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java +++ b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java @@ -23,6 +23,7 @@ import com.mongodb.MongoCredential; import com.mongodb.MongoSecurityException; import com.mongodb.MongoSocketException; +import com.mongodb.client.Fixture; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; import com.mongodb.client.TestListener; @@ -77,7 +78,7 @@ /** * See - * Prose Tests. + * Prose Tests. */ public class OidcAuthenticationProseTests { @@ -99,14 +100,6 @@ private static String getOidcUriMulti() { return getenv("MONGODB_URI_MULTI"); } - private static String getAdminOidcUri() { - String oidcUri = getOidcUri(); - if (!oidcUri.contains("ENVIRONMENT:")) { - oidcUri += "&authMechanismProperties=ENVIRONMENT:" + getOidcEnv(); - } - return oidcUri; - } - private static String getOidcEnv() { return getenv("OIDC_ENV"); } @@ -900,8 +893,8 @@ private static void assertCause( } protected void delayNextFind() { - try (MongoClient client = createMongoClient(createSettings( - getAdminOidcUri(), null, null, OIDC_CALLBACK_KEY))) { + + try (MongoClient client = createMongoClient(Fixture.getMongoClientSettings())) { BsonDocument failPointDocument = new BsonDocument("configureFailPoint", new BsonString("failCommand")) .append("mode", new BsonDocument("times", new BsonInt32(1))) .append("data", new BsonDocument() @@ -914,8 +907,7 @@ protected void delayNextFind() { } protected void failCommand(final int code, final int times, final String... commands) { - try (MongoClient mongoClient = createMongoClient(createSettings( - getAdminOidcUri(), null, null, OIDC_CALLBACK_KEY))) { + try (MongoClient mongoClient = createMongoClient(Fixture.getMongoClientSettings())) { List list = Arrays.stream(commands).map(c -> new BsonString(c)).collect(Collectors.toList()); BsonDocument failPointDocument = new BsonDocument("configureFailPoint", new BsonString("failCommand")) .append("mode", new BsonDocument("times", new BsonInt32(times))) @@ -928,8 +920,7 @@ protected void failCommand(final int code, final int times, final String... comm } private void failCommandAndCloseConnection(final String command, final int times) { - try (MongoClient mongoClient = createMongoClient(createSettings( - getAdminOidcUri(), null, null, OIDC_CALLBACK_KEY))) { + try (MongoClient mongoClient = createMongoClient(Fixture.getMongoClientSettings())) { BsonDocument failPointDocument = new BsonDocument("configureFailPoint", new BsonString("failCommand")) .append("mode", new BsonDocument("times", new BsonInt32(times))) .append("data", new BsonDocument() From c842d226108d0765b7f930e1522f10e8a474e52d Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Thu, 25 Apr 2024 16:56:11 -0600 Subject: [PATCH 08/25] PR fixes --- .../authentication/AzureCredentialHelper.java | 12 +++++++++++- .../authentication/GcpCredentialHelper.java | 8 ++++---- .../com/mongodb/client/unified/Entities.java | 13 ++++++------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/driver-core/src/main/com/mongodb/internal/authentication/AzureCredentialHelper.java b/driver-core/src/main/com/mongodb/internal/authentication/AzureCredentialHelper.java index 9f325c6f251..496ea1b1668 100644 --- a/driver-core/src/main/com/mongodb/internal/authentication/AzureCredentialHelper.java +++ b/driver-core/src/main/com/mongodb/internal/authentication/AzureCredentialHelper.java @@ -23,6 +23,8 @@ import org.bson.BsonString; import org.bson.json.JsonParseException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; import java.time.Duration; import java.util.HashMap; import java.util.Map; @@ -72,7 +74,7 @@ public static BsonDocument obtainFromEnvironment() { public static CredentialInfo fetchAzureCredentialInfo(final String resource, @Nullable final String clientId) { String endpoint = "http://169.254.169.254:80" + "/metadata/identity/oauth2/token?api-version=2018-02-01" - + "&resource=" + resource + + "&resource=" + getEncoded(resource) + (clientId == null ? "" : "&client_id=" + clientId); Map headers = new HashMap<>(); @@ -99,6 +101,14 @@ public static CredentialInfo fetchAzureCredentialInfo(final String resource, @Nu return new CredentialInfo(accessToken, Duration.ofSeconds(expiresInSeconds)); } + static String getEncoded(final String resource) { + try { + return URLEncoder.encode(resource, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + private AzureCredentialHelper() { } } diff --git a/driver-core/src/main/com/mongodb/internal/authentication/GcpCredentialHelper.java b/driver-core/src/main/com/mongodb/internal/authentication/GcpCredentialHelper.java index 697860a312d..3f0272da48c 100644 --- a/driver-core/src/main/com/mongodb/internal/authentication/GcpCredentialHelper.java +++ b/driver-core/src/main/com/mongodb/internal/authentication/GcpCredentialHelper.java @@ -18,12 +18,12 @@ import com.mongodb.MongoClientException; import org.bson.BsonDocument; -import org.bson.BsonString; import java.time.Duration; import java.util.HashMap; import java.util.Map; +import static com.mongodb.internal.authentication.AzureCredentialHelper.getEncoded; import static com.mongodb.internal.authentication.HttpHelper.getHttpContents; /** @@ -46,14 +46,14 @@ public static BsonDocument obtainFromEnvironment() { } } - public static CredentialInfo fetchGcpCredentialInfo(final String resource) { + public static CredentialInfo fetchGcpCredentialInfo(final String audience) { String endpoint = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=" - + resource; + + getEncoded(audience); Map header = new HashMap<>(); header.put("Metadata-Flavor", "Google"); String response = getHttpContents("GET", endpoint, header); return new CredentialInfo( - new BsonString(response).getValue(), + response, Duration.ZERO); } diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java index d9e049100fa..5a86d52c4de 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java @@ -27,7 +27,6 @@ import com.mongodb.ServerApiVersion; import com.mongodb.TransactionOptions; import com.mongodb.WriteConcern; -import com.mongodb.assertions.Assertions; import com.mongodb.client.ClientSession; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoCollection; @@ -92,6 +91,8 @@ import static com.mongodb.ClusterFixture.getMultiMongosConnectionString; import static com.mongodb.ClusterFixture.isLoadBalanced; import static com.mongodb.ClusterFixture.isSharded; +import static com.mongodb.assertions.Assertions.assertNotNull; +import static com.mongodb.assertions.Assertions.notNull; import static com.mongodb.client.Fixture.getMongoClientSettingsBuilder; import static com.mongodb.client.Fixture.getMultiMongosMongoClientSettingsBuilder; import static com.mongodb.client.unified.EventMatcher.getReasonString; @@ -530,13 +531,11 @@ private void initClient(final BsonDocument entity, final String id, boolean hasPlaceholder = authMechanismProperties.equals( new BsonDocument("$$placeholder", new BsonInt32(1))); if (!hasPlaceholder) { - throw new UnsupportedOperationException("Unsupported authMechanism: " + value); + throw new UnsupportedOperationException( + "Unsupported authMechanismProperties for authMechanism: " + value); } - String env = getenv("OIDC_ENV"); - if (env == null) { - env = "test"; - } + String env = assertNotNull(getenv("OIDC_ENV")); MongoCredential oidcCredential = MongoCredential .createOidcCredential(null) .withMechanismProperty("ENVIRONMENT", env); @@ -723,7 +722,7 @@ private void initClientEncryption(final BsonDocument entity, final String id, } } - putEntity(id, clientEncryptionSupplier.apply(Assertions.notNull("mongoClient", mongoClient), builder.build()), clientEncryptions); + putEntity(id, clientEncryptionSupplier.apply(notNull("mongoClient", mongoClient), builder.build()), clientEncryptions); } private TransactionOptions getTransactionOptions(final BsonDocument options) { From 3cea409cf3b45f387137cf46e2b868324a9ca4f2 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Fri, 26 Apr 2024 07:46:47 -0600 Subject: [PATCH 09/25] Apply suggestions from code review Co-authored-by: Valentin Kovalenko --- driver-core/src/main/com/mongodb/MongoCredential.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/driver-core/src/main/com/mongodb/MongoCredential.java b/driver-core/src/main/com/mongodb/MongoCredential.java index 7b06e3dbfff..1df71064678 100644 --- a/driver-core/src/main/com/mongodb/MongoCredential.java +++ b/driver-core/src/main/com/mongodb/MongoCredential.java @@ -194,6 +194,7 @@ public final class MongoCredential { * must not be provided. * * @see #createOidcCredential(String) + * @see MongoCredential#TOKEN_RESOURCE_KEY * @since 5.1 */ public static final String ENVIRONMENT_KEY = "ENVIRONMENT"; @@ -257,6 +258,8 @@ public final class MongoCredential { /** * The token resource. * + * @see MongoCredential#ENVIRONMENT_KEY + * @see #createOidcCredential(String) * @since 5.1 */ public static final String TOKEN_RESOURCE_KEY = "TOKEN_RESOURCE"; From e8832795b05fae70cf49f8fcb85bcc2c478367c9 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Fri, 26 Apr 2024 07:48:12 -0600 Subject: [PATCH 10/25] Update driver-core/src/main/com/mongodb/MongoCredential.java Co-authored-by: Valentin Kovalenko --- driver-core/src/main/com/mongodb/MongoCredential.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/driver-core/src/main/com/mongodb/MongoCredential.java b/driver-core/src/main/com/mongodb/MongoCredential.java index 1df71064678..6089a56d013 100644 --- a/driver-core/src/main/com/mongodb/MongoCredential.java +++ b/driver-core/src/main/com/mongodb/MongoCredential.java @@ -659,7 +659,7 @@ public String toString() { @Evolving public interface OidcCallbackContext { /** - * @return Convenience method to obtain the username. + * @return Convenience method to obtain the {@linkplain MongoCredential#getUserName() username}. */ @Nullable String getUserName(); From bc30a2f9bbd7a9979cd7b560c0657fd070f4e057 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Fri, 26 Apr 2024 07:48:33 -0600 Subject: [PATCH 11/25] Update driver-core/src/main/com/mongodb/internal/authentication/CredentialInfo.java Co-authored-by: Valentin Kovalenko --- .../com/mongodb/internal/authentication/CredentialInfo.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/driver-core/src/main/com/mongodb/internal/authentication/CredentialInfo.java b/driver-core/src/main/com/mongodb/internal/authentication/CredentialInfo.java index d4b45f557e3..8b1e601b13a 100644 --- a/driver-core/src/main/com/mongodb/internal/authentication/CredentialInfo.java +++ b/driver-core/src/main/com/mongodb/internal/authentication/CredentialInfo.java @@ -25,6 +25,10 @@ public final class CredentialInfo { private final String accessToken; private final Duration expiresIn; + /** + * @param expiresIn The meaning of {@linkplain Duration#isZero() zero-length} duration is the same as in + * {@link com.mongodb.MongoCredential.OidcCallbackResult#OidcCallbackResult(String, Duration)}. + */ public CredentialInfo(final String accessToken, final Duration expiresIn) { this.accessToken = accessToken; this.expiresIn = expiresIn; From d856d84a2b6793528253cacd0c81034803e49582 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Fri, 26 Apr 2024 09:14:04 -0600 Subject: [PATCH 12/25] Implement OIDC map value splitting --- .../main/com/mongodb/ConnectionString.java | 24 +++++-- .../auth/legacy/connection-string.json | 70 ++++++++++++++++++- .../com/mongodb/ConnectionStringUnitTest.java | 29 ++++++++ 3 files changed, 117 insertions(+), 6 deletions(-) diff --git a/driver-core/src/main/com/mongodb/ConnectionString.java b/driver-core/src/main/com/mongodb/ConnectionString.java index 375fa160ab3..f146d3c7479 100644 --- a/driver-core/src/main/com/mongodb/ConnectionString.java +++ b/driver-core/src/main/com/mongodb/ConnectionString.java @@ -38,6 +38,7 @@ import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -923,6 +924,9 @@ private MongoCredential createCredentials(final Map> option } String key = mechanismPropertyKeyValue[0].trim().toLowerCase(); String value = mechanismPropertyKeyValue[1].trim(); + if (!decodeWholeOptionValue(mechanism)) { + value = urldecode(value); + } if (MECHANISM_KEYS_DISALLOWED_IN_CONNECTION_STRING.contains(key)) { throw new IllegalArgumentException(format("The connection string contains disallowed mechanism properties. " + "'%s' must be set on the credential programmatically.", key)); @@ -938,6 +942,13 @@ private MongoCredential createCredentials(final Map> option return credential; } + private static boolean decodeWholeOptionValue(final AuthenticationMechanism mechanism) { + return !AuthenticationMechanism.MONGODB_OIDC.equals(mechanism); + } + private static boolean decodeWholeOptionValue(final List options) { + return !options.contains("authMechanism=" + AuthenticationMechanism.MONGODB_OIDC.getMechanismName()); + } + private MongoCredential createMongoCredentialWithMechanism(final AuthenticationMechanism mechanism, final String userName, @Nullable final char[] password, @Nullable final String authSource, @@ -1018,12 +1029,14 @@ private String getLastValue(final Map> optionsMap, final St private Map> parseOptions(final String optionsPart) { Map> optionsMap = new HashMap<>(); - if (optionsPart.length() == 0) { + if (optionsPart.isEmpty()) { return optionsMap; } - for (final String part : optionsPart.split("&|;")) { - if (part.length() == 0) { + List options = Arrays.asList(optionsPart.split("&|;")); + boolean decodeWholeOptionValue = decodeWholeOptionValue(options); + for (final String part : options) { + if (part.isEmpty()) { continue; } int idx = part.indexOf("="); @@ -1034,7 +1047,10 @@ private Map> parseOptions(final String optionsPart) { if (valueList == null) { valueList = new ArrayList<>(1); } - valueList.add(urldecode(value)); + if (decodeWholeOptionValue) { + value = urldecode(value); + } + valueList.add(value); optionsMap.put(key, valueList); } else { throw new IllegalArgumentException(format("The connection string contains an invalid option '%s'. " diff --git a/driver-core/src/test/resources/auth/legacy/connection-string.json b/driver-core/src/test/resources/auth/legacy/connection-string.json index d4baa89b6eb..991d38887ed 100644 --- a/driver-core/src/test/resources/auth/legacy/connection-string.json +++ b/driver-core/src/test/resources/auth/legacy/connection-string.json @@ -474,13 +474,13 @@ } }, { - "description": "should throw an exception if supplied a password (MONGODB-OIDC)", + "description": "should throw an exception if username and password is specified for test environment (MONGODB-OIDC)", "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test", "valid": false, "credential": null }, { - "description": "should throw an exception if username is specified for test (MONGODB-OIDC)", + "description": "should throw an exception if username is specified for test environment (MONGODB-OIDC)", "uri": "mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&ENVIRONMENT:test", "valid": false, "credential": null @@ -503,6 +503,12 @@ "valid": false, "credential": null }, + { + "description": "should throw an exception if neither provider nor callbacks specified (MONGODB-OIDC)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC", + "valid": false, + "credential": null + }, { "description": "should recognise the mechanism with azure provider (MONGODB-OIDC)", "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo", @@ -533,6 +539,66 @@ } } }, + { + "description": "should accept a url-encoded TOKEN_RESOURCE (MONGODB-OIDC)", + "uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:mongodb%3A%2F%2Ftest-cluster", + "valid": true, + "credential": { + "username": "user", + "password": null, + "source": "$external", + "mechanism": "MONGODB-OIDC", + "mechanism_properties": { + "ENVIRONMENT": "azure", + "TOKEN_RESOURCE": "mongodb://test-cluster" + } + } + }, + { + "description": "should accept an un-encoded TOKEN_RESOURCE (MONGODB-OIDC)", + "uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:mongodb://test-cluster", + "valid": true, + "credential": { + "username": "user", + "password": null, + "source": "$external", + "mechanism": "MONGODB-OIDC", + "mechanism_properties": { + "ENVIRONMENT": "azure", + "TOKEN_RESOURCE": "mongodb://test-cluster" + } + } + }, + { + "description": "should handle a complicated url-encoded TOKEN_RESOURCE (MONGODB-OIDC)", + "uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:abc%2Cd%25ef%3Ag%26hi", + "valid": true, + "credential": { + "username": "user", + "password": null, + "source": "$external", + "mechanism": "MONGODB-OIDC", + "mechanism_properties": { + "ENVIRONMENT": "azure", + "TOKEN_RESOURCE": "abc,d%ef:g&hi" + } + } + }, + { + "description": "should url-encode a TOKEN_RESOURCE (MONGODB-OIDC)", + "uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:a$b", + "valid": true, + "credential": { + "username": "user", + "password": null, + "source": "$external", + "mechanism": "MONGODB-OIDC", + "mechanism_properties": { + "ENVIRONMENT": "azure", + "TOKEN_RESOURCE": "a$b" + } + } + }, { "description": "should accept a username and throw an error for a password with azure provider (MONGODB-OIDC)", "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo", diff --git a/driver-core/src/test/unit/com/mongodb/ConnectionStringUnitTest.java b/driver-core/src/test/unit/com/mongodb/ConnectionStringUnitTest.java index d2e41ebeafd..6d52cd199a9 100644 --- a/driver-core/src/test/unit/com/mongodb/ConnectionStringUnitTest.java +++ b/driver-core/src/test/unit/com/mongodb/ConnectionStringUnitTest.java @@ -20,6 +20,10 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -34,6 +38,31 @@ void defaults() { assertAll(() -> assertNull(connectionStringDefault.getServerMonitoringMode())); } + @Test + public void mustDecodeOidcIndividually() { + String string = "abc,d!@#$%^&*;ef:ghi"; + ConnectionString cs = new ConnectionString( + "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=" + + "ENVIRONMENT:azure,TOKEN_RESOURCE:" + encode(string)); + assertEquals(string, cs.getCredential().getMechanismProperty("TOKEN_RESOURCE", null)); + } + + @Test + public void mustDecodeNonOidcAsWhole() { + ConnectionString cs2 = new ConnectionString( + "mongodb://foo:bar@example.com/?authMechanism=GSSAPI&authMechanismProperties=" + + encode("SERVICE_NAME:other,CANONICALIZE_HOST_NAME:true&authSource=$external")); + assertEquals("other", cs2.getCredential().getMechanismProperty("SERVICE_NAME", null)); + } + + private static String encode(final String string) { + try { + return URLEncoder.encode(string, StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + @ParameterizedTest @ValueSource(strings = {DEFAULT_OPTIONS + "serverMonitoringMode=stream"}) void equalAndHashCode(final String connectionString) { From 479fcddaa743f2e8cc85733e548d4b9fe67b16b4 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Fri, 26 Apr 2024 10:14:55 -0600 Subject: [PATCH 13/25] PR fixes, doc updates --- .../src/main/com/mongodb/MongoCredential.java | 24 +++++++++++++++---- .../com/mongodb/client/unified/Entities.java | 10 ++++---- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/driver-core/src/main/com/mongodb/MongoCredential.java b/driver-core/src/main/com/mongodb/MongoCredential.java index 6089a56d013..393eda6bbb5 100644 --- a/driver-core/src/main/com/mongodb/MongoCredential.java +++ b/driver-core/src/main/com/mongodb/MongoCredential.java @@ -186,7 +186,13 @@ public final class MongoCredential { public static final String AWS_CREDENTIAL_PROVIDER_KEY = "AWS_CREDENTIAL_PROVIDER"; /** - * The environment. The value must be a string. + * Mechanism property key for specifying the environment for OIDC. + * The value must be either "gcp" or "azure". + * The environment determines how the driver should obtain an access token, + * as an alternative to supplying a callback. + *

+ * The "gcp" and "azure" environments require + * {@link MongoCredential#TOKEN_RESOURCE_KEY} to be specified. *

* If this is provided, * {@link MongoCredential#OIDC_CALLBACK_KEY} and @@ -200,6 +206,7 @@ public final class MongoCredential { public static final String ENVIRONMENT_KEY = "ENVIRONMENT"; /** + * Mechanism property key for the OIDC callback. * This callback is invoked when the OIDC-based authenticator requests * a token. The type of the value must be {@link OidcCallback}. * {@link IdpInfo} will not be supplied to the callback, @@ -216,6 +223,7 @@ public final class MongoCredential { public static final String OIDC_CALLBACK_KEY = "OIDC_CALLBACK"; /** + * Mechanism property key for the OIDC human callback. * This callback is invoked when the OIDC-based authenticator requests * a token from the identity provider (IDP) using the IDP information * from the MongoDB server. The type of the value must be @@ -232,7 +240,7 @@ public final class MongoCredential { /** - * Mechanism key for a list of allowed hostnames or ip-addresses for MongoDB connections. Ports must be excluded. + * Mechanism property key for a list of allowed hostnames or ip-addresses for MongoDB connections. Ports must be excluded. * The hostnames may include a leading "*." wildcard, which allows for matching (potentially nested) subdomains. * When MONGODB-OIDC authentication is attempted against a hostname that does not match any of list of allowed hosts * the driver will raise an error. The type of the value must be {@code List}. @@ -256,7 +264,8 @@ public final class MongoCredential { "*.mongodb.net", "*.mongodb-qa.net", "*.mongodb-dev.net", "*.mongodbgov.net", "localhost", "127.0.0.1", "::1")); /** - * The token resource. + * Mechanism property key for specifying he URI of the target resource (sometimes called the audience), + * used in some OIDC environments. * * @see MongoCredential#ENVIRONMENT_KEY * @see #createOidcCredential(String) @@ -420,6 +429,7 @@ public static MongoCredential createAwsCredential(@Nullable final String userNam * @since 5.1 * @see #withMechanismProperty(String, Object) * @see #ENVIRONMENT_KEY + * @see #TOKEN_RESOURCE_KEY * @see #OIDC_CALLBACK_KEY * @see #OIDC_HUMAN_CALLBACK_KEY * @see #ALLOWED_HOSTS_KEY @@ -675,13 +685,17 @@ public interface OidcCallbackContext { int getVersion(); /** - * @return The OIDC Identity Provider's configuration that can be used to acquire an Access Token. + * @return The OIDC Identity Provider's configuration that can be used + * to acquire an Access Token, or null if not using a + * {@linkplain MongoCredential#OIDC_HUMAN_CALLBACK_KEY human callback.} */ @Nullable IdpInfo getIdpInfo(); /** - * @return The OIDC Refresh token supplied by a prior callback invocation. + * @return The OIDC Refresh token supplied by a prior callback invocation, + * or null if no token was supplied, or if not using a + * {@linkplain MongoCredential#OIDC_HUMAN_CALLBACK_KEY human callback.} */ @Nullable String getRefreshToken(); diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java index 5a86d52c4de..0e0558c63ee 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java @@ -540,11 +540,11 @@ private void initClient(final BsonDocument entity, final String id, .createOidcCredential(null) .withMechanismProperty("ENVIRONMENT", env); if (env.equals("azure")) { - oidcCredential = oidcCredential - .withMechanismProperty("TOKEN_RESOURCE", getenv("AZUREOIDC_RESOURCE")); + oidcCredential = oidcCredential.withMechanismProperty( + MongoCredential.TOKEN_RESOURCE_KEY, getenv("AZUREOIDC_RESOURCE")); } else if (env.equals("gcp")) { - oidcCredential = oidcCredential - .withMechanismProperty("TOKEN_RESOURCE", getenv("GCPOIDC_RESOURCE")); + oidcCredential = oidcCredential.withMechanismProperty( + MongoCredential.TOKEN_RESOURCE_KEY, getenv("GCPOIDC_RESOURCE")); } clientSettingsBuilder.credential(oidcCredential); break; @@ -556,7 +556,7 @@ private void initClient(final BsonDocument entity, final String id, .getDocument("uriOptions") .get("authMechanism"); if (authMechanism.equals(new BsonString(MONGODB_OIDC.getMechanismName()))) { - break; // only OIDC supports authMechanismProperties + break; // authMechanismProperties only supported here for OIDC } throw new UnsupportedOperationException("Failure to apply authMechanismProperties: " + value); default: From 4a844b14cac11632b5cae0cf9dfd457c847b24e3 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Fri, 26 Apr 2024 12:41:36 -0600 Subject: [PATCH 14/25] PR fixes for OIDC feature branch --- .../src/main/com/mongodb/ConnectionString.java | 4 ++-- .../src/main/com/mongodb/internal/Locks.java | 2 +- .../internal/connection/OidcAuthenticator.java | 17 ++++++----------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/driver-core/src/main/com/mongodb/ConnectionString.java b/driver-core/src/main/com/mongodb/ConnectionString.java index f146d3c7479..1187744699a 100644 --- a/driver-core/src/main/com/mongodb/ConnectionString.java +++ b/driver-core/src/main/com/mongodb/ConnectionString.java @@ -230,9 +230,9 @@ * *

Authentication configuration:

*
    - *
  • {@code authMechanism=MONGO-CR|GSSAPI|PLAIN|MONGODB-X509}: The authentication mechanism to use if a credential was supplied. + *
  • {@code authMechanism=MONGO-CR|GSSAPI|PLAIN|MONGODB-X509|MONGODB-OIDC}: The authentication mechanism to use if a credential was supplied. * The default is unspecified, in which case the client will pick the most secure mechanism available based on the sever version. For the - * GSSAPI and MONGODB-X509 mechanisms, no password is accepted, only the username. + * GSSAPI, MONGODB-X509, and MONGODB-OIDC mechanisms, no password is accepted, only the username. *
  • *
  • {@code authSource=string}: The source of the authentication credentials. This is typically the database that * the credentials have been created. The value defaults to the database specified in the path portion of the connection string. diff --git a/driver-core/src/main/com/mongodb/internal/Locks.java b/driver-core/src/main/com/mongodb/internal/Locks.java index 2a169f45c52..7dd966f183b 100644 --- a/driver-core/src/main/com/mongodb/internal/Locks.java +++ b/driver-core/src/main/com/mongodb/internal/Locks.java @@ -54,7 +54,7 @@ public static void withLockAsync(final StampedLock lock, final AsyncRunnable run }, callback); } - public static void withLock(final StampedLock lock, final Runnable runnable) { + public static void withInterruptibleLock(final StampedLock lock, final Runnable runnable) { long stamp; try { stamp = lock.writeLockInterruptibly(); diff --git a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java index 705d74a3c2c..2102ed53bbd 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java +++ b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java @@ -126,7 +126,9 @@ protected SaslClient createSaslClient(final ServerAddress serverAddress) { @Nullable public BsonDocument createSpeculativeAuthenticateCommand(final InternalConnection connection) { try { - String cachedAccessToken = getCachedAccessToken(); + String cachedAccessToken = getMongoCredentialWithCache() + .getOidcCacheEntry() + .getCachedAccessToken(); if (cachedAccessToken != null) { return wrapInSpeculative(prepareTokenAsJwt(cachedAccessToken)); } else { @@ -284,7 +286,7 @@ private void authenticationLoopAsync(final InternalConnection connection, final private byte[] evaluate(final byte[] challenge) { byte[][] jwt = new byte[1][]; - Locks.withLock(getMongoCredentialWithCache().getOidcLock(), () -> { + Locks.withInterruptibleLock(getMongoCredentialWithCache().getOidcLock(), () -> { OidcCacheEntry oidcCacheEntry = getMongoCredentialWithCache().getOidcCacheEntry(); String cachedRefreshToken = oidcCacheEntry.getRefreshToken(); IdpInfo cachedIdpInfo = oidcCacheEntry.getIdpInfo(); @@ -358,7 +360,7 @@ private byte[] evaluate(final byte[] challenge) { private String validatedCachedAccessToken() { MongoCredentialWithCache mongoCredentialWithCache = getMongoCredentialWithCache(); OidcCacheEntry cacheEntry = mongoCredentialWithCache.getOidcCacheEntry(); - String cachedAccessToken = getCachedAccessToken(); + String cachedAccessToken = cacheEntry.getCachedAccessToken(); String invalidConnectionAccessToken = connectionLastAccessToken; if (cachedAccessToken != null) { @@ -377,7 +379,7 @@ private boolean clientIsComplete() { private boolean shouldRetryHandler() { boolean[] result = new boolean[1]; - Locks.withLock(getMongoCredentialWithCache().getOidcLock(), () -> { + Locks.withInterruptibleLock(getMongoCredentialWithCache().getOidcLock(), () -> { MongoCredentialWithCache mongoCredentialWithCache = getMongoCredentialWithCache(); OidcCacheEntry cacheEntry = mongoCredentialWithCache.getOidcCacheEntry(); if (fallbackState == FallbackState.PHASE_1_CACHED_TOKEN) { @@ -402,13 +404,6 @@ private boolean shouldRetryHandler() { return result[0]; } - @Nullable - private String getCachedAccessToken() { - return getMongoCredentialWithCache() - .getOidcCacheEntry() - .getCachedAccessToken(); - } - static final class OidcCacheEntry { @Nullable private final String accessToken; From cc1c7eccd48c2a38c9362a5fa385a4e56ec1dcf2 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Fri, 26 Apr 2024 12:58:38 -0600 Subject: [PATCH 15/25] Update connection-string latest specifications/pull/1569 --- .../test/resources/auth/legacy/connection-string.json | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/driver-core/src/test/resources/auth/legacy/connection-string.json b/driver-core/src/test/resources/auth/legacy/connection-string.json index 991d38887ed..072dd176dc8 100644 --- a/driver-core/src/test/resources/auth/legacy/connection-string.json +++ b/driver-core/src/test/resources/auth/legacy/connection-string.json @@ -474,13 +474,13 @@ } }, { - "description": "should throw an exception if username and password is specified for test environment (MONGODB-OIDC)", + "description": "should throw an exception if supplied a password (MONGODB-OIDC)", "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test", "valid": false, "credential": null }, { - "description": "should throw an exception if username is specified for test environment (MONGODB-OIDC)", + "description": "should throw an exception if username is specified for test (MONGODB-OIDC)", "uri": "mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&ENVIRONMENT:test", "valid": false, "credential": null @@ -503,12 +503,6 @@ "valid": false, "credential": null }, - { - "description": "should throw an exception if neither provider nor callbacks specified (MONGODB-OIDC)", - "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC", - "valid": false, - "credential": null - }, { "description": "should recognise the mechanism with azure provider (MONGODB-OIDC)", "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo", From 678d7b7281225ec363111f8f6bce88e7e15cde06 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Fri, 26 Apr 2024 14:41:36 -0600 Subject: [PATCH 16/25] PR fixes --- .../connection/OidcAuthenticator.java | 49 ++++++++----------- .../OidcAuthenticationProseTests.java | 8 +-- 2 files changed, 24 insertions(+), 33 deletions(-) diff --git a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java index 2102ed53bbd..7276831fff7 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java +++ b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java @@ -53,14 +53,14 @@ import static com.mongodb.AuthenticationMechanism.MONGODB_OIDC; import static com.mongodb.MongoCredential.ALLOWED_HOSTS_KEY; -import static com.mongodb.MongoCredential.TOKEN_RESOURCE_KEY; import static com.mongodb.MongoCredential.DEFAULT_ALLOWED_HOSTS; +import static com.mongodb.MongoCredential.ENVIRONMENT_KEY; import static com.mongodb.MongoCredential.IdpInfo; +import static com.mongodb.MongoCredential.OIDC_CALLBACK_KEY; import static com.mongodb.MongoCredential.OIDC_HUMAN_CALLBACK_KEY; import static com.mongodb.MongoCredential.OidcCallback; import static com.mongodb.MongoCredential.OidcCallbackContext; -import static com.mongodb.MongoCredential.ENVIRONMENT_KEY; -import static com.mongodb.MongoCredential.OIDC_CALLBACK_KEY; +import static com.mongodb.MongoCredential.TOKEN_RESOURCE_KEY; import static com.mongodb.assertions.Assertions.assertFalse; import static com.mongodb.assertions.Assertions.assertNotNull; import static com.mongodb.assertions.Assertions.assertTrue; @@ -78,7 +78,7 @@ public final class OidcAuthenticator extends SaslAuthenticator { private static final String GCP_ENVIRONMENT = "gcp"; private static final List SUPPORTED_ENVIRONMENTS = Arrays.asList( AZURE_ENVIRONMENT, GCP_ENVIRONMENT, TEST_ENVIRONMENT); - private static final List SUPPORTS_TOKEN_RESOURCE = Arrays.asList( + private static final List REQUIRES_TOKEN_RESOURCE = Arrays.asList( AZURE_ENVIRONMENT, GCP_ENVIRONMENT); private static final List ALLOWS_USERNAME = Arrays.asList( AZURE_ENVIRONMENT); @@ -177,7 +177,7 @@ private OidcCallback getOidcCallbackMechanismProperty(final String key) { } private OidcCallback getRequestCallback() { - String environment = getEnvironmentName(getMongoCredential()); + String environment = getMongoCredential().getMechanismProperty(ENVIRONMENT_KEY, null); OidcCallback machine; if (TEST_ENVIRONMENT.equals(environment)) { machine = getTestCallback(); @@ -193,7 +193,7 @@ private OidcCallback getRequestCallback() { } @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) - public static OidcCallback getTestCallback() { + static OidcCallback getTestCallback() { return (context) -> { String accessToken = readTokenFromFile(); return new OidcCallbackResult(accessToken); @@ -201,7 +201,7 @@ public static OidcCallback getTestCallback() { } @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) - public static OidcCallback getAzureCallback(final MongoCredential credential) { + static OidcCallback getAzureCallback(final MongoCredential credential) { return (context) -> { String resource = assertNotNull(credential.getMechanismProperty(TOKEN_RESOURCE_KEY, null)); String clientId = credential.getUserName(); @@ -211,7 +211,7 @@ public static OidcCallback getAzureCallback(final MongoCredential credential) { } @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) - public static OidcCallback getGcpCallback(final MongoCredential credential) { + static OidcCallback getGcpCallback(final MongoCredential credential) { return (context) -> { String resource = assertNotNull(credential.getMechanismProperty(TOKEN_RESOURCE_KEY, null)); CredentialInfo response = GcpCredentialHelper.fetchGcpCredentialInfo(resource); @@ -584,9 +584,9 @@ public static void validateOidcCredentialConstruction( throw new IllegalArgumentException("source must be '$external'"); } - String providerName = getEnvironmentName(mechanismProperties); - if (providerName != null) { - if (!SUPPORTED_ENVIRONMENTS.contains(providerName)) { + Object environmentName = mechanismProperties.get(ENVIRONMENT_KEY.toLowerCase()); + if (environmentName != null) { + if (!(environmentName instanceof String) || !SUPPORTED_ENVIRONMENTS.contains(environmentName)) { throw new IllegalArgumentException(ENVIRONMENT_KEY + " must be one of: " + SUPPORTED_ENVIRONMENTS); } } @@ -602,10 +602,10 @@ public static void validateCreateOidcCredential(@Nullable final char[] password) @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) public static void validateBeforeUse(final MongoCredential credential) { String userName = credential.getUserName(); - Object providerName = credential.getMechanismProperty(ENVIRONMENT_KEY, null); + Object environmentName = credential.getMechanismProperty(ENVIRONMENT_KEY, null); Object machineCallback = credential.getMechanismProperty(OIDC_CALLBACK_KEY, null); Object humanCallback = credential.getMechanismProperty(OIDC_HUMAN_CALLBACK_KEY, null); - if (providerName == null) { + if (environmentName == null) { // callback if (machineCallback == null && humanCallback == null) { throw new IllegalArgumentException("Either " + ENVIRONMENT_KEY @@ -619,7 +619,10 @@ public static void validateBeforeUse(final MongoCredential credential) { + " must not be specified"); } } else { - if (userName != null && !ALLOWS_USERNAME.contains(providerName)) { + if (!(environmentName instanceof String)) { + throw new IllegalArgumentException(ENVIRONMENT_KEY + " must be a String"); + } + if (userName != null && !ALLOWS_USERNAME.contains(environmentName)) { throw new IllegalArgumentException("user name must not be specified when " + ENVIRONMENT_KEY + " is specified"); } if (machineCallback != null) { @@ -630,30 +633,18 @@ public static void validateBeforeUse(final MongoCredential credential) { } String tokenResource = credential.getMechanismProperty(TOKEN_RESOURCE_KEY, null); boolean hasTokenResourceProperty = tokenResource != null; - boolean tokenResourceSupported = SUPPORTS_TOKEN_RESOURCE.contains(providerName); + boolean tokenResourceSupported = REQUIRES_TOKEN_RESOURCE.contains(environmentName); if (hasTokenResourceProperty != tokenResourceSupported) { throw new IllegalArgumentException(TOKEN_RESOURCE_KEY + " must be provided if and only if " + ENVIRONMENT_KEY - + " " + providerName + " " - + " is one of: " + SUPPORTS_TOKEN_RESOURCE + + " " + environmentName + " " + + " is one of: " + REQUIRES_TOKEN_RESOURCE + ". " + TOKEN_RESOURCE_KEY + ": " + tokenResource); } } } } - @Nullable - private static String getEnvironmentName(final Map mechanismProperties) { - Object o = mechanismProperties.get(ENVIRONMENT_KEY.toLowerCase()); - return o instanceof String ? (String) o : null; - } - - @Nullable - private static String getEnvironmentName(final MongoCredential credential) { - Object o = credential.getMechanismProperty(ENVIRONMENT_KEY, null); - return o instanceof String ? (String) o : null; - } - @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) static class OidcCallbackContextImpl implements OidcCallbackContext { private final Duration timeout; diff --git a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java index 76877af199e..1481db406dd 100644 --- a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java +++ b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java @@ -70,7 +70,7 @@ import static java.lang.System.getenv; import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -364,11 +364,11 @@ private static void performInsert(final MongoClient mongoClient) { } @Test - public void test5p1Azure() { + public void test5p1AzureSucceedsWithNoUsername() { assumeTrue(getOidcEnv().equals("azure")); String oidcUri = getOidcUri(); - assertFalse(oidcUri.contains("@")); MongoClientSettings clientSettings = createSettings(oidcUri, createCallback(), null); + assertNull(clientSettings.getCredential().getUserName()); try (MongoClient mongoClient = createMongoClient(clientSettings)) { // #. Perform a find operation that succeeds. performFind(mongoClient); @@ -376,7 +376,7 @@ public void test5p1Azure() { } @Test - public void test5p1AzureFails() { + public void test5p2AzureFailsWithBadUsername() { assumeTrue(getOidcEnv().equals("azure")); String oidcUri = getOidcUri(); oidcUri = oidcUri.replace("://", "://bad@"); From fcb65dc2ee93cf6089ff5def3b0d9d600320107b Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Fri, 26 Apr 2024 15:21:17 -0600 Subject: [PATCH 17/25] PR fixes --- .../src/main/com/mongodb/MongoCredential.java | 8 ++++---- .../src/main/com/mongodb/internal/Locks.java | 20 +------------------ .../authentication/AzureCredentialHelper.java | 2 +- .../connection/OidcAuthenticator.java | 6 +++--- .../com/mongodb/client/unified/Entities.java | 2 +- .../OidcAuthenticationProseTests.java | 11 +++++++--- 6 files changed, 18 insertions(+), 31 deletions(-) diff --git a/driver-core/src/main/com/mongodb/MongoCredential.java b/driver-core/src/main/com/mongodb/MongoCredential.java index 393eda6bbb5..e085ac074f0 100644 --- a/driver-core/src/main/com/mongodb/MongoCredential.java +++ b/driver-core/src/main/com/mongodb/MongoCredential.java @@ -186,10 +186,10 @@ public final class MongoCredential { public static final String AWS_CREDENTIAL_PROVIDER_KEY = "AWS_CREDENTIAL_PROVIDER"; /** - * Mechanism property key for specifying the environment for OIDC. - * The value must be either "gcp" or "azure". - * The environment determines how the driver should obtain an access token, - * as an alternative to supplying a callback. + * Mechanism property key for specifying the environment for OIDC, which is + * the name of a built-in OIDC application environment integration to use + * to obtain credentials. The value must be either "gcp" or "azure". + * This is an alternative to supplying a callback. *

    * The "gcp" and "azure" environments require * {@link MongoCredential#TOKEN_RESOURCE_KEY} to be specified. diff --git a/driver-core/src/main/com/mongodb/internal/Locks.java b/driver-core/src/main/com/mongodb/internal/Locks.java index 7dd966f183b..984de156f27 100644 --- a/driver-core/src/main/com/mongodb/internal/Locks.java +++ b/driver-core/src/main/com/mongodb/internal/Locks.java @@ -17,8 +17,6 @@ package com.mongodb.internal; import com.mongodb.MongoInterruptedException; -import com.mongodb.internal.async.AsyncRunnable; -import com.mongodb.internal.async.SingleResultCallback; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -38,23 +36,7 @@ public static void withLock(final Lock lock, final Runnable action) { }); } - public static void withLockAsync(final StampedLock lock, final AsyncRunnable runnable, - final SingleResultCallback callback) { - long stamp; - try { - stamp = lock.writeLockInterruptibly(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - callback.onResult(null, new MongoInterruptedException("Interrupted waiting for lock", e)); - return; - } - - runnable.thenAlwaysRunAndFinish(() -> { - lock.unlockWrite(stamp); - }, callback); - } - - public static void withInterruptibleLock(final StampedLock lock, final Runnable runnable) { + public static void withInterruptibleLock(final StampedLock lock, final Runnable runnable) throws MongoInterruptedException{ long stamp; try { stamp = lock.writeLockInterruptibly(); diff --git a/driver-core/src/main/com/mongodb/internal/authentication/AzureCredentialHelper.java b/driver-core/src/main/com/mongodb/internal/authentication/AzureCredentialHelper.java index 496ea1b1668..2a48b8b6fc3 100644 --- a/driver-core/src/main/com/mongodb/internal/authentication/AzureCredentialHelper.java +++ b/driver-core/src/main/com/mongodb/internal/authentication/AzureCredentialHelper.java @@ -75,7 +75,7 @@ public static CredentialInfo fetchAzureCredentialInfo(final String resource, @Nu String endpoint = "http://169.254.169.254:80" + "/metadata/identity/oauth2/token?api-version=2018-02-01" + "&resource=" + getEncoded(resource) - + (clientId == null ? "" : "&client_id=" + clientId); + + (clientId == null ? "" : "&client_id=" + getEncoded(clientId)); Map headers = new HashMap<>(); headers.put("Metadata", "true"); diff --git a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java index 7276831fff7..d3db5ab9ab2 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java +++ b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java @@ -76,7 +76,7 @@ public final class OidcAuthenticator extends SaslAuthenticator { private static final String TEST_ENVIRONMENT = "test"; private static final String AZURE_ENVIRONMENT = "azure"; private static final String GCP_ENVIRONMENT = "gcp"; - private static final List SUPPORTED_ENVIRONMENTS = Arrays.asList( + private static final List IMPLEMENTED_ENVIRONMENTS = Arrays.asList( AZURE_ENVIRONMENT, GCP_ENVIRONMENT, TEST_ENVIRONMENT); private static final List REQUIRES_TOKEN_RESOURCE = Arrays.asList( AZURE_ENVIRONMENT, GCP_ENVIRONMENT); @@ -586,8 +586,8 @@ public static void validateOidcCredentialConstruction( Object environmentName = mechanismProperties.get(ENVIRONMENT_KEY.toLowerCase()); if (environmentName != null) { - if (!(environmentName instanceof String) || !SUPPORTED_ENVIRONMENTS.contains(environmentName)) { - throw new IllegalArgumentException(ENVIRONMENT_KEY + " must be one of: " + SUPPORTED_ENVIRONMENTS); + if (!(environmentName instanceof String) || !IMPLEMENTED_ENVIRONMENTS.contains(environmentName)) { + throw new IllegalArgumentException(ENVIRONMENT_KEY + " must be one of: " + IMPLEMENTED_ENVIRONMENTS); } } } diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java index 0e0558c63ee..76e49d68cdb 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java @@ -556,7 +556,7 @@ private void initClient(final BsonDocument entity, final String id, .getDocument("uriOptions") .get("authMechanism"); if (authMechanism.equals(new BsonString(MONGODB_OIDC.getMechanismName()))) { - break; // authMechanismProperties only supported here for OIDC + break; } throw new UnsupportedOperationException("Failure to apply authMechanismProperties: " + value); default: diff --git a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java index 1481db406dd..e88ad2455b5 100644 --- a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java +++ b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java @@ -23,6 +23,7 @@ import com.mongodb.MongoCredential; import com.mongodb.MongoSecurityException; import com.mongodb.MongoSocketException; +import com.mongodb.assertions.Assertions; import com.mongodb.client.Fixture; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; @@ -61,11 +62,13 @@ import java.util.stream.Collectors; import static com.mongodb.MongoCredential.ALLOWED_HOSTS_KEY; +import static com.mongodb.MongoCredential.ENVIRONMENT_KEY; import static com.mongodb.MongoCredential.OIDC_CALLBACK_KEY; import static com.mongodb.MongoCredential.OIDC_HUMAN_CALLBACK_KEY; import static com.mongodb.MongoCredential.OidcCallback; import static com.mongodb.MongoCredential.OidcCallbackContext; import static com.mongodb.MongoCredential.OidcCallbackResult; +import static com.mongodb.MongoCredential.TOKEN_RESOURCE_KEY; import static com.mongodb.assertions.Assertions.assertNotNull; import static java.lang.System.getenv; import static java.util.Arrays.asList; @@ -368,9 +371,11 @@ public void test5p1AzureSucceedsWithNoUsername() { assumeTrue(getOidcEnv().equals("azure")); String oidcUri = getOidcUri(); MongoClientSettings clientSettings = createSettings(oidcUri, createCallback(), null); - assertNull(clientSettings.getCredential().getUserName()); + // Create an OIDC configured client with `ENVIRONMENT:azure` and a valid + // `TOKEN_RESOURCE` and no username. + assertNull(Assertions.assertNotNull(clientSettings.getCredential()).getUserName()); try (MongoClient mongoClient = createMongoClient(clientSettings)) { - // #. Perform a find operation that succeeds. + // Perform a `find` operation that succeeds.. performFind(mongoClient); } } @@ -469,7 +474,7 @@ public void testAllowedHostsDisallowedInConnectionString() { } @Test - public void testh1p7() { + public void testh1p7AllowedHostsInConnectionStringIgnored() { // example.com changed to localhost String string = "mongodb+srv://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ALLOWED_HOSTS:%5B%22localhost%22%5D"; assertCause(IllegalArgumentException.class, From 71b38467138e7731046543bfe4fc351b1c794507 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Fri, 26 Apr 2024 16:11:13 -0600 Subject: [PATCH 18/25] PR fixes --- .../OidcAuthenticationProseTests.java | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java index e88ad2455b5..a3353a959f4 100644 --- a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java +++ b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java @@ -22,6 +22,7 @@ import com.mongodb.MongoConfigurationException; import com.mongodb.MongoCredential; import com.mongodb.MongoSecurityException; +import com.mongodb.MongoServerException; import com.mongodb.MongoSocketException; import com.mongodb.assertions.Assertions; import com.mongodb.client.Fixture; @@ -373,9 +374,11 @@ public void test5p1AzureSucceedsWithNoUsername() { MongoClientSettings clientSettings = createSettings(oidcUri, createCallback(), null); // Create an OIDC configured client with `ENVIRONMENT:azure` and a valid // `TOKEN_RESOURCE` and no username. + MongoCredential credential = Assertions.assertNotNull(new ConnectionString(oidcUri).getCredential()); + assertNotNull(credential.getMechanismProperty(TOKEN_RESOURCE_KEY, null)); assertNull(Assertions.assertNotNull(clientSettings.getCredential()).getUserName()); try (MongoClient mongoClient = createMongoClient(clientSettings)) { - // Perform a `find` operation that succeeds.. + // Perform a `find` operation that succeeds. performFind(mongoClient); } } @@ -384,12 +387,21 @@ public void test5p1AzureSucceedsWithNoUsername() { public void test5p2AzureFailsWithBadUsername() { assumeTrue(getOidcEnv().equals("azure")); String oidcUri = getOidcUri(); - oidcUri = oidcUri.replace("://", "://bad@"); - MongoClientSettings clientSettings = createSettings(oidcUri, createCallback(), null); - try (MongoClient mongoClient = createMongoClient(clientSettings)) { - // #. Perform a find operation that succeeds. - performFind(mongoClient); - } + ConnectionString cs = new ConnectionString(oidcUri); + MongoCredential oldCredential = Assertions.assertNotNull(cs.getCredential()); + String tokenResource = oldCredential.getMechanismProperty(TOKEN_RESOURCE_KEY, null); + assertNotNull(tokenResource); + MongoCredential cred = MongoCredential.createOidcCredential("bad") + .withMechanismProperty(ENVIRONMENT_KEY, "azure") + .withMechanismProperty(TOKEN_RESOURCE_KEY, tokenResource); + MongoClientSettings.Builder builder = MongoClientSettings.builder() + .applicationName(appName) + .retryReads(false) + .applyConnectionString(cs) + .credential(cred); + MongoClientSettings clientSettings = builder.build(); + // the failure is external to the driver + assertFindFails(clientSettings, MongoServerException.class, ""); } // Tests for human authentication ("testh", to preserve ordering) @@ -475,7 +487,8 @@ public void testAllowedHostsDisallowedInConnectionString() { @Test public void testh1p7AllowedHostsInConnectionStringIgnored() { - // example.com changed to localhost + // example.com changed to localhost, because resolveAdditionalQueryParametersFromTxtRecords + // fails with "Failed looking up TXT record for host example.com" String string = "mongodb+srv://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ALLOWED_HOSTS:%5B%22localhost%22%5D"; assertCause(IllegalArgumentException.class, "connection string contains disallowed mechanism properties", From f6cb3da140ae38963c0f4b5f122283ae4934ec33 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Fri, 26 Apr 2024 17:42:46 -0600 Subject: [PATCH 19/25] PR Fixes --- .../connection/OidcAuthenticationProseTests.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java index a3353a959f4..31ff5e3ef60 100644 --- a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java +++ b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java @@ -22,7 +22,6 @@ import com.mongodb.MongoConfigurationException; import com.mongodb.MongoCredential; import com.mongodb.MongoSecurityException; -import com.mongodb.MongoServerException; import com.mongodb.MongoSocketException; import com.mongodb.assertions.Assertions; import com.mongodb.client.Fixture; @@ -401,7 +400,7 @@ public void test5p2AzureFailsWithBadUsername() { .credential(cred); MongoClientSettings clientSettings = builder.build(); // the failure is external to the driver - assertFindFails(clientSettings, MongoServerException.class, ""); + assertFindFails(clientSettings, IOException.class, "400 Bad Request"); } // Tests for human authentication ("testh", to preserve ordering) @@ -902,11 +901,11 @@ private static void assertCause( while (cause.getCause() != null) { cause = cause.getCause(); } - if (!expectedCause.isInstance(cause)) { - throw new AssertionFailedError("Unexpected cause: " + assertThrows(Throwable.class, e).getClass(), assertThrows(Throwable.class, e)); - } if (!cause.getMessage().contains(expectedMessageFragment)) { - throw new AssertionFailedError("Unexpected message", assertThrows(Throwable.class, e)); + throw new AssertionFailedError("Unexpected message: " + cause.getMessage(), cause); + } + if (!expectedCause.isInstance(cause)) { + throw new AssertionFailedError("Unexpected cause: " + cause.getClass(), assertThrows(Throwable.class, e)); } } From be636435ca3c2a84d91b840f2b90701ff8f095e0 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Fri, 26 Apr 2024 18:17:38 -0600 Subject: [PATCH 20/25] PR fixes --- .../main/com/mongodb/ConnectionString.java | 31 ++++++++++++++----- .../com/mongodb/ConnectionStringUnitTest.java | 16 +++++++--- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/driver-core/src/main/com/mongodb/ConnectionString.java b/driver-core/src/main/com/mongodb/ConnectionString.java index 1187744699a..38fe8ffc7a6 100644 --- a/driver-core/src/main/com/mongodb/ConnectionString.java +++ b/driver-core/src/main/com/mongodb/ConnectionString.java @@ -240,7 +240,8 @@ * mechanism (the default). *

  • *
  • {@code authMechanismProperties=PROPERTY_NAME:PROPERTY_VALUE,PROPERTY_NAME2:PROPERTY_VALUE2}: This option allows authentication - * mechanism properties to be set on the connection string. + * mechanism properties to be set on the connection string. Property values must be percent-encoded individually, as needed. The + * entire substring following the {@code =} should not itself be encoded. *
  • *
  • {@code gssapiServiceName=string}: This option only applies to the GSSAPI mechanism and is used to alter the service name. * Deprecated, please use {@code authMechanismProperties=SERVICE_NAME:string} instead. @@ -924,7 +925,7 @@ private MongoCredential createCredentials(final Map> option } String key = mechanismPropertyKeyValue[0].trim().toLowerCase(); String value = mechanismPropertyKeyValue[1].trim(); - if (!decodeWholeOptionValue(mechanism)) { + if (decodeValueOfKeyValuePair(credential.getMechanism())) { value = urldecode(value); } if (MECHANISM_KEYS_DISALLOWED_IN_CONNECTION_STRING.contains(key)) { @@ -942,11 +943,25 @@ private MongoCredential createCredentials(final Map> option return credential; } - private static boolean decodeWholeOptionValue(final AuthenticationMechanism mechanism) { - return !AuthenticationMechanism.MONGODB_OIDC.equals(mechanism); + private static boolean decodeWholeOptionValue(final boolean isOidc, final String key) { + // The "whole option value" is the entire string following = in an option, + // including separators when the value is a list or list of key-values. + // This is the original parsing behaviour, but implies that users can + // encode separators (much like they might with URL parameters). This + // behaviour implies that users cannot encode "key-value" values that + // contain a comma, because this will (after this "whole value decoding) + // be parsed as a key-value separator, rather than part of a value. + return !(isOidc && key.equals("authmechanismproperties")); } - private static boolean decodeWholeOptionValue(final List options) { - return !options.contains("authMechanism=" + AuthenticationMechanism.MONGODB_OIDC.getMechanismName()); + + private static boolean decodeValueOfKeyValuePair(@Nullable final String mechanismName) { + // Only authMechanismProperties should be individually decoded, and only + // when the mechanism is OIDC. These will not have been decoded. + return AuthenticationMechanism.MONGODB_OIDC.getMechanismName().equals(mechanismName); + } + + private static boolean isOidc(final List options) { + return options.contains("authMechanism=" + AuthenticationMechanism.MONGODB_OIDC.getMechanismName()); } private MongoCredential createMongoCredentialWithMechanism(final AuthenticationMechanism mechanism, final String userName, @@ -1034,7 +1049,7 @@ private Map> parseOptions(final String optionsPart) { } List options = Arrays.asList(optionsPart.split("&|;")); - boolean decodeWholeOptionValue = decodeWholeOptionValue(options); + boolean isOidc = isOidc(options); for (final String part : options) { if (part.isEmpty()) { continue; @@ -1047,7 +1062,7 @@ private Map> parseOptions(final String optionsPart) { if (valueList == null) { valueList = new ArrayList<>(1); } - if (decodeWholeOptionValue) { + if (decodeWholeOptionValue(isOidc, key)) { value = urldecode(value); } valueList.add(value); diff --git a/driver-core/src/test/unit/com/mongodb/ConnectionStringUnitTest.java b/driver-core/src/test/unit/com/mongodb/ConnectionStringUnitTest.java index 6d52cd199a9..79aefe9ea45 100644 --- a/driver-core/src/test/unit/com/mongodb/ConnectionStringUnitTest.java +++ b/driver-core/src/test/unit/com/mongodb/ConnectionStringUnitTest.java @@ -15,6 +15,7 @@ */ package com.mongodb; +import com.mongodb.assertions.Assertions; import com.mongodb.connection.ServerMonitoringMode; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -41,18 +42,23 @@ void defaults() { @Test public void mustDecodeOidcIndividually() { String string = "abc,d!@#$%^&*;ef:ghi"; + // encoded tags will fail parsing with an "invalid read preference tag" + // error if decoding is skipped. + String encodedTags = encode("dc:ny,rack:1"); ConnectionString cs = new ConnectionString( - "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=" - + "ENVIRONMENT:azure,TOKEN_RESOURCE:" + encode(string)); - assertEquals(string, cs.getCredential().getMechanismProperty("TOKEN_RESOURCE", null)); + "mongodb://localhost/?readPreference=primaryPreferred&readPreferenceTags=" + encodedTags + + "&authMechanism=MONGODB-OIDC&authMechanismProperties=" + + "ENVIRONMENT:azure,TOKEN_RESOURCE:" + encode(string)); + MongoCredential credential = Assertions.assertNotNull(cs.getCredential()); + assertEquals(string, credential.getMechanismProperty("TOKEN_RESOURCE", null)); } @Test public void mustDecodeNonOidcAsWhole() { ConnectionString cs2 = new ConnectionString( "mongodb://foo:bar@example.com/?authMechanism=GSSAPI&authMechanismProperties=" - + encode("SERVICE_NAME:other,CANONICALIZE_HOST_NAME:true&authSource=$external")); - assertEquals("other", cs2.getCredential().getMechanismProperty("SERVICE_NAME", null)); + + encode("SERVICE_NAME:ot her,CANONICALIZE_HOST_NAME:true&authSource=$external")); + assertEquals("ot her", cs2.getCredential().getMechanismProperty("SERVICE_NAME", null)); } private static String encode(final String string) { From 0532a87e6c597875690583e1b1fa3463fa727274 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Mon, 29 Apr 2024 11:24:26 -0600 Subject: [PATCH 21/25] PR fixes --- .../com/mongodb/internal/connection/OidcAuthenticator.java | 7 ++++--- .../test/unit/com/mongodb/ConnectionStringUnitTest.java | 5 +++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java index d3db5ab9ab2..af26abbf87f 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java +++ b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java @@ -78,6 +78,8 @@ public final class OidcAuthenticator extends SaslAuthenticator { private static final String GCP_ENVIRONMENT = "gcp"; private static final List IMPLEMENTED_ENVIRONMENTS = Arrays.asList( AZURE_ENVIRONMENT, GCP_ENVIRONMENT, TEST_ENVIRONMENT); + private static final List USER_SUPPORTED_ENVIRONMENTS = Arrays.asList( + AZURE_ENVIRONMENT, GCP_ENVIRONMENT); private static final List REQUIRES_TOKEN_RESOURCE = Arrays.asList( AZURE_ENVIRONMENT, GCP_ENVIRONMENT); private static final List ALLOWS_USERNAME = Arrays.asList( @@ -192,8 +194,7 @@ private OidcCallback getRequestCallback() { return machine != null ? machine : assertNotNull(human); } - @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) - static OidcCallback getTestCallback() { + private static OidcCallback getTestCallback() { return (context) -> { String accessToken = readTokenFromFile(); return new OidcCallbackResult(accessToken); @@ -587,7 +588,7 @@ public static void validateOidcCredentialConstruction( Object environmentName = mechanismProperties.get(ENVIRONMENT_KEY.toLowerCase()); if (environmentName != null) { if (!(environmentName instanceof String) || !IMPLEMENTED_ENVIRONMENTS.contains(environmentName)) { - throw new IllegalArgumentException(ENVIRONMENT_KEY + " must be one of: " + IMPLEMENTED_ENVIRONMENTS); + throw new IllegalArgumentException(ENVIRONMENT_KEY + " must be one of: " + USER_SUPPORTED_ENVIRONMENTS); } } } diff --git a/driver-core/src/test/unit/com/mongodb/ConnectionStringUnitTest.java b/driver-core/src/test/unit/com/mongodb/ConnectionStringUnitTest.java index 79aefe9ea45..237bbc9befe 100644 --- a/driver-core/src/test/unit/com/mongodb/ConnectionStringUnitTest.java +++ b/driver-core/src/test/unit/com/mongodb/ConnectionStringUnitTest.java @@ -55,6 +55,11 @@ public void mustDecodeOidcIndividually() { @Test public void mustDecodeNonOidcAsWhole() { + ConnectionString cs1 = new ConnectionString( + "mongodb://foo:bar@example.com/?authMechanism=GSSAPI&authMechanismProperties=" + + "SERVICE_NAME:" + encode("ot her") + ",CANONICALIZE_HOST_NAME:true&authSource=$external"); + assertEquals("ot her", cs1.getCredential().getMechanismProperty("SERVICE_NAME", null)); + ConnectionString cs2 = new ConnectionString( "mongodb://foo:bar@example.com/?authMechanism=GSSAPI&authMechanismProperties=" + encode("SERVICE_NAME:ot her,CANONICALIZE_HOST_NAME:true&authSource=$external")); From 761918eefe0d63a8b7921cf7f727f658af4a8709 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Mon, 29 Apr 2024 12:07:57 -0600 Subject: [PATCH 22/25] PR fixes: mustDecodeNonOidcAsWhole --- .../com/mongodb/ConnectionStringUnitTest.java | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/driver-core/src/test/unit/com/mongodb/ConnectionStringUnitTest.java b/driver-core/src/test/unit/com/mongodb/ConnectionStringUnitTest.java index 237bbc9befe..6a8d9ff4fc3 100644 --- a/driver-core/src/test/unit/com/mongodb/ConnectionStringUnitTest.java +++ b/driver-core/src/test/unit/com/mongodb/ConnectionStringUnitTest.java @@ -55,15 +55,22 @@ public void mustDecodeOidcIndividually() { @Test public void mustDecodeNonOidcAsWhole() { - ConnectionString cs1 = new ConnectionString( - "mongodb://foo:bar@example.com/?authMechanism=GSSAPI&authMechanismProperties=" - + "SERVICE_NAME:" + encode("ot her") + ",CANONICALIZE_HOST_NAME:true&authSource=$external"); - assertEquals("ot her", cs1.getCredential().getMechanismProperty("SERVICE_NAME", null)); - - ConnectionString cs2 = new ConnectionString( - "mongodb://foo:bar@example.com/?authMechanism=GSSAPI&authMechanismProperties=" - + encode("SERVICE_NAME:ot her,CANONICALIZE_HOST_NAME:true&authSource=$external")); - assertEquals("ot her", cs2.getCredential().getMechanismProperty("SERVICE_NAME", null)); + // this string allows us to check if there is no double decoding + String rawValue = encode("ot her"); + assertAll(() -> { + // even though only one part has been encoded by the user, the whole option value (pre-split) must be decoded + ConnectionString cs = new ConnectionString( + "mongodb://foo:bar@example.com/?authMechanism=GSSAPI&authMechanismProperties=" + + "SERVICE_NAME:" + encode(rawValue) + ",CANONICALIZE_HOST_NAME:true&authSource=$external"); + MongoCredential credential = Assertions.assertNotNull(cs.getCredential()); + assertEquals(rawValue, credential.getMechanismProperty("SERVICE_NAME", null)); + }, () -> { + ConnectionString cs = new ConnectionString( + "mongodb://foo:bar@example.com/?authMechanism=GSSAPI&authMechanismProperties=" + + encode("SERVICE_NAME:" + rawValue + ",CANONICALIZE_HOST_NAME:true&authSource=$external")); + MongoCredential credential = Assertions.assertNotNull(cs.getCredential()); + assertEquals(rawValue, credential.getMechanismProperty("SERVICE_NAME", null)); + }); } private static String encode(final String string) { From 7428fd129c6fefbec49f8fdc0b3a2106f3df7257 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Mon, 29 Apr 2024 12:16:05 -0600 Subject: [PATCH 23/25] PR fixes --- .../connection/OidcAuthenticationProseTests.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java index 31ff5e3ef60..9813874418d 100644 --- a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java +++ b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java @@ -107,6 +107,10 @@ private static String getOidcEnv() { return getenv("OIDC_ENV"); } + private static void assumeAzure() { + assumeTrue(getOidcEnv().equals("azure")); + } + @Nullable private static String getUserWithDomain(@Nullable final String user) { return user == null ? null : user + "@" + getenv("OIDC_DOMAIN"); @@ -368,14 +372,14 @@ private static void performInsert(final MongoClient mongoClient) { @Test public void test5p1AzureSucceedsWithNoUsername() { - assumeTrue(getOidcEnv().equals("azure")); + assumeAzure(); String oidcUri = getOidcUri(); MongoClientSettings clientSettings = createSettings(oidcUri, createCallback(), null); // Create an OIDC configured client with `ENVIRONMENT:azure` and a valid // `TOKEN_RESOURCE` and no username. MongoCredential credential = Assertions.assertNotNull(new ConnectionString(oidcUri).getCredential()); assertNotNull(credential.getMechanismProperty(TOKEN_RESOURCE_KEY, null)); - assertNull(Assertions.assertNotNull(clientSettings.getCredential()).getUserName()); + assertNull(credential.getUserName()); try (MongoClient mongoClient = createMongoClient(clientSettings)) { // Perform a `find` operation that succeeds. performFind(mongoClient); @@ -384,7 +388,7 @@ public void test5p1AzureSucceedsWithNoUsername() { @Test public void test5p2AzureFailsWithBadUsername() { - assumeTrue(getOidcEnv().equals("azure")); + assumeAzure(); String oidcUri = getOidcUri(); ConnectionString cs = new ConnectionString(oidcUri); MongoCredential oldCredential = Assertions.assertNotNull(cs.getCredential()); From fcdab2919c9b32d259fae665c323142f09a8d6d1 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Mon, 29 Apr 2024 14:15:38 -0600 Subject: [PATCH 24/25] Update driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java Co-authored-by: Valentin Kovalenko --- .../internal/connection/OidcAuthenticationProseTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java index 9813874418d..9915f6a6a34 100644 --- a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java +++ b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java @@ -377,7 +377,7 @@ public void test5p1AzureSucceedsWithNoUsername() { MongoClientSettings clientSettings = createSettings(oidcUri, createCallback(), null); // Create an OIDC configured client with `ENVIRONMENT:azure` and a valid // `TOKEN_RESOURCE` and no username. - MongoCredential credential = Assertions.assertNotNull(new ConnectionString(oidcUri).getCredential()); + MongoCredential credential = Assertions.assertNotNull(clientSettings.getCredential()); assertNotNull(credential.getMechanismProperty(TOKEN_RESOURCE_KEY, null)); assertNull(credential.getUserName()); try (MongoClient mongoClient = createMongoClient(clientSettings)) { From 8971a79eca986f3cd3c4626a837190717721eb9d Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Mon, 29 Apr 2024 14:30:58 -0600 Subject: [PATCH 25/25] Doc fix --- driver-core/src/main/com/mongodb/ConnectionString.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/driver-core/src/main/com/mongodb/ConnectionString.java b/driver-core/src/main/com/mongodb/ConnectionString.java index 38fe8ffc7a6..ae795a65bba 100644 --- a/driver-core/src/main/com/mongodb/ConnectionString.java +++ b/driver-core/src/main/com/mongodb/ConnectionString.java @@ -240,7 +240,8 @@ * mechanism (the default). *
  • *
  • {@code authMechanismProperties=PROPERTY_NAME:PROPERTY_VALUE,PROPERTY_NAME2:PROPERTY_VALUE2}: This option allows authentication - * mechanism properties to be set on the connection string. Property values must be percent-encoded individually, as needed. The + * mechanism properties to be set on the connection string. Property values must be percent-encoded individually, when + * separator or escape characters are used (including {@code ,} (comma), {@code =}, {@code +}, {@code &}, and {@code %}). The * entire substring following the {@code =} should not itself be encoded. *
  • *
  • {@code gssapiServiceName=string}: This option only applies to the GSSAPI mechanism and is used to alter the service name.