diff --git a/driver-core/src/main/com/mongodb/MongoCredential.java b/driver-core/src/main/com/mongodb/MongoCredential.java
index 418863dc21c..4c10e1f640c 100644
--- a/driver-core/src/main/com/mongodb/MongoCredential.java
+++ b/driver-core/src/main/com/mongodb/MongoCredential.java
@@ -25,7 +25,6 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -188,8 +187,7 @@ public final class MongoCredential {
* The provider name. The value must be a string.
*
* If this is provided,
- * {@link MongoCredential#REQUEST_TOKEN_CALLBACK_KEY} and
- * {@link MongoCredential#REFRESH_TOKEN_CALLBACK_KEY}
+ * {@link MongoCredential#OIDC_CALLBACK_KEY}
* must not be provided.
*
* @see #createOidcCredential(String)
@@ -208,45 +206,7 @@ public final class MongoCredential {
* @see #createOidcCredential(String)
* @since 4.10
*/
- public static final String REQUEST_TOKEN_CALLBACK_KEY = "REQUEST_TOKEN_CALLBACK";
-
- /**
- * Mechanism key for invoked when the OIDC-based authenticator refreshes
- * tokens from the identity provider. If this callback is not provided,
- * then refresh operations will not be attempted.The type of the value
- * must be {@link OidcRefreshCallback}.
- *
- * If this is provided, {@link MongoCredential#PROVIDER_NAME_KEY}
- * must not be provided.
- *
- * @see #createOidcCredential(String)
- * @since 4.10
- */
- public static final String REFRESH_TOKEN_CALLBACK_KEY = "REFRESH_TOKEN_CALLBACK";
-
- /**
- * Mechanism 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}.
- *
- * @see MongoCredential#DEFAULT_ALLOWED_HOSTS
- * @see #createOidcCredential(String)
- * @since 4.10
- */
- public static final String ALLOWED_HOSTS_KEY = "ALLOWED_HOSTS";
-
- /**
- * The list of allowed hosts that will be used if no
- * {@link MongoCredential#ALLOWED_HOSTS_KEY} value is supplied.
- * The default allowed hosts are:
- * {@code "*.mongodb.net", "*.mongodb-dev.net", "*.mongodbgov.net", "localhost", "127.0.0.1", "::1"}
- *
- * @see #createOidcCredential(String)
- * @since 4.10
- */
- public static final List DEFAULT_ALLOWED_HOSTS = Collections.unmodifiableList(Arrays.asList(
- "*.mongodb.net", "*.mongodb-dev.net", "*.mongodbgov.net", "localhost", "127.0.0.1", "::1"));
+ public static final String OIDC_CALLBACK_KEY = "OIDC_CALLBACK";
/**
* Creates a MongoCredential instance with an unspecified mechanism. The client will negotiate the best mechanism based on the
@@ -404,9 +364,7 @@ public static MongoCredential createAwsCredential(@Nullable final String userNam
* @since 4.10
* @see #withMechanismProperty(String, Object)
* @see #PROVIDER_NAME_KEY
- * @see #REQUEST_TOKEN_CALLBACK_KEY
- * @see #REFRESH_TOKEN_CALLBACK_KEY
- * @see #ALLOWED_HOSTS_KEY
+ * @see #OIDC_CALLBACK_KEY
* @mongodb.server.release 7.0
*/
public static MongoCredential createOidcCredential(@Nullable final String userName) {
@@ -639,26 +597,16 @@ public String toString() {
*/
@Evolving
public interface OidcRequestContext {
- /**
- * @return The OIDC Identity Provider's configuration that can be used to acquire an Access Token.
- */
- IdpInfo getIdpInfo();
/**
* @return The timeout that this callback must complete within.
*/
Duration getTimeout();
- }
- /**
- * The context for the {@link OidcRefreshCallback#onRefresh(OidcRefreshContext) OIDC refresh callback}.
- */
- @Evolving
- public interface OidcRefreshContext extends OidcRequestContext {
/**
- * @return The OIDC Refresh token supplied by a prior callback invocation.
+ * @return The OIDC callback API version. Currently, version 1.
*/
- String getRefreshToken();
+ int getVersion();
}
/**
@@ -673,72 +621,22 @@ public interface OidcRequestCallback {
* @param context The context.
* @return The response produced by an OIDC Identity Provider
*/
- IdpResponse onRequest(OidcRequestContext context);
- }
-
- /**
- * This callback is invoked when the OIDC-based authenticator refreshes
- * tokens from the identity provider. If this callback is not provided,
- * then refresh operations will not be attempted.
- *
- * It does not have to be thread-safe, unless it is provided to multiple
- * MongoClients.
- */
- public interface OidcRefreshCallback {
- /**
- * @param context The context.
- * @return The response produced by an OIDC Identity Provider
- */
- IdpResponse onRefresh(OidcRefreshContext context);
- }
-
- /**
- * The OIDC Identity Provider's configuration that can be used to acquire an Access Token.
- */
- @Evolving
- public interface IdpInfo {
- /**
- * @return URL which describes the Authorization Server. This identifier is the
- * iss of provided access tokens, and is viable for RFC8414 metadata
- * discovery and RFC9207 identification.
- */
- String getIssuer();
-
- /**
- * @return Unique client ID for this OIDC client.
- */
- String getClientId();
-
- /**
- * @return Additional scopes to request from Identity Provider. Immutable.
- */
- List getRequestScopes();
+ RequestCallbackResult onRequest(OidcRequestContext context);
}
/**
* The response produced by an OIDC Identity Provider.
*/
- public static final class IdpResponse {
+ public static final class RequestCallbackResult {
private final String accessToken;
- @Nullable
- private final Integer accessTokenExpiresInSeconds;
-
- @Nullable
- private final String refreshToken;
-
/**
* @param accessToken The OIDC access token
- * @param accessTokenExpiresInSeconds The expiration in seconds. If null, the access token is single-use.
- * @param refreshToken The refresh token. If null, refresh will not be attempted.
*/
- public IdpResponse(final String accessToken, @Nullable final Integer accessTokenExpiresInSeconds,
- @Nullable final String refreshToken) {
+ public RequestCallbackResult(final String accessToken) {
notNull("accessToken", accessToken);
this.accessToken = accessToken;
- this.accessTokenExpiresInSeconds = accessTokenExpiresInSeconds;
- this.refreshToken = refreshToken;
}
/**
@@ -747,22 +645,5 @@ public IdpResponse(final String accessToken, @Nullable final Integer accessToken
public String getAccessToken() {
return accessToken;
}
-
- /**
- * @return The expiration time for the access token in seconds.
- * If null, the access token is single-use.
- */
- @Nullable
- public Integer getAccessTokenExpiresInSeconds() {
- return accessTokenExpiresInSeconds;
- }
-
- /**
- * @return The OIDC refresh token. If null, refresh will not be attempted.
- */
- @Nullable
- public String getRefreshToken() {
- return refreshToken;
- }
}
}
diff --git a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java
index 09f2ec6a845..218835f083e 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java
@@ -383,7 +383,7 @@ public void sendAndReceiveAsync(final CommandMessage message, final Decoder<
message, decoder, sessionContext, requestContext, operationContext, c);
beginAsync().thenSupply(c -> {
sendAndReceiveAsyncInternal.getAsync(c);
- }).onErrorIf(e -> reauthenticationIsTriggered(e), c -> {
+ }).onErrorIf(e -> reauthenticationIsTriggered(e), (t, c) -> {
reauthenticateAndRetryAsync(sendAndReceiveAsyncInternal, c);
}).finish(callback);
}
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 2d3387e9216..70f9682476c 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java
@@ -21,8 +21,7 @@
import com.mongodb.MongoCommandException;
import com.mongodb.MongoConfigurationException;
import com.mongodb.MongoCredential;
-import com.mongodb.MongoCredential.IdpInfo;
-import com.mongodb.MongoCredential.IdpResponse;
+import com.mongodb.MongoCredential.RequestCallbackResult;
import com.mongodb.MongoException;
import com.mongodb.MongoSecurityException;
import com.mongodb.ServerAddress;
@@ -30,14 +29,11 @@
import com.mongodb.connection.ClusterConnectionMode;
import com.mongodb.connection.ConnectionDescription;
import com.mongodb.internal.Locks;
-import com.mongodb.internal.async.SingleResultCallback;
import com.mongodb.internal.VisibleForTesting;
-import com.mongodb.internal.time.Timeout;
+import com.mongodb.internal.async.SingleResultCallback;
import com.mongodb.lang.Nullable;
import org.bson.BsonDocument;
import org.bson.BsonString;
-import org.bson.RawBsonDocument;
-import org.jetbrains.annotations.NotNull;
import javax.security.sasl.SaslClient;
import java.io.IOException;
@@ -46,24 +42,14 @@
import java.nio.file.Paths;
import java.time.Duration;
import java.util.Arrays;
-import java.util.Collections;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Function;
-import java.util.stream.Collectors;
import static com.mongodb.AuthenticationMechanism.MONGODB_OIDC;
-import static com.mongodb.MongoCredential.ALLOWED_HOSTS_KEY;
-import static com.mongodb.MongoCredential.DEFAULT_ALLOWED_HOSTS;
-import static com.mongodb.MongoCredential.OidcRefreshCallback;
-import static com.mongodb.MongoCredential.OidcRefreshContext;
import static com.mongodb.MongoCredential.OidcRequestCallback;
import static com.mongodb.MongoCredential.OidcRequestContext;
import static com.mongodb.MongoCredential.PROVIDER_NAME_KEY;
-import static com.mongodb.MongoCredential.REFRESH_TOKEN_CALLBACK_KEY;
-import static com.mongodb.MongoCredential.REQUEST_TOKEN_CALLBACK_KEY;
+import static com.mongodb.MongoCredential.OIDC_CALLBACK_KEY;
import static com.mongodb.assertions.Assertions.assertFalse;
import static com.mongodb.assertions.Assertions.assertNotNull;
import static com.mongodb.assertions.Assertions.assertTrue;
@@ -80,10 +66,8 @@ public final class OidcAuthenticator extends SaslAuthenticator {
private static final Duration CALLBACK_TIMEOUT = Duration.ofMinutes(5);
- private static final String AWS_WEB_IDENTITY_TOKEN_FILE = "AWS_WEB_IDENTITY_TOKEN_FILE";
-
- @Nullable
- private ServerAddress serverAddress;
+ public static final String AWS_WEB_IDENTITY_TOKEN_FILE = "AWS_WEB_IDENTITY_TOKEN_FILE";
+ private static final int CALLBACK_API_VERSION_NUMBER = 1;
@Nullable
private String connectionLastAccessToken;
@@ -93,9 +77,6 @@ public final class OidcAuthenticator extends SaslAuthenticator {
@Nullable
private BsonDocument speculativeAuthenticateResponse;
- @Nullable
- private Function evaluateChallengeFunction;
-
public OidcAuthenticator(final MongoCredentialWithCache credential,
final ClusterConnectionMode clusterConnectionMode, @Nullable final ServerApi serverApi) {
super(credential, clusterConnectionMode, serverApi);
@@ -113,7 +94,6 @@ public String getMechanismName() {
@Override
protected SaslClient createSaslClient(final ServerAddress serverAddress) {
- this.serverAddress = serverAddress;
MongoCredentialWithCache mongoCredentialWithCache = getMongoCredentialWithCache();
return new OidcSaslClient(mongoCredentialWithCache);
}
@@ -125,13 +105,9 @@ public BsonDocument createSpeculativeAuthenticateCommand(final InternalConnectio
if (isAutomaticAuthentication()) {
return wrapInSpeculative(prepareAwsTokenFromFileAsJwt());
}
- String cachedAccessToken = getValidCachedAccessToken();
- MongoCredentialWithCache mongoCredentialWithCache = getMongoCredentialWithCache();
+ String cachedAccessToken = getCachedAccessToken();
if (cachedAccessToken != null) {
return wrapInSpeculative(prepareTokenAsJwt(cachedAccessToken));
- } else if (mongoCredentialWithCache.getOidcCacheEntry().getIdpInfo() == null) {
- String userName = mongoCredentialWithCache.getCredential().getUserName();
- return wrapInSpeculative(prepareUsername(userName));
} else {
// otherwise, skip speculative auth
return null;
@@ -141,7 +117,6 @@ public BsonDocument createSpeculativeAuthenticateCommand(final InternalConnectio
}
}
- @NotNull
private BsonDocument wrapInSpeculative(final byte[] outToken) {
BsonDocument startDocument = createSaslStartCommandDocument(outToken)
.append("db", new BsonString(getMongoCredential().getSource()));
@@ -166,43 +141,31 @@ public void setSpeculativeAuthenticateResponse(@Nullable final BsonDocument resp
speculativeAuthenticateResponse = response;
}
- @Nullable
- private OidcRefreshCallback getRefreshCallback() {
- return getMongoCredentialWithCache()
- .getCredential()
- .getMechanismProperty(REFRESH_TOKEN_CALLBACK_KEY, null);
- }
-
@Nullable
private OidcRequestCallback getRequestCallback() {
return getMongoCredentialWithCache()
.getCredential()
- .getMechanismProperty(REQUEST_TOKEN_CALLBACK_KEY, null);
+ .getMechanismProperty(OIDC_CALLBACK_KEY, null);
}
@Override
public void reauthenticate(final InternalConnection connection) {
assertTrue(connection.opened());
- authLock(connection, connection.getDescription());
+ authenticationLoop(connection, connection.getDescription());
}
@Override
public void reauthenticateAsync(final InternalConnection connection, final SingleResultCallback callback) {
beginAsync().thenRun(c -> {
assertTrue(connection.opened());
- authLockAsync(connection, connection.getDescription(), c);
+ authenticationLoopAsync(connection, connection.getDescription(), c);
}).finish(callback);
}
@Override
public void authenticate(final InternalConnection connection, final ConnectionDescription connectionDescription) {
assertFalse(connection.opened());
- String accessToken = getValidCachedAccessToken();
- if (accessToken != null) {
- authenticateOptimistically(connection, connectionDescription, accessToken);
- } else {
- authLock(connection, connectionDescription);
- }
+ authenticationLoop(connection, connectionDescription);
}
@Override
@@ -210,35 +173,7 @@ void authenticateAsync(final InternalConnection connection, final ConnectionDesc
final SingleResultCallback callback) {
beginAsync().thenRun(c -> {
assertFalse(connection.opened());
- String accessToken = getValidCachedAccessToken();
- if (accessToken != null) {
- authenticateOptimisticallyAsync(connection, connectionDescription, accessToken, c);
- } else {
- authLockAsync(connection, connectionDescription, c);
- }
- }).finish(callback);
- }
-
- private void authenticateOptimistically(final InternalConnection connection,
- final ConnectionDescription connectionDescription, final String accessToken) {
- try {
- authenticateUsingFunction(connection, connectionDescription, (challenge) -> prepareTokenAsJwt(accessToken));
- } catch (MongoSecurityException e) {
- if (triggersRetry(e)) {
- authLock(connection, connectionDescription);
- } else {
- throw e;
- }
- }
- }
-
- private void authenticateOptimisticallyAsync(final InternalConnection connection,
- final ConnectionDescription connectionDescription, final String accessToken,
- final SingleResultCallback callback) {
- beginAsync().thenRun(c -> {
- authenticateUsingFunctionAsync(connection, connectionDescription, (challenge) -> prepareTokenAsJwt(accessToken), c);
- }).onErrorIf(e -> triggersRetry(e), c -> {
- authLockAsync(connection, connectionDescription, c);
+ authenticationLoopAsync(connection, connectionDescription, c);
}).finish(callback);
}
@@ -254,60 +189,61 @@ private static boolean triggersRetry(@Nullable final Throwable t) {
return false;
}
- private void authenticateUsingFunctionAsync(final InternalConnection connection,
- final ConnectionDescription connectionDescription, final Function evaluateChallengeFunction,
- final SingleResultCallback callback) {
- this.evaluateChallengeFunction = evaluateChallengeFunction;
- super.authenticateAsync(connection, connectionDescription, callback);
- }
-
- private void authenticateUsingFunction(
- final InternalConnection connection,
- final ConnectionDescription connectionDescription,
- final Function evaluateChallengeFunction) {
- this.evaluateChallengeFunction = evaluateChallengeFunction;
- super.authenticate(connection, connectionDescription);
- }
-
- private void authLock(final InternalConnection connection, final ConnectionDescription description) {
+ private void authenticationLoop(final InternalConnection connection, final ConnectionDescription description) {
fallbackState = FallbackState.INITIAL;
- Locks.withLock(getMongoCredentialWithCache().getOidcLock(), () -> {
- while (true) {
- try {
- authenticateUsingFunction(connection, description, (challenge) -> evaluate(challenge));
- break;
- } catch (MongoSecurityException e) {
- if (triggersRetry(e) && shouldRetryHandler()) {
- continue;
- }
- throw e;
+ while (true) {
+ try {
+ super.authenticate(connection, description);
+ break;
+ } catch (MongoSecurityException e) {
+ if (triggersRetry(e) && shouldRetryHandler()) {
+ continue;
}
+ throw e;
}
- });
+ }
}
- private void authLockAsync(final InternalConnection connection, final ConnectionDescription description,
+ private void authenticationLoopAsync(final InternalConnection connection, final ConnectionDescription description,
final SingleResultCallback callback) {
fallbackState = FallbackState.INITIAL;
- Locks.withLockAsync(getMongoCredentialWithCache().getOidcLock(),
- beginAsync().thenRunRetryingWhile(
- c -> authenticateUsingFunctionAsync(connection, description, (challenge) -> evaluate(challenge), c),
- e -> triggersRetry(e) && shouldRetryHandler()
- ), callback);
+ beginAsync().thenRunRetryingWhile(
+ c -> super.authenticateAsync(connection, description, c),
+ e -> triggersRetry(e) && shouldRetryHandler()
+ ).finish(callback);
}
private byte[] evaluate(final byte[] challenge) {
if (isAutomaticAuthentication()) {
return prepareAwsTokenFromFileAsJwt();
}
+ byte[][] jwt = new byte[1][];
+ Locks.withLock(getMongoCredentialWithCache().getOidcLock(), () -> {
+ String cachedAccessToken = validatedCachedAccessToken();
- OidcRequestCallback requestCallback = assertNotNull(getRequestCallback());
+ if (cachedAccessToken != null) {
+ jwt[0] = prepareTokenAsJwt(cachedAccessToken);
+ fallbackState = FallbackState.PHASE_1_CACHED_TOKEN;
+ } else {
+ // cache is empty
+ OidcRequestCallback requestCallback = assertNotNull(getRequestCallback());
+ RequestCallbackResult result = requestCallback.onRequest(new OidcRequestContextImpl(CALLBACK_TIMEOUT));
+ jwt[0] = populateCacheWithCallbackResultAndPrepareJwt(result);
+ fallbackState = FallbackState.PHASE_2_CALLBACK_TOKEN;
+ }
+ });
+ return jwt[0];
+ }
+
+ /**
+ * Must be guarded by {@link MongoCredentialWithCache#getOidcLock()}.
+ */
+ @Nullable
+ private String validatedCachedAccessToken() {
MongoCredentialWithCache mongoCredentialWithCache = getMongoCredentialWithCache();
OidcCacheEntry cacheEntry = mongoCredentialWithCache.getOidcCacheEntry();
- String cachedAccessToken = getValidCachedAccessToken();
+ String cachedAccessToken = getCachedAccessToken();
String invalidConnectionAccessToken = connectionLastAccessToken;
- String cachedRefreshToken = cacheEntry.getRefreshToken();
- IdpInfo cachedIdpInfo = cacheEntry.getIdpInfo();
if (cachedAccessToken != null) {
boolean cachedTokenIsInvalid = cachedAccessToken.equals(invalidConnectionAccessToken);
@@ -316,45 +252,7 @@ private byte[] evaluate(final byte[] challenge) {
cachedAccessToken = null;
}
}
- OidcRefreshCallback refreshCallback = getRefreshCallback();
- if (cachedAccessToken != null) {
- fallbackState = FallbackState.PHASE_1_CACHED_TOKEN;
- return prepareTokenAsJwt(cachedAccessToken);
- } else if (refreshCallback != null && cachedRefreshToken != null) {
- assertNotNull(cachedIdpInfo);
- // Invoke Refresh Callback using cached Refresh Token
- validateAllowedHosts(getMongoCredential());
- fallbackState = FallbackState.PHASE_2_REFRESH_CALLBACK_TOKEN;
- IdpResponse result = refreshCallback.onRefresh(new OidcRefreshContextImpl(
- cachedIdpInfo, cachedRefreshToken, CALLBACK_TIMEOUT));
- return populateCacheWithCallbackResultAndPrepareJwt(cachedIdpInfo, result);
- } else {
- // cache is empty
-
- /*
- A check for present idp info short-circuits phase-3a.
-
- If a challenge is present, it can only be a response to a
- "principal-request", so the challenge must be the resulting
- idp info. Such a request is made during speculative auth,
- though the source is unimportant, as long as we detect and
- use it here.
-
- Checking that the fallback state is not phase-3a ensures that
- this does not loop infinitely in the case of a bug.
- */
- boolean idpInfoNotPresent = challenge.length == 0;
- if (fallbackState != FallbackState.PHASE_3A_PRINCIPAL && idpInfoNotPresent) {
- fallbackState = FallbackState.PHASE_3A_PRINCIPAL;
- return prepareUsername(mongoCredentialWithCache.getCredential().getUserName());
- } else {
- IdpInfo idpInfo = toIdpInfo(challenge);
- validateAllowedHosts(getMongoCredential());
- IdpResponse result = requestCallback.onRequest(new OidcRequestContextImpl(idpInfo, CALLBACK_TIMEOUT));
- fallbackState = FallbackState.PHASE_3B_REQUEST_CALLBACK_TOKEN;
- return populateCacheWithCallbackResultAndPrepareJwt(idpInfo, result);
- }
- }
+ return cachedAccessToken;
}
private boolean isAutomaticAuthentication() {
@@ -362,124 +260,53 @@ private boolean isAutomaticAuthentication() {
}
private boolean clientIsComplete() {
- return fallbackState != FallbackState.PHASE_3A_PRINCIPAL;
+ return true; // all possibilities are 1-step
}
private boolean shouldRetryHandler() {
- MongoCredentialWithCache mongoCredentialWithCache = getMongoCredentialWithCache();
- OidcCacheEntry cacheEntry = mongoCredentialWithCache.getOidcCacheEntry();
- if (fallbackState == FallbackState.PHASE_1_CACHED_TOKEN) {
- // a cached access token failed
- mongoCredentialWithCache.setOidcCacheEntry(cacheEntry
- .clearAccessToken());
- } else if (fallbackState == FallbackState.PHASE_2_REFRESH_CALLBACK_TOKEN) {
- // a refresh token failed
- mongoCredentialWithCache.setOidcCacheEntry(cacheEntry
- .clearAccessToken()
- .clearRefreshToken());
- } else {
- // a clean-restart failed
- mongoCredentialWithCache.setOidcCacheEntry(cacheEntry
- .clearAccessToken()
- .clearRefreshToken());
- return false;
- }
- return true;
+ Locks.withLock(getMongoCredentialWithCache().getOidcLock(), () -> {
+ validatedCachedAccessToken();
+ });
+ return fallbackState == FallbackState.PHASE_1_CACHED_TOKEN;
}
@Nullable
- private String getValidCachedAccessToken() {
+ private String getCachedAccessToken() {
return getMongoCredentialWithCache()
.getOidcCacheEntry()
- .getValidCachedAccessToken();
+ .getCachedAccessToken();
}
static final class OidcCacheEntry {
@Nullable
private final String accessToken;
- @Nullable
- private final Timeout accessTokenExpiry;
- @Nullable
- private final String refreshToken;
- @Nullable
- private final IdpInfo idpInfo;
@Override
public String toString() {
return "OidcCacheEntry{"
- + "\n accessToken#hashCode='" + Objects.hashCode(accessToken) + '\''
- + ",\n accessTokenExpiry=" + accessTokenExpiry
- + ",\n refreshToken='" + refreshToken + '\''
- + ",\n idpInfo=" + idpInfo
+ + "\n accessToken=[omitted]"
+ '}';
}
- OidcCacheEntry(final IdpInfo idpInfo, final IdpResponse idpResponse) {
- Integer accessTokenExpiresInSeconds = idpResponse.getAccessTokenExpiresInSeconds();
- if (accessTokenExpiresInSeconds != null) {
- this.accessToken = idpResponse.getAccessToken();
- long accessTokenExpiryReservedSeconds = TimeUnit.MINUTES.toSeconds(5);
- this.accessTokenExpiry = Timeout.startNow(
- Math.max(0, accessTokenExpiresInSeconds - accessTokenExpiryReservedSeconds),
- TimeUnit.SECONDS);
- } else {
- this.accessToken = null;
- this.accessTokenExpiry = null;
- }
- String refreshToken = idpResponse.getRefreshToken();
- if (refreshToken != null) {
- this.refreshToken = refreshToken;
- this.idpInfo = idpInfo;
- } else {
- this.refreshToken = null;
- this.idpInfo = null;
- }
+ OidcCacheEntry(final RequestCallbackResult requestCallbackResult) {
+ this.accessToken = requestCallbackResult.getAccessToken();
}
OidcCacheEntry() {
- this(null, null, null, null);
+ this((String) null);
}
- private OidcCacheEntry(@Nullable final String accessToken, @Nullable final Timeout accessTokenExpiry,
- @Nullable final String refreshToken, @Nullable final IdpInfo idpInfo) {
+ private OidcCacheEntry(@Nullable final String accessToken) {
this.accessToken = accessToken;
- this.accessTokenExpiry = accessTokenExpiry;
- this.refreshToken = refreshToken;
- this.idpInfo = idpInfo;
}
@Nullable
- String getValidCachedAccessToken() {
- if (accessToken == null || accessTokenExpiry == null || accessTokenExpiry.expired()) {
- return null;
- }
+ String getCachedAccessToken() {
return accessToken;
}
- @Nullable
- String getRefreshToken() {
- return refreshToken;
- }
-
- @Nullable
- IdpInfo getIdpInfo() {
- return idpInfo;
- }
-
OidcCacheEntry clearAccessToken() {
- return new OidcCacheEntry(
- null,
- null,
- this.refreshToken,
- this.idpInfo);
- }
-
- OidcCacheEntry clearRefreshToken() {
- return new OidcCacheEntry(
- this.accessToken,
- this.accessTokenExpiry,
- null,
- null);
+ return new OidcCacheEntry((String) null);
}
}
@@ -491,7 +318,7 @@ private OidcSaslClient(final MongoCredentialWithCache mongoCredentialWithCache)
@Override
public byte[] evaluateChallenge(final byte[] challenge) {
- return assertNotNull(evaluateChallengeFunction).apply(challenge);
+ return evaluate(challenge);
}
@Override
@@ -516,65 +343,13 @@ private static String readAwsTokenFromFile() {
}
}
- private static byte[] prepareUsername(@Nullable final String username) {
- BsonDocument document = new BsonDocument();
- if (username != null) {
- document = document.append("n", new BsonString(username));
- }
- return toBson(document);
- }
-
- private byte[] populateCacheWithCallbackResultAndPrepareJwt(
- final IdpInfo serverInfo,
- @Nullable final IdpResponse idpResponse) {
- if (idpResponse == null) {
+ private byte[] populateCacheWithCallbackResultAndPrepareJwt(@Nullable final RequestCallbackResult requestCallbackResult) {
+ if (requestCallbackResult == null) {
throw new MongoConfigurationException("Result of callback must not be null");
}
- OidcCacheEntry newEntry = new OidcCacheEntry(serverInfo, idpResponse);
+ OidcCacheEntry newEntry = new OidcCacheEntry(requestCallbackResult);
getMongoCredentialWithCache().setOidcCacheEntry(newEntry);
- return prepareTokenAsJwt(idpResponse.getAccessToken());
- }
-
- private static IdpInfo toIdpInfo(final byte[] challenge) {
- BsonDocument c = new RawBsonDocument(challenge);
- String issuer = c.getString("issuer").getValue();
- String clientId = c.getString("clientId").getValue();
- return new IdpInfoImpl(
- issuer,
- clientId,
- getStringArray(c, "requestScopes"));
- }
-
- private void validateAllowedHosts(final MongoCredential credential) {
- List allowedHosts = assertNotNull(credential.getMechanismProperty(ALLOWED_HOSTS_KEY, DEFAULT_ALLOWED_HOSTS));
- String host = assertNotNull(serverAddress).getHost();
- boolean permitted = allowedHosts.stream().anyMatch(allowedHost -> {
- if (allowedHost.startsWith("*.")) {
- String ending = allowedHost.substring(1);
- return host.endsWith(ending);
- } else if (allowedHost.contains("*")) {
- throw new IllegalArgumentException(
- "Allowed host " + allowedHost + " contains invalid wildcard");
- } else {
- return host.equals(allowedHost);
- }
- });
- if (!permitted) {
- throw new MongoSecurityException(
- credential, "Host not permitted by " + ALLOWED_HOSTS_KEY + ": " + host);
- }
- }
-
- @Nullable
- private static List getStringArray(final BsonDocument document, final String key) {
- if (!document.isArray(key)) {
- return null;
- }
- return document.getArray(key).stream()
- // ignore non-string values from server, rather than error
- .filter(v -> v.isString())
- .map(v -> v.asString().getValue())
- .collect(Collectors.toList());
+ return prepareTokenAsJwt(requestCallbackResult.getAccessToken());
}
private byte[] prepareTokenAsJwt(final String accessToken) {
@@ -625,13 +400,12 @@ public static void validateCreateOidcCredential(@Nullable final char[] password)
public static void validateBeforeUse(final MongoCredential credential) {
String userName = credential.getUserName();
Object providerName = credential.getMechanismProperty(PROVIDER_NAME_KEY, null);
- Object requestCallback = credential.getMechanismProperty(REQUEST_TOKEN_CALLBACK_KEY, null);
- Object refreshCallback = credential.getMechanismProperty(REFRESH_TOKEN_CALLBACK_KEY, null);
+ Object requestCallback = credential.getMechanismProperty(OIDC_CALLBACK_KEY, null);
if (providerName == null) {
// callback
if (requestCallback == null) {
throw new IllegalArgumentException("Either " + PROVIDER_NAME_KEY + " or "
- + REQUEST_TOKEN_CALLBACK_KEY + " must be specified");
+ + OIDC_CALLBACK_KEY + " must be specified");
}
} else {
// automatic
@@ -639,10 +413,7 @@ public static void validateBeforeUse(final MongoCredential credential) {
throw new IllegalArgumentException("user name must not be specified when " + PROVIDER_NAME_KEY + " is specified");
}
if (requestCallback != null) {
- throw new IllegalArgumentException(REQUEST_TOKEN_CALLBACK_KEY + " must not be specified when " + PROVIDER_NAME_KEY + " is specified");
- }
- if (refreshCallback != null) {
- throw new IllegalArgumentException(REFRESH_TOKEN_CALLBACK_KEY + " must not be specified when " + PROVIDER_NAME_KEY + " is specified");
+ throw new IllegalArgumentException(OIDC_CALLBACK_KEY + " must not be specified when " + PROVIDER_NAME_KEY + " is specified");
}
}
}
@@ -651,81 +422,29 @@ public static void validateBeforeUse(final MongoCredential credential) {
@VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE)
static class OidcRequestContextImpl implements OidcRequestContext {
- private final IdpInfo idpInfo;
private final Duration timeout;
- OidcRequestContextImpl(final IdpInfo idpInfo, final Duration timeout) {
- this.idpInfo = assertNotNull(idpInfo);
+ OidcRequestContextImpl(final Duration timeout) {
this.timeout = assertNotNull(timeout);
}
- @Override
- public IdpInfo getIdpInfo() {
- return idpInfo;
- }
-
@Override
public Duration getTimeout() {
return timeout;
}
- }
-
- @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE)
- static final class OidcRefreshContextImpl extends OidcRequestContextImpl
- implements OidcRefreshContext {
- private final String refreshToken;
-
- OidcRefreshContextImpl(final IdpInfo idpInfo, final String refreshToken,
- final Duration timeout) {
- super(idpInfo, timeout);
- this.refreshToken = assertNotNull(refreshToken);
- }
-
- @Override
- public String getRefreshToken() {
- return refreshToken;
- }
- }
-
- @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE)
- static final class IdpInfoImpl implements IdpInfo {
- private final String issuer;
- private final String clientId;
-
- private final List requestScopes;
-
- IdpInfoImpl(final String issuer, final String clientId, @Nullable final List requestScopes) {
- this.issuer = assertNotNull(issuer);
- this.clientId = assertNotNull(clientId);
- this.requestScopes = requestScopes == null
- ? Collections.emptyList()
- : Collections.unmodifiableList(requestScopes);
- }
-
- @Override
- public String getIssuer() {
- return issuer;
- }
-
- @Override
- public String getClientId() {
- return clientId;
- }
@Override
- public List getRequestScopes() {
- return requestScopes;
+ public int getVersion() {
+ return CALLBACK_API_VERSION_NUMBER;
}
}
/**
- * Represents what was sent in the last request to the MongoDB server.
+ * What was sent in the last request by this connection to the server.
*/
private enum FallbackState {
INITIAL,
PHASE_1_CACHED_TOKEN,
- PHASE_2_REFRESH_CALLBACK_TOKEN,
- PHASE_3A_PRINCIPAL,
- PHASE_3B_REQUEST_CALLBACK_TOKEN
+ PHASE_2_CALLBACK_TOKEN
}
}
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 1d69685df10..f8521be9d19 100644
--- a/driver-core/src/test/resources/auth/legacy/connection-string.json
+++ b/driver-core/src/test/resources/auth/legacy/connection-string.json
@@ -446,68 +446,7 @@
}
},
{
- "description": "should recognise the mechanism and request callback (MONGODB-OIDC)",
- "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC",
- "callback": ["oidcRequest"],
- "valid": true,
- "credential": {
- "username": null,
- "password": null,
- "source": "$external",
- "mechanism": "MONGODB-OIDC",
- "mechanism_properties": {
- "REQUEST_TOKEN_CALLBACK": true
- }
- }
- },
- {
- "description": "should recognise the mechanism when auth source is explicitly specified and with request callback (MONGODB-OIDC)",
- "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external",
- "callback": ["oidcRequest"],
- "valid": true,
- "credential": {
- "username": null,
- "password": null,
- "source": "$external",
- "mechanism": "MONGODB-OIDC",
- "mechanism_properties": {
- "REQUEST_TOKEN_CALLBACK": true
- }
- }
- },
- {
- "description": "should recognise the mechanism with request and refresh callback (MONGODB-OIDC)",
- "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC",
- "callback": ["oidcRequest", "oidcRefresh"],
- "valid": true,
- "credential": {
- "username": null,
- "password": null,
- "source": "$external",
- "mechanism": "MONGODB-OIDC",
- "mechanism_properties": {
- "REQUEST_TOKEN_CALLBACK": true,
- "REFRESH_TOKEN_CALLBACK": true
- }
- }
- },
- {
- "description": "should recognise the mechanism and username with request callback (MONGODB-OIDC)",
- "uri": "mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC",
- "callback": ["oidcRequest"],
- "valid": true,
- "credential": {
- "username": "principalName",
- "password": null,
- "source": "$external",
- "mechanism": "MONGODB-OIDC",
- "mechanism_properties": {
- "REQUEST_TOKEN_CALLBACK": true
- }
- }
- },
- {
- "description": "should recognise the mechanism with aws device (MONGODB-OIDC)",
+ "description": "should recognise the mechanism with aws provider (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws",
"valid": true,
"credential": {
@@ -521,7 +460,7 @@
}
},
{
- "description": "should recognise the mechanism when auth source is explicitly specified and with aws device (MONGODB-OIDC)",
+ "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",
"valid": true,
"credential": {
@@ -535,51 +474,29 @@
}
},
{
- "description": "should throw an exception if username and password are specified (MONGODB-OIDC)",
- "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC",
- "callback": ["oidcRequest"],
+ "description": "should throw an exception if supplied a password (MONGODB-OIDC)",
+ "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws",
"valid": false,
"credential": null
},
{
- "description": "should throw an exception if username and deviceName are specified (MONGODB-OIDC)",
- "uri": "mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&PROVIDER_NAME:gcp",
+ "description": "should throw an exception if username is specified for aws (MONGODB-OIDC)",
+ "uri": "mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&PROVIDER_NAME:aws",
"valid": false,
"credential": null
},
{
- "description": "should throw an exception if specified deviceName is not supported (MONGODB-OIDC)",
- "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:unexisted",
+ "description": "should throw an exception if specified provider is not supported (MONGODB-OIDC)",
+ "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:invalid",
"valid": false,
"credential": null
},
{
- "description": "should throw an exception if neither deviceName nor callbacks specified (MONGODB-OIDC)",
+ "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 throw an exception when only refresh callback is specified (MONGODB-OIDC)",
- "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC",
- "callback": ["oidcRefresh"],
- "valid": false,
- "credential": null
- },
- {
- "description": "should throw an exception if provider name and request callback are specified",
- "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws",
- "callback": ["oidcRequest"],
- "valid": false,
- "credential": null
- },
- {
- "description": "should throw an exception if provider name and refresh callback are specified",
- "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws",
- "callback": ["oidcRefresh"],
- "valid": false,
- "credential": null
- },
{
"description": "should throw an exception when unsupported auth property is specified (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=UnsupportedProperty:unexisted",
@@ -587,4 +504,4 @@
"credential": null
}
]
-}
\ No newline at end of file
+}
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
new file mode 100644
index 00000000000..7287c2486f0
--- /dev/null
+++ b/driver-core/src/test/resources/unified-test-format/auth/mongodb-oidc-no-retry.json
@@ -0,0 +1,428 @@
+{
+ "description": "MONGODB-OIDC authentication with retry disabled",
+ "schemaVersion": "1.19",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "7.0",
+ "auth": true,
+ "authMechanism": "MONGODB-OIDC"
+ }
+ ],
+ "createEntities": [
+ {
+ "client": {
+ "id": "failPointClient",
+ "useMultipleMongoses": false
+ }
+ },
+ {
+ "client": {
+ "id": "client0",
+ "uriOptions": {
+ "authMechanism": "MONGODB-OIDC",
+ "authMechanismProperties": {
+ "$$placeholder": 1
+ },
+ "retryReads": false,
+ "retryWrites": false
+ },
+ "observeEvents": [
+ "commandStartedEvent",
+ "commandSucceededEvent",
+ "commandFailedEvent"
+ ]
+ }
+ },
+ {
+ "database": {
+ "id": "database0",
+ "client": "client0",
+ "databaseName": "test"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection0",
+ "database": "database0",
+ "collectionName": "collName"
+ }
+ }
+ ],
+ "initialData": [
+ {
+ "collectionName": "collName",
+ "databaseName": "test",
+ "documents": [
+
+ ]
+ }
+ ],
+ "tests": [
+ {
+ "description": "A read operation should succeed",
+ "operations": [
+ {
+ "name": "find",
+ "object": "collection0",
+ "arguments": {
+ "filter": {
+ }
+ },
+ "expectResult": [
+
+ ]
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "find": "collName",
+ "filter": {
+ }
+ }
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "find"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "A write operation should succeed",
+ "operations": [
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "x": 1
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "insert": "collName",
+ "documents": [
+ {
+ "_id": 1,
+ "x": 1
+ }
+ ]
+ }
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "insert"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "Read commands should reauthenticate and retry when a ReauthenticationRequired error happens",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "failPointClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "find"
+ ],
+ "errorCode": 391
+ }
+ }
+ }
+ },
+ {
+ "name": "find",
+ "object": "collection0",
+ "arguments": {
+ "filter": {
+ }
+ },
+ "expectResult": [
+
+ ]
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "find": "collName",
+ "filter": {
+ }
+ }
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "find": "collName",
+ "filter": {
+ }
+ }
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "find"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "Write commands should reauthenticate and retry when a ReauthenticationRequired error happens",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "failPointClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "insert"
+ ],
+ "errorCode": 391
+ }
+ }
+ }
+ },
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "x": 1
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "insert": "collName",
+ "documents": [
+ {
+ "_id": 1,
+ "x": 1
+ }
+ ]
+ }
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "insert": "collName",
+ "documents": [
+ {
+ "_id": 1,
+ "x": 1
+ }
+ ]
+ }
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "insert"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "Handshake with cached token should use speculative authentication",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "failPointClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "insert"
+ ],
+ "closeConnection": true
+ }
+ }
+ }
+ },
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "x": 1
+ }
+ },
+ "expectError": {
+ "isClientError": true
+ }
+ },
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "failPointClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "saslStart"
+ ],
+ "errorCode": 20
+ }
+ }
+ }
+ },
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "x": 1
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "insert": "collName",
+ "documents": [
+ {
+ "_id": 1,
+ "x": 1
+ }
+ ]
+ }
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "insert": "collName",
+ "documents": [
+ {
+ "_id": 1,
+ "x": 1
+ }
+ ]
+ }
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "insert"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "Handshake without cached token should not use speculative authentication",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "failPointClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "saslStart"
+ ],
+ "errorCode": 20
+ }
+ }
+ }
+ },
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "x": 1
+ }
+ },
+ "expectError": {
+ "errorCode": 20
+ }
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/driver-core/src/test/resources/unified-test-format/auth/reauthenticate_with_retry.json b/driver-core/src/test/resources/unified-test-format/auth/reauthenticate_with_retry.json
deleted file mode 100644
index c99ebc6ece2..00000000000
--- a/driver-core/src/test/resources/unified-test-format/auth/reauthenticate_with_retry.json
+++ /dev/null
@@ -1,191 +0,0 @@
-{
- "description": "reauthenticate_with_retry",
- "schemaVersion": "1.12",
- "runOnRequirements": [
- {
- "minServerVersion": "6.3",
- "auth": true
- }
- ],
- "createEntities": [
- {
- "client": {
- "id": "client0",
- "uriOptions": {
- "retryReads": true,
- "retryWrites": true
- },
- "observeEvents": [
- "commandStartedEvent",
- "commandSucceededEvent",
- "commandFailedEvent"
- ]
- }
- },
- {
- "database": {
- "id": "database0",
- "client": "client0",
- "databaseName": "db"
- }
- },
- {
- "collection": {
- "id": "collection0",
- "database": "database0",
- "collectionName": "collName"
- }
- }
- ],
- "initialData": [
- {
- "collectionName": "collName",
- "databaseName": "db",
- "documents": []
- }
- ],
- "tests": [
- {
- "description": "Read command should reauthenticate when receive ReauthenticationRequired error code and retryReads=true",
- "operations": [
- {
- "name": "failPoint",
- "object": "testRunner",
- "arguments": {
- "client": "client0",
- "failPoint": {
- "configureFailPoint": "failCommand",
- "mode": {
- "times": 1
- },
- "data": {
- "failCommands": [
- "find"
- ],
- "errorCode": 391
- }
- }
- }
- },
- {
- "name": "find",
- "arguments": {
- "filter": {}
- },
- "object": "collection0",
- "expectResult": []
- }
- ],
- "expectEvents": [
- {
- "client": "client0",
- "events": [
- {
- "commandStartedEvent": {
- "command": {
- "find": "collName",
- "filter": {}
- }
- }
- },
- {
- "commandFailedEvent": {
- "commandName": "find"
- }
- },
- {
- "commandStartedEvent": {
- "command": {
- "find": "collName",
- "filter": {}
- }
- }
- },
- {
- "commandSucceededEvent": {
- "commandName": "find"
- }
- }
- ]
- }
- ]
- },
- {
- "description": "Write command should reauthenticate when receive ReauthenticationRequired error code and retryWrites=true",
- "operations": [
- {
- "name": "failPoint",
- "object": "testRunner",
- "arguments": {
- "client": "client0",
- "failPoint": {
- "configureFailPoint": "failCommand",
- "mode": {
- "times": 1
- },
- "data": {
- "failCommands": [
- "insert"
- ],
- "errorCode": 391
- }
- }
- }
- },
- {
- "name": "insertOne",
- "object": "collection0",
- "arguments": {
- "document": {
- "_id": 1,
- "x": 1
- }
- }
- }
- ],
- "expectEvents": [
- {
- "client": "client0",
- "events": [
- {
- "commandStartedEvent": {
- "command": {
- "insert": "collName",
- "documents": [
- {
- "_id": 1,
- "x": 1
- }
- ]
- }
- }
- },
- {
- "commandFailedEvent": {
- "commandName": "insert"
- }
- },
- {
- "commandStartedEvent": {
- "command": {
- "insert": "collName",
- "documents": [
- {
- "_id": 1,
- "x": 1
- }
- ]
- }
- }
- },
- {
- "commandSucceededEvent": {
- "commandName": "insert"
- }
- }
- ]
- }
- ]
- }
- ]
-}
\ No newline at end of file
diff --git a/driver-core/src/test/resources/unified-test-format/auth/reauthenticate_without_retry.json b/driver-core/src/test/resources/unified-test-format/auth/reauthenticate_without_retry.json
deleted file mode 100644
index 799057bf74f..00000000000
--- a/driver-core/src/test/resources/unified-test-format/auth/reauthenticate_without_retry.json
+++ /dev/null
@@ -1,191 +0,0 @@
-{
- "description": "reauthenticate_without_retry",
- "schemaVersion": "1.12",
- "runOnRequirements": [
- {
- "minServerVersion": "6.3",
- "auth": true
- }
- ],
- "createEntities": [
- {
- "client": {
- "id": "client0",
- "uriOptions": {
- "retryReads": false,
- "retryWrites": false
- },
- "observeEvents": [
- "commandStartedEvent",
- "commandSucceededEvent",
- "commandFailedEvent"
- ]
- }
- },
- {
- "database": {
- "id": "database0",
- "client": "client0",
- "databaseName": "db"
- }
- },
- {
- "collection": {
- "id": "collection0",
- "database": "database0",
- "collectionName": "collName"
- }
- }
- ],
- "initialData": [
- {
- "collectionName": "collName",
- "databaseName": "db",
- "documents": []
- }
- ],
- "tests": [
- {
- "description": "Read command should reauthenticate when receive ReauthenticationRequired error code and retryReads=false",
- "operations": [
- {
- "name": "failPoint",
- "object": "testRunner",
- "arguments": {
- "client": "client0",
- "failPoint": {
- "configureFailPoint": "failCommand",
- "mode": {
- "times": 1
- },
- "data": {
- "failCommands": [
- "find"
- ],
- "errorCode": 391
- }
- }
- }
- },
- {
- "name": "find",
- "arguments": {
- "filter": {}
- },
- "object": "collection0",
- "expectResult": []
- }
- ],
- "expectEvents": [
- {
- "client": "client0",
- "events": [
- {
- "commandStartedEvent": {
- "command": {
- "find": "collName",
- "filter": {}
- }
- }
- },
- {
- "commandFailedEvent": {
- "commandName": "find"
- }
- },
- {
- "commandStartedEvent": {
- "command": {
- "find": "collName",
- "filter": {}
- }
- }
- },
- {
- "commandSucceededEvent": {
- "commandName": "find"
- }
- }
- ]
- }
- ]
- },
- {
- "description": "Write command should reauthenticate when receive ReauthenticationRequired error code and retryWrites=false",
- "operations": [
- {
- "name": "failPoint",
- "object": "testRunner",
- "arguments": {
- "client": "client0",
- "failPoint": {
- "configureFailPoint": "failCommand",
- "mode": {
- "times": 1
- },
- "data": {
- "failCommands": [
- "insert"
- ],
- "errorCode": 391
- }
- }
- }
- },
- {
- "name": "insertOne",
- "object": "collection0",
- "arguments": {
- "document": {
- "_id": 1,
- "x": 1
- }
- }
- }
- ],
- "expectEvents": [
- {
- "client": "client0",
- "events": [
- {
- "commandStartedEvent": {
- "command": {
- "insert": "collName",
- "documents": [
- {
- "_id": 1,
- "x": 1
- }
- ]
- }
- }
- },
- {
- "commandFailedEvent": {
- "commandName": "insert"
- }
- },
- {
- "commandStartedEvent": {
- "command": {
- "insert": "collName",
- "documents": [
- {
- "_id": 1,
- "x": 1
- }
- ]
- }
- }
- },
- {
- "commandSucceededEvent": {
- "commandName": "insert"
- }
- }
- ]
- }
- ]
- }
- ]
-}
\ No newline at end of file
diff --git a/driver-core/src/test/unit/com/mongodb/AuthConnectionStringTest.java b/driver-core/src/test/unit/com/mongodb/AuthConnectionStringTest.java
index 7f4acab857d..4da83dc7d4f 100644
--- a/driver-core/src/test/unit/com/mongodb/AuthConnectionStringTest.java
+++ b/driver-core/src/test/unit/com/mongodb/AuthConnectionStringTest.java
@@ -37,8 +37,7 @@
import java.util.List;
import static com.mongodb.AuthenticationMechanism.MONGODB_OIDC;
-import static com.mongodb.MongoCredential.REFRESH_TOKEN_CALLBACK_KEY;
-import static com.mongodb.MongoCredential.REQUEST_TOKEN_CALLBACK_KEY;
+import static com.mongodb.MongoCredential.OIDC_CALLBACK_KEY;
// See https://github.com/mongodb/specifications/tree/master/source/auth/legacy/tests
@RunWith(Parameterized.class)
@@ -119,12 +118,8 @@ private MongoCredential getMongoCredential() {
String string = ((BsonString) v).getValue();
if ("oidcRequest".equals(string)) {
credential = credential.withMechanismProperty(
- REQUEST_TOKEN_CALLBACK_KEY,
+ OIDC_CALLBACK_KEY,
(MongoCredential.OidcRequestCallback) (context) -> null);
- } else if ("oidcRefresh".equals(string)) {
- credential = credential.withMechanismProperty(
- REFRESH_TOKEN_CALLBACK_KEY,
- (MongoCredential.OidcRefreshCallback) (context) -> null);
} else {
fail("Unsupported callback: " + string);
}
@@ -180,14 +175,10 @@ private void assertMechanismProperties(final MongoCredential credential) {
}
} else if ((document.get(key).isBoolean())) {
boolean expectedValue = document.getBoolean(key).getValue();
- if (REQUEST_TOKEN_CALLBACK_KEY.equals(key)) {
+ if (OIDC_CALLBACK_KEY.equals(key)) {
assertTrue(actualMechanismProperty instanceof MongoCredential.OidcRequestCallback);
return;
}
- if (REFRESH_TOKEN_CALLBACK_KEY.equals(key)) {
- assertTrue(actualMechanismProperty instanceof MongoCredential.OidcRefreshCallback);
- return;
- }
assertNotNull(actualMechanismProperty);
assertEquals(expectedValue, actualMechanismProperty);
} else {
diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationAsyncProseTests.java b/driver-reactive-streams/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationAsyncProseTests.java
index b18825e89a8..276dc9b68a9 100644
--- a/driver-reactive-streams/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationAsyncProseTests.java
+++ b/driver-reactive-streams/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationAsyncProseTests.java
@@ -42,10 +42,9 @@ public void testNonblockingCallbacks() {
delayNextFind();
int simulatedDelayMs = 100;
- TestCallback requestCallback = createCallback().setExpired().setDelayMs(simulatedDelayMs);
- TestCallback refreshCallback = createCallback().setDelayMs(simulatedDelayMs);
+ TestCallback requestCallback = createCallback().setDelayMs(simulatedDelayMs);
- MongoClientSettings clientSettings = createSettings(OIDC_URL, requestCallback, refreshCallback);
+ MongoClientSettings clientSettings = createSettings(getOidcUri(), requestCallback);
try (com.mongodb.reactivestreams.client.MongoClient client = MongoClients.create(clientSettings)) {
executeAll(2, () -> {
@@ -64,7 +63,6 @@ public void testNonblockingCallbacks() {
// ensure both callbacks have been tested
assertEquals(1, requestCallback.getInvocations());
- assertEquals(1, refreshCallback.getInvocations());
}
}
}
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 ebf09b0ab7c..26eca9e89ba 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,13 +16,16 @@
package com.mongodb.client.unified;
+import com.mongodb.AuthenticationMechanism;
import com.mongodb.ClientEncryptionSettings;
import com.mongodb.ClientSessionOptions;
import com.mongodb.MongoClientSettings;
+import com.mongodb.MongoCredential;
import com.mongodb.ReadConcern;
import com.mongodb.ReadConcernLevel;
import com.mongodb.ServerApi;
import com.mongodb.ServerApiVersion;
+import com.mongodb.internal.connection.OidcAuthenticator;
import com.mongodb.internal.connection.TestClusterListener;
import com.mongodb.logging.TestLoggingInterceptor;
import com.mongodb.TransactionOptions;
@@ -67,9 +70,15 @@
import org.bson.BsonDouble;
import org.bson.BsonInt32;
import org.bson.BsonInt64;
+import org.bson.BsonNumber;
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.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
@@ -95,6 +104,7 @@
import static com.mongodb.client.unified.UnifiedCrudHelper.asReadPreference;
import static com.mongodb.client.unified.UnifiedCrudHelper.asWriteConcern;
import static com.mongodb.internal.connection.AbstractConnectionPoolTest.waitForPoolAsyncWorkManagerStart;
+import static java.lang.System.getenv;
import static java.util.Arrays.asList;
import static java.util.Collections.synchronizedList;
import static org.junit.Assume.assumeTrue;
@@ -499,6 +509,33 @@ private void initClient(final BsonDocument entity, final String id,
case "appName":
clientSettingsBuilder.applicationName(value.asString().getValue());
break;
+ case "authMechanism":
+ if (value.equals(new BsonString(AuthenticationMechanism.MONGODB_OIDC.getMechanismName()))) {
+ clientSettingsBuilder.credential(MongoCredential.createOidcCredential(null));
+ 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.OidcRequestCallback) 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.RequestCallbackResult(accessToken);
+ }));
+ break;
+ }
+ throw new UnsupportedOperationException("Failure to apply authMechanismProperties: " + value);
default:
throw new UnsupportedOperationException("Unsupported uri option: " + key);
}
diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/ErrorMatcher.java b/driver-sync/src/test/functional/com/mongodb/client/unified/ErrorMatcher.java
index e05420f36f8..44563ea2e80 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/unified/ErrorMatcher.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/unified/ErrorMatcher.java
@@ -20,6 +20,7 @@
import com.mongodb.MongoClientException;
import com.mongodb.MongoCommandException;
import com.mongodb.MongoException;
+import com.mongodb.MongoSecurityException;
import com.mongodb.MongoServerException;
import com.mongodb.MongoSocketException;
import com.mongodb.MongoWriteException;
@@ -72,12 +73,17 @@ void assertErrorsMatch(final BsonDocument expectedError, final Exception e) {
valueMatcher.assertValuesMatch(expectedError.getDocument("errorResponse"), ((MongoCommandException) e).getResponse());
}
if (expectedError.containsKey("errorCode")) {
- assertTrue(context.getMessage("Exception must be of type MongoCommandException or MongoQueryException when checking"
- + " for error codes"),
- e instanceof MongoCommandException || e instanceof MongoWriteException);
- int errorCode = (e instanceof MongoCommandException)
- ? ((MongoCommandException) e).getErrorCode()
- : ((MongoWriteException) e).getCode();
+ Exception errorCodeException = e;
+ if (e instanceof MongoSecurityException && e.getCause() instanceof MongoCommandException) {
+ errorCodeException = (Exception) e.getCause();
+ }
+ assertTrue(context.getMessage("Exception must be of type MongoCommandException or MongoWriteException when checking"
+ + " for error codes, but was " + e.getClass().getSimpleName()),
+ errorCodeException instanceof MongoCommandException
+ || errorCodeException instanceof MongoWriteException);
+ int errorCode = (errorCodeException instanceof MongoCommandException)
+ ? ((MongoCommandException) errorCodeException).getErrorCode()
+ : ((MongoWriteException) errorCodeException).getCode();
assertEquals(context.getMessage("Error codes must match"), expectedError.getNumber("errorCode").intValue(),
errorCode);
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 bf6c0dcda01..aa7a3f80a53 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
@@ -74,6 +74,15 @@ public static boolean runOnRequirementsMet(final BsonArray runOnRequirements, fi
break requirementLoop;
}
break;
+ case "authMechanism":
+ boolean containsMechanism = getServerParameters()
+ .getArray("authenticationMechanisms")
+ .contains(curRequirement.getValue());
+ if (!containsMechanism) {
+ requirementMet = false;
+ break requirementLoop;
+ }
+ break;
case "serverParameters":
BsonDocument serverParameters = getServerParameters();
for (Map.Entry curParameter: curRequirement.getValue().asDocument().entrySet()) {
diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java
index 8b76f426dbc..6a073a4c68f 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java
@@ -199,7 +199,9 @@ public void setUp() {
|| schemaVersion.equals("1.12")
|| schemaVersion.equals("1.13")
|| schemaVersion.equals("1.14")
- || schemaVersion.equals("1.15"));
+ || schemaVersion.equals("1.15")
+ || schemaVersion.equals("1.18")
+ || schemaVersion.equals("1.19"));
if (runOnRequirements != null) {
assumeTrue("Run-on requirements not met",
runOnRequirementsMet(runOnRequirements, getMongoClientSettings(), getServerVersion()));
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 368e1342e1f..66b6a305297 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
@@ -16,14 +16,13 @@
package com.mongodb.internal.connection;
+import com.mongodb.ClusterFixture;
import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.MongoCommandException;
import com.mongodb.MongoConfigurationException;
import com.mongodb.MongoCredential;
-import com.mongodb.MongoCredential.IdpResponse;
-import com.mongodb.MongoCredential.OidcRefreshCallback;
-import com.mongodb.MongoSecurityException;
+import com.mongodb.MongoCredential.RequestCallbackResult;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.TestListener;
@@ -39,48 +38,36 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.CsvSource;
import org.opentest4j.AssertionFailedError;
-import org.opentest4j.MultipleFailuresError;
import java.io.IOException;
+import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
-import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
-import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import static com.mongodb.MongoCredential.ALLOWED_HOSTS_KEY;
-import static com.mongodb.MongoCredential.IdpInfo;
-import static com.mongodb.MongoCredential.OidcRefreshContext;
import static com.mongodb.MongoCredential.OidcRequestCallback;
import static com.mongodb.MongoCredential.OidcRequestContext;
-import static com.mongodb.MongoCredential.PROVIDER_NAME_KEY;
-import static com.mongodb.MongoCredential.REFRESH_TOKEN_CALLBACK_KEY;
-import static com.mongodb.MongoCredential.REQUEST_TOKEN_CALLBACK_KEY;
-import static com.mongodb.MongoCredential.createOidcCredential;
-import static com.mongodb.client.TestHelper.setEnvironmentVariable;
+import static com.mongodb.MongoCredential.OIDC_CALLBACK_KEY;
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;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static util.ThreadTestHelpers.executeAll;
-
/**
* See
* Prose Tests.
@@ -91,28 +78,25 @@ public static boolean oidcTestsEnabled() {
return Boolean.parseBoolean(getenv().get("OIDC_TESTS_ENABLED"));
}
- private static final String AWS_WEB_IDENTITY_TOKEN_FILE = "AWS_WEB_IDENTITY_TOKEN_FILE";
+ private String appName;
- public static final String TOKEN_DIRECTORY = "/tmp/tokens/"; // TODO-OIDC
+ protected static String getOidcUri() {
+ ConnectionString cs = ClusterFixture.getConnectionString();
+ // remove username and password
+ return "mongodb+srv://" + cs.getHosts().get(0) + "/?authMechanism=MONGODB-OIDC";
+ }
- protected static final String OIDC_URL = "mongodb://localhost/?authMechanism=MONGODB-OIDC";
- private static final String AWS_OIDC_URL =
- "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws";
- private String appName;
+ private static String getAwsOidcUri() {
+ return getOidcUri() + "&authMechanismProperties=PROVIDER_NAME:aws";
+ }
protected MongoClient createMongoClient(final MongoClientSettings settings) {
return MongoClients.create(settings);
}
- protected void setOidcFile(final String file) {
- setEnvironmentVariable(AWS_WEB_IDENTITY_TOKEN_FILE, TOKEN_DIRECTORY + file);
- }
-
@BeforeEach
public void beforeEach() {
assumeTrue(oidcTestsEnabled());
- // In each test, clearing the cache is not required, since there is no global cache
- setOidcFile("test_user1");
InternalStreamConnection.setRecordEverything(true);
this.appName = this.getClass().getSimpleName() + "-" + new Random().nextInt(Integer.MAX_VALUE);
}
@@ -122,196 +106,77 @@ public void afterEach() {
InternalStreamConnection.setRecordEverything(false);
}
- @ParameterizedTest
- @CsvSource(delimiter = '#', value = {
- // 1.1 to 1.5:
- "test1p1 # test_user1 # " + OIDC_URL,
- "test1p2 # test_user1 # mongodb://test_user1@localhost/?authMechanism=MONGODB-OIDC",
- "test1p3 # test_user1 # mongodb://test_user1@localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred",
- "test1p4 # test_user2 # mongodb://test_user2@localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred",
- "test1p5 # invalid # mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred",
- })
- public void test1CallbackDrivenAuth(final String name, final String file, final String url) {
- boolean shouldPass = !file.equals("invalid");
- setOidcFile(file);
- // #. Create a request callback that returns a valid token.
- OidcRequestCallback onRequest = createCallback();
- // #. Create a client with a URL of the form ... and the OIDC request callback.
- MongoClientSettings clientSettings = createSettings(url, onRequest, null);
- // #. Perform a find operation that succeeds / fails
- if (shouldPass) {
- performFind(clientSettings);
- } else {
- performFind(
- clientSettings,
- MongoCommandException.class,
- "Command failed with error 18 (AuthenticationFailed)");
- }
- }
-
- @ParameterizedTest
- @CsvSource(delimiter = '#', value = {
- // 1.6, both variants:
- "'' # " + OIDC_URL,
- "example.com # mongodb://localhost/?authMechanism=MONGODB-OIDC&ignored=example.com",
- })
- public void test1p6CallbackDrivenAuthAllowedHostsBlocked(final String allowedHosts, final String url) {
- // Create a client that uses the OIDC url and a request callback, and an ALLOWED_HOSTS that contains...
- List allowedHostsList = asList(allowedHosts.split(","));
- MongoClientSettings settings = createSettings(url, createCallback(), null, allowedHostsList, null);
- // #. Assert that a find operation fails with a client-side error.
- performFind(settings, MongoSecurityException.class, "");
- }
-
@Test
- public void test1p7LockAvoidsExtraCallbackCalls() {
- proveThatConcurrentCallbacksThrow();
- // The test requires that two operations are attempted concurrently.
- // The delay on the next find should cause the initial request to delay
- // and the ensuing refresh to block, rather than entering onRefresh.
- // After blocking, this ensuing refresh thread will enter onRefresh.
- AtomicInteger concurrent = new AtomicInteger();
- TestCallback onRequest = createCallback().setExpired().setConcurrentTracker(concurrent);
- TestCallback onRefresh = createCallback().setConcurrentTracker(concurrent);
- MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest, onRefresh);
- try (MongoClient mongoClient = createMongoClient(clientSettings)) {
- delayNextFind(); // cause both callbacks to be called
- executeAll(2, () -> performFind(mongoClient));
- assertEquals(1, onRequest.getInvocations());
- assertEquals(1, onRefresh.getInvocations());
- }
- }
-
- public void proveThatConcurrentCallbacksThrow() {
- // ensure that, via delay, test callbacks throw when invoked concurrently
- AtomicInteger c = new AtomicInteger();
- TestCallback request = createCallback().setConcurrentTracker(c).setDelayMs(5);
- TestCallback refresh = createCallback().setConcurrentTracker(c);
- IdpInfo serverInfo = new OidcAuthenticator.IdpInfoImpl("issuer", "clientId", asList());
- executeAll(() -> {
- sleep(2);
- assertThrows(RuntimeException.class, () -> {
- refresh.onRefresh(new OidcAuthenticator.OidcRefreshContextImpl(serverInfo, "refToken", Duration.ofSeconds(1234)));
- });
- }, () -> {
- request.onRequest(new OidcAuthenticator.OidcRequestContextImpl(serverInfo, Duration.ofSeconds(1234)));
- });
- }
-
- private void sleep(final long ms) {
- try {
- Thread.sleep(ms);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
-
- @ParameterizedTest
- @CsvSource(delimiter = '#', value = {
- // 2.1 to 2.3:
- "test2p1 # test_user1 # " + AWS_OIDC_URL,
- "test2p2 # test_user1 # mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred",
- "test2p3 # test_user2 # mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred",
- })
- public void test2AwsAutomaticAuth(final String name, final String file, final String url) {
- setOidcFile(file);
- // #. Create a client with a url of the form ...
- MongoCredential credential = createOidcCredential(null)
- .withMechanismProperty(PROVIDER_NAME_KEY, "aws");
- MongoClientSettings clientSettings = MongoClientSettings.builder()
- .applicationName(appName)
- .credential(credential)
- .applyConnectionString(new ConnectionString(url))
- .build();
- // #. Perform a find operation that succeeds.
+ public void test1p1CallbackIsCalledDuringAuth() {
+ // #. Create a ``MongoClient`` configured with an OIDC callback...
+ TestCallback onRequest = createCallback();
+ MongoClientSettings clientSettings = createSettings(getOidcUri(), onRequest, null);
+ // #. Perform a find operation that succeeds
performFind(clientSettings);
+ assertEquals(1, onRequest.invocations.get());
}
@Test
- public void test2p4AllowedHostsIgnored() {
- MongoClientSettings settings = createSettings(
- AWS_OIDC_URL, null, null, Arrays.asList(), null);
- performFind(settings);
+ public void test1p2CallbackCalledOnceForMultipleConnections() {
+ TestCallback onRequest = createCallback();
+ MongoClientSettings clientSettings = createSettings(getOidcUri(), onRequest, null);
+ try (MongoClient mongoClient = createMongoClient(clientSettings)) {
+ List threads = new ArrayList<>();
+ for (int i = 0; i < 10; i++) {
+ Thread t = new Thread(() -> performFind(mongoClient));
+ t.setDaemon(true);
+ t.start();
+ threads.add(t);
+ }
+ for (Thread t : threads) {
+ try {
+ t.join();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ assertEquals(1, onRequest.invocations.get());
}
@Test
- public void test3p1ValidCallbacks() {
- String connectionString = "mongodb://test_user1@localhost/?authMechanism=MONGODB-OIDC";
- String expectedClientId = "0oadp0hpl7q3UIehP297";
- String expectedIssuer = "https://ebgxby0dw8.execute-api.us-west-1.amazonaws.com/default/mock-identity-config-oidc";
+ public void test2p1ValidCallbackInputs() {
+ String connectionString = getOidcUri();
Duration expectedSeconds = Duration.ofMinutes(5);
- TestCallback onRequest = createCallback().setExpired();
- TestCallback onRefresh = createCallback();
+ TestCallback onRequest = createCallback();
// #. Verify that the request callback was called with the appropriate
// inputs, including the timeout parameter if possible.
- // #. Verify that the refresh callback was called with the appropriate
- // inputs, including the timeout parameter if possible.
OidcRequestCallback onRequest2 = (context) -> {
- assertEquals(expectedClientId, context.getIdpInfo().getClientId());
- assertEquals(expectedIssuer, context.getIdpInfo().getIssuer());
- assertEquals(Arrays.asList(), context.getIdpInfo().getRequestScopes());
assertEquals(expectedSeconds, context.getTimeout());
return onRequest.onRequest(context);
};
- OidcRefreshCallback onRefresh2 = (context) -> {
- assertEquals(expectedClientId, context.getIdpInfo().getClientId());
- assertEquals(expectedIssuer, context.getIdpInfo().getIssuer());
- assertEquals(Arrays.asList(), context.getIdpInfo().getRequestScopes());
- assertEquals(expectedSeconds, context.getTimeout());
- assertEquals("refreshToken", context.getRefreshToken());
- return onRefresh.onRefresh(context);
- };
- MongoClientSettings clientSettings = createSettings(connectionString, onRequest2, onRefresh2);
+ MongoClientSettings clientSettings = createSettings(connectionString, onRequest2);
try (MongoClient mongoClient = createMongoClient(clientSettings)) {
- delayNextFind(); // cause both callbacks to be called
- executeAll(2, () -> performFind(mongoClient));
- // Ensure that both callbacks were called
+ performFind(mongoClient);
+ // callback was called
assertEquals(1, onRequest.getInvocations());
- assertEquals(1, onRefresh.getInvocations());
}
}
@Test
- public void test3p2RequestCallbackReturnsNull() {
+ public void test2p2RequestCallbackReturnsNull() {
//noinspection ConstantConditions
OidcRequestCallback onRequest = (context) -> null;
- MongoClientSettings settings = this.createSettings(OIDC_URL, onRequest, null);
+ MongoClientSettings settings = this.createSettings(getOidcUri(), onRequest, null);
performFind(settings, MongoConfigurationException.class, "Result of callback must not be null");
}
@Test
- public void test3p3RefreshCallbackReturnsNull() {
- TestCallback onRequest = createCallback().setExpired().setDelayMs(100);
- //noinspection ConstantConditions
- OidcRefreshCallback onRefresh = (context) -> null;
- MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest, onRefresh);
- try (MongoClient mongoClient = createMongoClient(clientSettings)) {
- delayNextFind(); // cause both callbacks to be called
- try {
- executeAll(2, () -> performFind(mongoClient));
- } catch (MultipleFailuresError actual) {
- assertEquals(1, actual.getFailures().size());
- assertCause(
- MongoConfigurationException.class,
- "Result of callback must not be null",
- actual.getFailures().get(0));
- }
- assertEquals(1, onRequest.getInvocations());
- }
- }
-
- @Test
- public void test3p4RequestCallbackReturnsInvalidData() {
+ public void test2p3CallbackReturnsMissingData() {
// #. Create a client with a request callback that returns data not
// conforming to the OIDCRequestTokenResult with missing field(s).
- // #. ... with extra field(s). - not possible
OidcRequestCallback onRequest = (context) -> {
//noinspection ConstantConditions
- return new IdpResponse(null, null, null);
+ return new RequestCallbackResult(null);
};
// we ensure that the error is propagated
- MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest, null);
+ MongoClientSettings clientSettings = createSettings(getOidcUri(), onRequest, null);
try (MongoClient mongoClient = createMongoClient(clientSettings)) {
try {
performFind(mongoClient);
@@ -323,399 +188,100 @@ public void test3p4RequestCallbackReturnsInvalidData() {
}
@Test
- public void test3p5RefreshCallbackReturnsInvalidData() {
- TestCallback onRequest = createCallback().setExpired();
- OidcRefreshCallback onRefresh = (context) -> {
- //noinspection ConstantConditions
- return new IdpResponse(null, null, null);
- };
- MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest, onRefresh);
- try (MongoClient mongoClient = createMongoClient(clientSettings)) {
- try {
- executeAll(2, () -> performFind(mongoClient));
- } catch (MultipleFailuresError actual) {
- assertEquals(1, actual.getFailures().size());
- assertCause(
- IllegalArgumentException.class,
- "accessToken can not be null",
- actual.getFailures().get(0));
- }
- assertEquals(1, onRequest.getInvocations());
- }
- }
-
- // 3.6 Refresh Callback Returns Extra Data - not possible due to use of class
-
- @Test
- public void test4p1CachedCredentialsCacheWithRefresh() {
- // #. Create a new client with a request callback that gives credentials that expire in one minute.
- TestCallback onRequest = createCallback().setExpired();
- TestCallback onRefresh = createCallback();
- MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest, onRefresh);
- try (MongoClient mongoClient = createMongoClient(clientSettings)) {
- // #. Create a new client with the same request callback and a refresh callback.
- // Instead:
- // 1. Delay the first find, causing the second find to authenticate a second connection
- delayNextFind(); // cause both callbacks to be called
- executeAll(2, () -> performFind(mongoClient));
- // #. Ensure that a find operation adds credentials to the cache.
- // #. Ensure that a find operation results in a call to the refresh callback.
- assertEquals(1, onRequest.getInvocations());
- assertEquals(1, onRefresh.getInvocations());
- // the refresh invocation will fail if the cached tokens are null
- // so a success implies that credentials were present in the cache
- }
- }
-
- @Test
- public void test4p2CachedCredentialsCacheWithNoRefresh() {
- // #. Create a new client with a request callback that gives credentials that expire in one minute.
- // #. Ensure that a find operation adds credentials to the cache.
- // #. Create a new client with a request callback but no refresh callback.
- // #. Ensure that a find operation results in a call to the request callback.
- TestCallback onRequest = createCallback().setExpired();
- MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest, null);
- try (MongoClient mongoClient = createMongoClient(clientSettings)) {
- delayNextFind(); // cause both callbacks to be called
- executeAll(2, () -> performFind(mongoClient));
- // test is the same as 4.1, but no onRefresh, and assert that the onRequest is called twice
- assertEquals(2, onRequest.getInvocations());
- }
- }
-
- // 4.3 Cache key includes callback - skipped:
- // If the driver does not support using callback references or hashes as part of the cache key, skip this test.
-
- @Test
- public void test4p4ErrorClearsCache() {
- // #. 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.
-
- TestListener listener = new TestListener();
- ConcurrentLinkedQueue tokens = tokenQueue(
- "test_user1",
- "test_user1_expires",
- "test_user1_expires",
- "test_user1_1");
- TestCallback onRequest = createCallback()
- .setExpired()
- .setPathSupplier(() -> tokens.remove())
- .setEventListener(listener);
- TestCallback onRefresh = createCallback()
- .setPathSupplier(() -> tokens.remove())
- .setEventListener(listener);
-
- TestCommandListener commandListener = new TestCommandListener(listener);
-
- MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest, onRefresh, null, commandListener);
- try (MongoClient mongoClient = createMongoClient(clientSettings)) {
- // #. Ensure that a find operation adds a new entry to the cache.
- performFind(mongoClient);
- assertEquals(Arrays.asList(
- "isMaster started",
- "isMaster succeeded",
- "onRequest invoked",
- "read access token: test_user1",
- "saslContinue started",
- "saslContinue succeeded",
- "find started",
- "find succeeded"
- ), listener.getEventStrings());
- listener.clear();
-
- // #. Ensure that a subsequent find operation results in a 391 error.
- failCommand(391, 1, "find");
- // ensure that the operation entirely fails, after attempting both potential fallback callbacks
- assertThrows(MongoSecurityException.class, () -> performFind(mongoClient));
- assertEquals(Arrays.asList(
- "find started",
- "find failed",
- "onRefresh invoked",
- "read access token: test_user1_expires",
- "saslStart started",
- "saslStart failed",
- // falling back to principal request, onRequest callback.
- "saslStart started",
- "saslStart succeeded",
- "onRequest invoked",
- "read access token: test_user1_expires",
- "saslContinue started",
- "saslContinue failed"
- ), listener.getEventStrings());
- listener.clear();
-
- // #. Ensure that the cache value cleared.
- failCommand(391, 1, "find");
- performFind(mongoClient);
- assertEquals(Arrays.asList(
- "find started",
- "find failed",
- // falling back to principal request, onRequest callback.
- // this implies that the cache has been cleared during the
- // preceding find operation.
- "saslStart started",
- "saslStart succeeded",
- "onRequest invoked",
- "read access token: test_user1_1",
- "saslContinue started",
- "saslContinue succeeded",
- // auth has finished
- "find started",
- "find succeeded"
- ), listener.getEventStrings());
- listener.clear();
+ public void test2p4InvalidClientConfigurationWithCallback() {
+ String awsOidcUri = getAwsOidcUri();
+ MongoClientSettings settings = createSettings(
+ awsOidcUri, createCallback(), null);
+ try {
+ performFind(settings);
+ fail();
+ } catch (Exception e) {
+ assertCause(IllegalArgumentException.class,
+ "OIDC_CALLBACK must not be specified when PROVIDER_NAME is specified", e);
}
}
- // not a prose test.
@Test
- public void testEventListenerMustNotLogReauthentication() {
- InternalStreamConnection.setRecordEverything(false);
-
- TestListener listener = new TestListener();
- ConcurrentLinkedQueue tokens = tokenQueue(
- "test_user1",
- "test_user1_expires",
- "test_user1_expires",
- "test_user1_1");
- TestCallback onRequest = createCallback()
- .setExpired()
- .setPathSupplier(() -> tokens.remove())
- .setEventListener(listener);
- TestCallback onRefresh = createCallback()
- .setPathSupplier(() -> tokens.remove())
- .setEventListener(listener);
-
- TestCommandListener commandListener = new TestCommandListener(listener);
-
- MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest, onRefresh, null, commandListener);
- try (MongoClient mongoClient = createMongoClient(clientSettings)) {
- performFind(mongoClient);
- assertEquals(Arrays.asList(
- "onRequest invoked",
- "read access token: test_user1",
- "find started",
- "find succeeded"
- ), listener.getEventStrings());
- listener.clear();
-
- failCommand(391, 1, "find");
- assertThrows(MongoSecurityException.class, () -> performFind(mongoClient));
- assertEquals(Arrays.asList(
- "find started",
- "find failed",
- "onRefresh invoked",
- "read access token: test_user1_expires",
- // falling back to principal request, onRequest callback
- "onRequest invoked",
- "read access token: test_user1_expires"
- ), listener.getEventStrings());
- }
- }
+ public void test3p1AuthFailsWithCachedToken() throws ExecutionException, InterruptedException, NoSuchFieldException, IllegalAccessException {
+ TestCallback onRequestWrapped = createCallback();
+ CompletableFuture poisonToken = new CompletableFuture<>();
+ OidcRequestCallback onRequest = (context) -> {
+ RequestCallbackResult result = onRequestWrapped.onRequest(context);
+ String accessToken = result.getAccessToken();
+ if (!poisonToken.isDone()) {
+ poisonToken.complete(accessToken);
+ }
+ return result;
+ };
- @Test
- public void test4p5AwsAutomaticWorkflowDoesNotUseCache() {
- // #. Create a new client that uses the AWS automatic workflow.
- // #. Ensure that a find operation does not add credentials to the cache.
- setOidcFile("test_user1");
- MongoCredential credential = createOidcCredential(null)
- .withMechanismProperty(PROVIDER_NAME_KEY, "aws");
- ConnectionString connectionString = new ConnectionString(AWS_OIDC_URL);
- MongoClientSettings clientSettings = MongoClientSettings.builder()
- .applicationName(appName)
- .credential(credential)
- .applyConnectionString(connectionString)
- .build();
+ MongoClientSettings clientSettings = createSettings(getOidcUri(), onRequest, null);
try (MongoClient mongoClient = createMongoClient(clientSettings)) {
+ // populate cache
performFind(mongoClient);
- // This ensures that the next find failure results in a file (rather than cache) read
- failCommand(391, 1, "find");
- setOidcFile("invalid_file");
- assertCause(NoSuchFileException.class, "invalid_file", () -> performFind(mongoClient));
- }
- }
-
- @Test
- public void test5SpeculativeAuthentication() {
- // #. We can only test the successful case, by verifying that saslStart is not called.
- // #. Create a client with a request callback that returns a valid token that will not expire soon.
- TestListener listener = new TestListener();
- TestCallback onRequest = createCallback().setEventListener(listener);
- TestCommandListener commandListener = new TestCommandListener(listener);
- MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest, null, null, commandListener);
- try (MongoClient mongoClient = createMongoClient(clientSettings)) {
- // instead of setting failpoints for saslStart, we inspect events
- delayNextFind();
+ assertEquals(1, onRequestWrapped.invocations.get());
+ // Poison the *Client Cache* with an invalid access token.
+ // uses reflection
+ String poisonString = poisonToken.get();
+ Field f = String.class.getDeclaredField("value");
+ f.setAccessible(true);
+ byte[] poisonChars = (byte[]) f.get(poisonString);
+ poisonChars[0] = '~';
+ poisonChars[1] = '~';
+
+ assertEquals(1, onRequestWrapped.invocations.get());
+
+ // cause another connection to be opened
+ delayNextFind(); // cause both callbacks to be called
executeAll(2, () -> performFind(mongoClient));
-
- List events = listener.getEventStrings();
- assertFalse(events.stream().anyMatch(e -> e.contains("saslStart")));
- // onRequest is 2-step, so we expect 2 continues
- assertEquals(2, events.stream().filter(e -> e.contains("saslContinue started")).count());
- // confirm all commands are enabled
- assertTrue(events.stream().anyMatch(e -> e.contains("isMaster started")));
- }
- }
-
- // Not a prose test
- @Test
- public void testAutomaticAuthUsesSpeculative() {
- TestListener listener = new TestListener();
- TestCommandListener commandListener = new TestCommandListener(listener);
-
- MongoClientSettings settings = createSettings(
- AWS_OIDC_URL, null, null, Arrays.asList(), commandListener);
- try (MongoClient mongoClient = createMongoClient(settings)) {
- // we use a listener instead of a failpoint
- performFind(mongoClient);
- assertEquals(Arrays.asList(
- "isMaster started",
- "isMaster succeeded",
- "find started",
- "find succeeded"
- ), listener.getEventStrings());
}
+ assertEquals(2, onRequestWrapped.invocations.get());
}
@Test
- public void test6p1ReauthenticationSucceeds() {
- // #. Create request and refresh callbacks that return valid credentials that will not expire soon.
- TestListener listener = new TestListener();
- TestCallback onRequest = createCallback().setEventListener(listener);
- TestCallback onRefresh = createCallback().setEventListener(listener);
-
- // #. Create a client with the callbacks and an event listener capable of listening for SASL commands.
- TestCommandListener commandListener = new TestCommandListener(listener);
-
- MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest, onRefresh, null, commandListener);
+ public void test3p2AuthFailsWithoutCachedToken() {
+ MongoClientSettings clientSettings = createSettings(getOidcUri(),
+ (x) -> new RequestCallbackResult("invalid_token"), null);
try (MongoClient mongoClient = createMongoClient(clientSettings)) {
-
- // #. Perform a find operation that succeeds.
- performFind(mongoClient);
-
- // #. Assert that the refresh callback has not been called.
- assertEquals(0, onRefresh.getInvocations());
-
- assertEquals(Arrays.asList(
- // speculative:
- "isMaster started",
- "isMaster succeeded",
- // onRequest:
- "onRequest invoked",
- "read access token: test_user1",
- // jwt from onRequest:
- "saslContinue started",
- "saslContinue succeeded",
- // ensuing find:
- "find started",
- "find succeeded"
- ), listener.getEventStrings());
-
- // #. Clear the listener state if possible.
- commandListener.reset();
- listener.clear();
-
- // #. Force a reauthenication using a failCommand
- failCommand(391, 1, "find");
-
- // #. Perform another find operation that succeeds.
- performFind(mongoClient);
-
- // #. Assert that the ordering of command started events is: find, find.
- // #. Assert that the ordering of command succeeded events is: find.
- // #. Assert that a find operation failed once during the command execution.
- assertEquals(Arrays.asList(
- "find started",
- "find failed",
- // find has triggered 391, and cleared the access token; fall back to refresh:
- "onRefresh invoked",
- "read access token: test_user1",
- "saslStart started",
- "saslStart succeeded",
- // find retry succeeds:
- "find started",
- "find succeeded"
- ), listener.getEventStrings());
-
- // #. Assert that the refresh callback has been called once, if possible.
- assertEquals(1, onRefresh.getInvocations());
+ try {
+ performFind(mongoClient);
+ fail();
+ } catch (Exception e) {
+ assertCause(MongoCommandException.class,
+ "Command failed with error 18 (AuthenticationFailed):", e);
+ }
}
}
- @NotNull
- private ConcurrentLinkedQueue tokenQueue(final String... queue) {
- return Stream
- .of(queue)
- .map(v -> TOKEN_DIRECTORY + v)
- .collect(Collectors.toCollection(ConcurrentLinkedQueue::new));
- }
@Test
- public void test6p2ReauthenticationRetriesAndSucceedsWithCache() {
- // #. Create request and refresh callbacks that return valid credentials that will not expire soon.
+ public void test4p1Reauthentication() {
TestCallback onRequest = createCallback();
- TestCallback onRefresh = createCallback();
- MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest, onRefresh);
+ MongoClientSettings clientSettings = createSettings(getOidcUri(), onRequest);
try (MongoClient mongoClient = createMongoClient(clientSettings)) {
- // #. Perform a find operation that succeeds.
- performFind(mongoClient);
- // #. Force a reauthenication using a failCommand
failCommand(391, 1, "find");
// #. Perform a find operation that succeeds.
performFind(mongoClient);
}
- }
-
- // 6.3 Retries and Fails with no Cache
- // Appears to be untestable, since it requires 391 failure on jwt (may be fixed in future spec)
-
- @Test
- public void test6p4SeparateConnectionsAvoidExtraCallbackCalls() {
- ConcurrentLinkedQueue tokens = tokenQueue(
- "test_user1",
- "test_user1_1");
- TestCallback onRequest = createCallback().setPathSupplier(() -> tokens.remove());
- TestCallback onRefresh = createCallback().setPathSupplier(() -> tokens.remove());
- MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest, onRefresh);
- try (MongoClient mongoClient = createMongoClient(clientSettings)) {
- // #. Peform a find operation on each ... that succeeds.
- delayNextFind();
- executeAll(2, () -> performFind(mongoClient));
- // #. Ensure that the request callback has been called once and the refresh callback has not been called.
- assertEquals(1, onRequest.getInvocations());
- assertEquals(0, onRefresh.getInvocations());
-
- failCommand(391, 2, "find");
- executeAll(2, () -> performFind(mongoClient));
-
- // #. Ensure that the request callback has been called once and the refresh callback has been called once.
- assertEquals(1, onRequest.getInvocations());
- assertEquals(1, onRefresh.getInvocations());
- }
+ assertEquals(2, onRequest.invocations.get());
}
public MongoClientSettings createSettings(
final String connectionString,
- @Nullable final OidcRequestCallback onRequest,
- @Nullable final OidcRefreshCallback onRefresh) {
- return createSettings(connectionString, onRequest, onRefresh, null, null);
+ @Nullable final OidcRequestCallback onRequest) {
+ return createSettings(connectionString, onRequest, null);
}
private MongoClientSettings createSettings(
final String connectionString,
@Nullable final OidcRequestCallback onRequest,
- @Nullable final OidcRefreshCallback onRefresh,
- @Nullable final List allowedHosts,
@Nullable final CommandListener commandListener) {
ConnectionString cs = new ConnectionString(connectionString);
MongoCredential credential = cs.getCredential()
- .withMechanismProperty(REQUEST_TOKEN_CALLBACK_KEY, onRequest)
- .withMechanismProperty(REFRESH_TOKEN_CALLBACK_KEY, onRefresh)
- .withMechanismProperty(ALLOWED_HOSTS_KEY, allowedHosts);
+ .withMechanismProperty(OIDC_CALLBACK_KEY, onRequest);
MongoClientSettings.Builder builder = MongoClientSettings.builder()
.applicationName(appName)
.applyConnectionString(cs)
+ .retryReads(false)
.credential(credential);
if (commandListener != null) {
builder.addCommandListener(commandListener);
@@ -767,7 +333,7 @@ private static void assertCause(
}
protected void delayNextFind() {
- try (MongoClient client = createMongoClient(createSettings(AWS_OIDC_URL, null, null))) {
+ try (MongoClient client = createMongoClient(createSettings(getAwsOidcUri(), null, null))) {
BsonDocument failPointDocument = new BsonDocument("configureFailPoint", new BsonString("failCommand"))
.append("mode", new BsonDocument("times", new BsonInt32(1)))
.append("data", new BsonDocument()
@@ -781,7 +347,7 @@ protected void delayNextFind() {
protected void failCommand(final int code, final int times, final String... commands) {
try (MongoClient mongoClient = createMongoClient(createSettings(
- AWS_OIDC_URL, null, null))) {
+ getAwsOidcUri(), null, null))) {
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)))
@@ -793,11 +359,9 @@ protected void failCommand(final int code, final int times, final String... comm
}
}
- public static class TestCallback implements OidcRequestCallback, OidcRefreshCallback {
+ public static class TestCallback implements OidcRequestCallback {
private final AtomicInteger invocations = new AtomicInteger();
@Nullable
- private final Integer expiresInSeconds;
- @Nullable
private final Integer delayInMilliseconds;
@Nullable
private final AtomicInteger concurrentTracker;
@@ -807,16 +371,14 @@ public static class TestCallback implements OidcRequestCallback, OidcRefreshCall
private final Supplier pathSupplier;
public TestCallback() {
- this(60 * 60, null, new AtomicInteger(), null, null);
+ this(null, new AtomicInteger(), null, null);
}
public TestCallback(
- @Nullable final Integer expiresInSeconds,
@Nullable final Integer delayInMilliseconds,
@Nullable final AtomicInteger concurrentTracker,
@Nullable final TestListener testListener,
@Nullable final Supplier pathSupplier) {
- this.expiresInSeconds = expiresInSeconds;
this.delayInMilliseconds = delayInMilliseconds;
this.concurrentTracker = concurrentTracker;
this.testListener = testListener;
@@ -828,26 +390,15 @@ public int getInvocations() {
}
@Override
- public IdpResponse onRequest(final OidcRequestContext context) {
+ public RequestCallbackResult onRequest(final OidcRequestContext context) {
if (testListener != null) {
testListener.add("onRequest invoked");
}
return callback();
}
- @Override
- public IdpResponse onRefresh(final OidcRefreshContext context) {
- if (context.getRefreshToken() == null) {
- throw new IllegalArgumentException("refreshToken was null");
- }
- if (testListener != null) {
- testListener.add("onRefresh invoked");
- }
- return callback();
- }
-
@NotNull
- private IdpResponse callback() {
+ private RequestCallbackResult callback() {
if (concurrentTracker != null) {
if (concurrentTracker.get() > 0) {
throw new RuntimeException("Callbacks should not be invoked by multiple threads.");
@@ -857,7 +408,7 @@ private IdpResponse callback() {
try {
invocations.incrementAndGet();
Path path = Paths.get(pathSupplier == null
- ? getenv(AWS_WEB_IDENTITY_TOKEN_FILE)
+ ? getenv(OidcAuthenticator.AWS_WEB_IDENTITY_TOKEN_FILE)
: pathSupplier.get());
String accessToken;
try {
@@ -866,14 +417,10 @@ private IdpResponse callback() {
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
- String refreshToken = "refreshToken";
if (testListener != null) {
testListener.add("read access token: " + path.getFileName());
}
- return new IdpResponse(
- accessToken,
- expiresInSeconds,
- refreshToken);
+ return new RequestCallbackResult(accessToken);
} finally {
if (concurrentTracker != null) {
concurrentTracker.decrementAndGet();
@@ -887,18 +434,8 @@ private void simulateDelay() throws InterruptedException {
}
}
- public TestCallback setExpiresInSeconds(final Integer expiresInSeconds) {
- return new TestCallback(
- expiresInSeconds,
- this.delayInMilliseconds,
- this.concurrentTracker,
- this.testListener,
- this.pathSupplier);
- }
-
public TestCallback setDelayMs(final int milliseconds) {
return new TestCallback(
- this.expiresInSeconds,
milliseconds,
this.concurrentTracker,
this.testListener,
@@ -907,7 +444,6 @@ public TestCallback setDelayMs(final int milliseconds) {
public TestCallback setConcurrentTracker(final AtomicInteger c) {
return new TestCallback(
- this.expiresInSeconds,
this.delayInMilliseconds,
c,
this.testListener,
@@ -916,7 +452,6 @@ public TestCallback setConcurrentTracker(final AtomicInteger c) {
public TestCallback setEventListener(final TestListener testListener) {
return new TestCallback(
- this.expiresInSeconds,
this.delayInMilliseconds,
this.concurrentTracker,
testListener,
@@ -925,16 +460,11 @@ public TestCallback setEventListener(final TestListener testListener) {
public TestCallback setPathSupplier(final Supplier pathSupplier) {
return new TestCallback(
- this.expiresInSeconds,
this.delayInMilliseconds,
this.concurrentTracker,
this.testListener,
pathSupplier);
}
-
- public TestCallback setExpired() {
- return this.setExpiresInSeconds(60);
- }
}
public TestCallback createCallback() {