From 16d7a3977c4fe7bd975dedc2d8dd847e47e66b2c Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Fri, 10 Nov 2023 15:51:53 -0700 Subject: [PATCH 1/6] Remove non-machine workflow --- .../src/main/com/mongodb/MongoCredential.java | 132 +-------- .../connection/OidcAuthenticator.java | 251 ++---------------- .../auth/legacy/connection-string.json | 30 --- .../com/mongodb/AuthConnectionStringTest.java | 9 - .../OidcAuthenticationAsyncProseTests.java | 6 +- .../OidcAuthenticationProseTests.java | 187 +++---------- 6 files changed, 55 insertions(+), 560 deletions(-) diff --git a/driver-core/src/main/com/mongodb/MongoCredential.java b/driver-core/src/main/com/mongodb/MongoCredential.java index 418863dc21c..cf876206d4c 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#REQUEST_TOKEN_CALLBACK_KEY} * must not be provided. * * @see #createOidcCredential(String) @@ -210,44 +208,6 @@ public final class MongoCredential { */ 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")); - /** * Creates a MongoCredential instance with an unspecified mechanism. The client will negotiate the best mechanism based on the * version of the server that the client is authenticating to. @@ -405,8 +365,6 @@ public static MongoCredential createAwsCredential(@Nullable final String userNam * @see #withMechanismProperty(String, Object) * @see #PROVIDER_NAME_KEY * @see #REQUEST_TOKEN_CALLBACK_KEY - * @see #REFRESH_TOKEN_CALLBACK_KEY - * @see #ALLOWED_HOSTS_KEY * @mongodb.server.release 7.0 */ public static MongoCredential createOidcCredential(@Nullable final String userName) { @@ -639,10 +597,6 @@ 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. @@ -650,17 +604,6 @@ public interface OidcRequestContext { 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. - */ - String getRefreshToken(); - } - /** * This callback is invoked when the OIDC-based authenticator requests * tokens from the identity provider. @@ -673,72 +616,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 +640,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/OidcAuthenticator.java b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java index 2d3387e9216..d610042fcb0 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,13 +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; @@ -46,23 +43,15 @@ 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.assertions.Assertions.assertFalse; import static com.mongodb.assertions.Assertions.assertNotNull; @@ -82,9 +71,6 @@ public final class OidcAuthenticator extends SaslAuthenticator { private static final String AWS_WEB_IDENTITY_TOKEN_FILE = "AWS_WEB_IDENTITY_TOKEN_FILE"; - @Nullable - private ServerAddress serverAddress; - @Nullable private String connectionLastAccessToken; @@ -113,7 +99,6 @@ public String getMechanismName() { @Override protected SaslClient createSaslClient(final ServerAddress serverAddress) { - this.serverAddress = serverAddress; MongoCredentialWithCache mongoCredentialWithCache = getMongoCredentialWithCache(); return new OidcSaslClient(mongoCredentialWithCache); } @@ -126,12 +111,8 @@ public BsonDocument createSpeculativeAuthenticateCommand(final InternalConnectio return wrapInSpeculative(prepareAwsTokenFromFileAsJwt()); } String cachedAccessToken = getValidCachedAccessToken(); - MongoCredentialWithCache mongoCredentialWithCache = getMongoCredentialWithCache(); 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; @@ -166,13 +147,6 @@ 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() @@ -306,8 +280,6 @@ private byte[] evaluate(final byte[] challenge) { OidcCacheEntry cacheEntry = mongoCredentialWithCache.getOidcCacheEntry(); String cachedAccessToken = getValidCachedAccessToken(); String invalidConnectionAccessToken = connectionLastAccessToken; - String cachedRefreshToken = cacheEntry.getRefreshToken(); - IdpInfo cachedIdpInfo = cacheEntry.getIdpInfo(); if (cachedAccessToken != null) { boolean cachedTokenIsInvalid = cachedAccessToken.equals(invalidConnectionAccessToken); @@ -316,44 +288,14 @@ 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); - } + fallbackState = FallbackState.PHASE_2_REQUEST_CALLBACK_TOKEN; + RequestCallbackResult result = requestCallback.onRequest(new OidcRequestContextImpl(CALLBACK_TIMEOUT)); + return populateCacheWithCallbackResultAndPrepareJwt(result); } } @@ -362,7 +304,7 @@ private boolean isAutomaticAuthentication() { } private boolean clientIsComplete() { - return fallbackState != FallbackState.PHASE_3A_PRINCIPAL; + return true; // all possibilities are 1-step } private boolean shouldRetryHandler() { @@ -372,16 +314,10 @@ private boolean shouldRetryHandler() { // 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()); + .clearAccessToken()); return false; } return true; @@ -397,89 +333,33 @@ private String getValidCachedAccessToken() { 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; - } 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); } } @@ -524,45 +404,13 @@ private static byte[] prepareUsername(@Nullable final String 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); - } + return prepareTokenAsJwt(requestCallbackResult.getAccessToken()); } @Nullable @@ -626,7 +474,6 @@ 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); if (providerName == null) { // callback if (requestCallback == null) { @@ -641,9 +488,6 @@ public static void validateBeforeUse(final MongoCredential credential) { 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"); - } } } } @@ -651,81 +495,24 @@ 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; - } - } - /** * Represents what was sent in the last request to the MongoDB server. */ private enum FallbackState { INITIAL, PHASE_1_CACHED_TOKEN, - PHASE_2_REFRESH_CALLBACK_TOKEN, - PHASE_3A_PRINCIPAL, - PHASE_3B_REQUEST_CALLBACK_TOKEN + PHASE_2_REQUEST_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..4089531860e 100644 --- a/driver-core/src/test/resources/auth/legacy/connection-string.json +++ b/driver-core/src/test/resources/auth/legacy/connection-string.json @@ -475,22 +475,6 @@ } } }, - { - "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", @@ -559,13 +543,6 @@ "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", @@ -573,13 +550,6 @@ "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", diff --git a/driver-core/src/test/unit/com/mongodb/AuthConnectionStringTest.java b/driver-core/src/test/unit/com/mongodb/AuthConnectionStringTest.java index 7f4acab857d..b80fc12ddbf 100644 --- a/driver-core/src/test/unit/com/mongodb/AuthConnectionStringTest.java +++ b/driver-core/src/test/unit/com/mongodb/AuthConnectionStringTest.java @@ -37,7 +37,6 @@ 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; // See https://github.com/mongodb/specifications/tree/master/source/auth/legacy/tests @@ -121,10 +120,6 @@ private MongoCredential getMongoCredential() { credential = credential.withMechanismProperty( REQUEST_TOKEN_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); } @@ -184,10 +179,6 @@ private void assertMechanismProperties(final MongoCredential credential) { 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..7d2cd2dbb3e 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(OIDC_URL, 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/internal/connection/OidcAuthenticationProseTests.java b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java index 368e1342e1f..e4ce440d2af 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 @@ -21,8 +21,7 @@ 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.MongoCredential.RequestCallbackResult; import com.mongodb.MongoSecurityException; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; @@ -42,7 +41,6 @@ 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.nio.charset.StandardCharsets; @@ -60,13 +58,9 @@ 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; @@ -158,7 +152,7 @@ public void test1CallbackDrivenAuth(final String name, final String file, final 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); + MongoClientSettings settings = createSettings(url, createCallback(), null); // #. Assert that a find operation fails with a client-side error. performFind(settings, MongoSecurityException.class, ""); } @@ -171,14 +165,12 @@ public void test1p7LockAvoidsExtraCallbackCalls() { // 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); + TestCallback onRequest = createCallback().setConcurrentTracker(concurrent); + MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest); try (MongoClient mongoClient = createMongoClient(clientSettings)) { delayNextFind(); // cause both callbacks to be called executeAll(2, () -> performFind(mongoClient)); assertEquals(1, onRequest.getInvocations()); - assertEquals(1, onRefresh.getInvocations()); } } @@ -187,14 +179,14 @@ public void proveThatConcurrentCallbacksThrow() { 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))); + // TODO-OIDC previously onRefresh, confirm that this update is correct + refresh.onRequest(new OidcAuthenticator.OidcRequestContextImpl(Duration.ofSeconds(1234))); }); }, () -> { - request.onRequest(new OidcAuthenticator.OidcRequestContextImpl(serverInfo, Duration.ofSeconds(1234))); + request.onRequest(new OidcAuthenticator.OidcRequestContextImpl(Duration.ofSeconds(1234))); }); } @@ -230,45 +222,30 @@ public void test2AwsAutomaticAuth(final String name, final String file, final St @Test public void test2p4AllowedHostsIgnored() { MongoClientSettings settings = createSettings( - AWS_OIDC_URL, null, null, Arrays.asList(), null); + AWS_OIDC_URL, null, null); performFind(settings); } @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"; 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 assertEquals(1, onRequest.getInvocations()); - assertEquals(1, onRefresh.getInvocations()); } } @@ -280,27 +257,6 @@ public void test3p2RequestCallbackReturnsNull() { 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() { // #. Create a client with a request callback that returns data not @@ -308,7 +264,7 @@ public void test3p4RequestCallbackReturnsInvalidData() { // #. ... 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); @@ -322,36 +278,13 @@ 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); + TestCallback onRequest = createCallback(); + MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest); try (MongoClient mongoClient = createMongoClient(clientSettings)) { // #. Create a new client with the same request callback and a refresh callback. // Instead: @@ -361,7 +294,6 @@ public void test4p1CachedCredentialsCacheWithRefresh() { // #. 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 } @@ -373,7 +305,7 @@ public void test4p2CachedCredentialsCacheWithNoRefresh() { // #. 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(); + TestCallback onRequest = createCallback(); MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest, null); try (MongoClient mongoClient = createMongoClient(clientSettings)) { delayNextFind(); // cause both callbacks to be called @@ -399,16 +331,12 @@ public void test4p4ErrorClearsCache() { "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); + MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest, commandListener); try (MongoClient mongoClient = createMongoClient(clientSettings)) { // #. Ensure that a find operation adds a new entry to the cache. performFind(mongoClient); @@ -480,16 +408,12 @@ public void testEventListenerMustNotLogReauthentication() { "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); + MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest, commandListener); try (MongoClient mongoClient = createMongoClient(clientSettings)) { performFind(mongoClient); assertEquals(Arrays.asList( @@ -543,7 +467,7 @@ public void test5SpeculativeAuthentication() { TestListener listener = new TestListener(); TestCallback onRequest = createCallback().setEventListener(listener); TestCommandListener commandListener = new TestCommandListener(listener); - MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest, null, null, commandListener); + MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest, commandListener); try (MongoClient mongoClient = createMongoClient(clientSettings)) { // instead of setting failpoints for saslStart, we inspect events delayNextFind(); @@ -565,7 +489,7 @@ public void testAutomaticAuthUsesSpeculative() { TestCommandListener commandListener = new TestCommandListener(listener); MongoClientSettings settings = createSettings( - AWS_OIDC_URL, null, null, Arrays.asList(), commandListener); + AWS_OIDC_URL, null, commandListener); try (MongoClient mongoClient = createMongoClient(settings)) { // we use a listener instead of a failpoint performFind(mongoClient); @@ -580,23 +504,19 @@ public void testAutomaticAuthUsesSpeculative() { @Test public void test6p1ReauthenticationSucceeds() { - // #. Create request and refresh callbacks that return valid credentials that will not expire soon. + // #. Create request callback that returns 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); + MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest, commandListener); 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", @@ -637,9 +557,6 @@ public void test6p1ReauthenticationSucceeds() { "find started", "find succeeded" ), listener.getEventStrings()); - - // #. Assert that the refresh callback has been called once, if possible. - assertEquals(1, onRefresh.getInvocations()); } } @@ -655,8 +572,7 @@ private ConcurrentLinkedQueue tokenQueue(final String... queue) { public void test6p2ReauthenticationRetriesAndSucceedsWithCache() { // #. Create request and refresh callbacks that return valid credentials that will not expire soon. TestCallback onRequest = createCallback(); - TestCallback onRefresh = createCallback(); - MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest, onRefresh); + MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest); try (MongoClient mongoClient = createMongoClient(clientSettings)) { // #. Perform a find operation that succeeds. performFind(mongoClient); @@ -676,43 +592,35 @@ public void test6p4SeparateConnectionsAvoidExtraCallbackCalls() { "test_user1", "test_user1_1"); TestCallback onRequest = createCallback().setPathSupplier(() -> tokens.remove()); - TestCallback onRefresh = createCallback().setPathSupplier(() -> tokens.remove()); - MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest, onRefresh); + MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest); 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()); } } 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(REQUEST_TOKEN_CALLBACK_KEY, onRequest); MongoClientSettings.Builder builder = MongoClientSettings.builder() .applicationName(appName) .applyConnectionString(cs) @@ -793,11 +701,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 +713,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 +732,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."); @@ -866,14 +759,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 +776,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 +786,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 +794,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 +802,12 @@ 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() { From 929add5138e7e637ad8bcd56f7b1ff7b2267fe53 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Mon, 20 Nov 2023 14:06:59 -0700 Subject: [PATCH 2/6] Update prose tests to remove refresh token, principal-request --- .../OidcAuthenticationAsyncProseTests.java | 2 +- .../OidcAuthenticationProseTests.java | 226 ++++-------------- 2 files changed, 49 insertions(+), 179 deletions(-) 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 7d2cd2dbb3e..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 @@ -44,7 +44,7 @@ public void testNonblockingCallbacks() { int simulatedDelayMs = 100; TestCallback requestCallback = createCallback().setDelayMs(simulatedDelayMs); - MongoClientSettings clientSettings = createSettings(OIDC_URL, requestCallback); + MongoClientSettings clientSettings = createSettings(getOidcUri(), requestCallback); try (com.mongodb.reactivestreams.client.MongoClient client = MongoClients.create(clientSettings)) { executeAll(2, () -> { 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 e4ce440d2af..5ea10e7e4ed 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 @@ -18,7 +18,6 @@ import com.mongodb.ConnectionString; import com.mongodb.MongoClientSettings; -import com.mongodb.MongoCommandException; import com.mongodb.MongoConfigurationException; import com.mongodb.MongoCredential; import com.mongodb.MongoCredential.RequestCallbackResult; @@ -38,8 +37,6 @@ 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 java.io.IOException; @@ -67,9 +64,7 @@ import static java.lang.System.getenv; import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.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; @@ -87,13 +82,20 @@ public static boolean oidcTestsEnabled() { private static final String AWS_WEB_IDENTITY_TOKEN_FILE = "AWS_WEB_IDENTITY_TOKEN_FILE"; - public static final String TOKEN_DIRECTORY = "/tmp/tokens/"; // TODO-OIDC + public static final String TOKEN_DIRECTORY = "/tmp/tokens/"; - 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; + protected static String getOidcUri() { + ConnectionString cs = new ConnectionString(getenv("MONGODB_URI")); + // remove username and password + return "mongodb+srv://" + cs.getHosts().get(0) + "/?authMechanism=MONGODB-OIDC"; + } + + private static String getAwsOidcUri() { + return getOidcUri() + "&authMechanismProperties=PROVIDER_NAME:aws"; + } + protected MongoClient createMongoClient(final MongoClientSettings settings) { return MongoClients.create(settings); } @@ -116,45 +118,14 @@ 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); + @Test + public void test1p1CallbackDrivenAuth() { // #. 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); - // #. Assert that a find operation fails with a client-side error. - performFind(settings, MongoSecurityException.class, ""); + MongoClientSettings clientSettings = createSettings(getOidcUri(), onRequest, null); + // #. Perform a find operation that succeeds + performFind(clientSettings); } @Test @@ -166,7 +137,7 @@ public void test1p7LockAvoidsExtraCallbackCalls() { // After blocking, this ensuing refresh thread will enter onRefresh. AtomicInteger concurrent = new AtomicInteger(); TestCallback onRequest = createCallback().setConcurrentTracker(concurrent); - MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest); + MongoClientSettings clientSettings = createSettings(getOidcUri(), onRequest); try (MongoClient mongoClient = createMongoClient(clientSettings)) { delayNextFind(); // cause both callbacks to be called executeAll(2, () -> performFind(mongoClient)); @@ -182,7 +153,6 @@ public void proveThatConcurrentCallbacksThrow() { executeAll(() -> { sleep(2); assertThrows(RuntimeException.class, () -> { - // TODO-OIDC previously onRefresh, confirm that this update is correct refresh.onRequest(new OidcAuthenticator.OidcRequestContextImpl(Duration.ofSeconds(1234))); }); }, () -> { @@ -198,22 +168,17 @@ private void sleep(final long ms) { } } - @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); + @Test + public void test2AwsAutomaticAuth() { + String uri = getAwsOidcUri(); + // #. 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)) + .applyConnectionString(new ConnectionString(uri)) .build(); // #. Perform a find operation that succeeds. performFind(clientSettings); @@ -222,13 +187,13 @@ public void test2AwsAutomaticAuth(final String name, final String file, final St @Test public void test2p4AllowedHostsIgnored() { MongoClientSettings settings = createSettings( - AWS_OIDC_URL, null, null); + getAwsOidcUri(), null, null); performFind(settings); } @Test public void test3p1ValidCallbacks() { - String connectionString = "mongodb://test_user1@localhost/?authMechanism=MONGODB-OIDC"; + String connectionString = getOidcUri(); Duration expectedSeconds = Duration.ofMinutes(5); TestCallback onRequest = createCallback(); @@ -253,7 +218,7 @@ public void test3p1ValidCallbacks() { public void test3p2RequestCallbackReturnsNull() { //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"); } @@ -267,7 +232,7 @@ public void test3p4RequestCallbackReturnsInvalidData() { 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); @@ -284,7 +249,7 @@ public void test3p4RequestCallbackReturnsInvalidData() { public void test4p1CachedCredentialsCacheWithRefresh() { // #. Create a new client with a request callback that gives credentials that expire in one minute. TestCallback onRequest = createCallback(); - MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest); + MongoClientSettings clientSettings = createSettings(getOidcUri(), onRequest); try (MongoClient mongoClient = createMongoClient(clientSettings)) { // #. Create a new client with the same request callback and a refresh callback. // Instead: @@ -299,25 +264,6 @@ public void test4p1CachedCredentialsCacheWithRefresh() { } } - @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(); - 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 @@ -328,7 +274,6 @@ public void test4p4ErrorClearsCache() { ConcurrentLinkedQueue tokens = tokenQueue( "test_user1", "test_user1_expires", - "test_user1_expires", "test_user1_1"); TestCallback onRequest = createCallback() .setPathSupplier(() -> tokens.remove()) @@ -336,7 +281,7 @@ public void test4p4ErrorClearsCache() { TestCommandListener commandListener = new TestCommandListener(listener); - MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest, commandListener); + MongoClientSettings clientSettings = createSettings(getOidcUri(), onRequest, commandListener); try (MongoClient mongoClient = createMongoClient(clientSettings)) { // #. Ensure that a find operation adds a new entry to the cache. performFind(mongoClient); @@ -345,8 +290,8 @@ public void test4p4ErrorClearsCache() { "isMaster succeeded", "onRequest invoked", "read access token: test_user1", - "saslContinue started", - "saslContinue succeeded", + "saslStart started", + "saslStart succeeded", "find started", "find succeeded" ), listener.getEventStrings()); @@ -359,17 +304,10 @@ public void test4p4ErrorClearsCache() { 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" + "saslStart started", + "saslStart failed" ), listener.getEventStrings()); listener.clear(); @@ -379,15 +317,10 @@ public void test4p4ErrorClearsCache() { 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", + "saslStart started", + "saslStart succeeded", // auth has finished "find started", "find succeeded" @@ -396,48 +329,6 @@ public void test4p4ErrorClearsCache() { } } - // 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() - .setPathSupplier(() -> tokens.remove()) - .setEventListener(listener); - - TestCommandListener commandListener = new TestCommandListener(listener); - - MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest, 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()); - } - } - @Test public void test4p5AwsAutomaticWorkflowDoesNotUseCache() { // #. Create a new client that uses the AWS automatic workflow. @@ -445,7 +336,7 @@ public void test4p5AwsAutomaticWorkflowDoesNotUseCache() { setOidcFile("test_user1"); MongoCredential credential = createOidcCredential(null) .withMechanismProperty(PROVIDER_NAME_KEY, "aws"); - ConnectionString connectionString = new ConnectionString(AWS_OIDC_URL); + ConnectionString connectionString = new ConnectionString(getAwsOidcUri()); MongoClientSettings clientSettings = MongoClientSettings.builder() .applicationName(appName) .credential(credential) @@ -460,28 +351,6 @@ public void test4p5AwsAutomaticWorkflowDoesNotUseCache() { } } - @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, commandListener); - try (MongoClient mongoClient = createMongoClient(clientSettings)) { - // instead of setting failpoints for saslStart, we inspect events - delayNextFind(); - 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() { @@ -489,7 +358,7 @@ public void testAutomaticAuthUsesSpeculative() { TestCommandListener commandListener = new TestCommandListener(listener); MongoClientSettings settings = createSettings( - AWS_OIDC_URL, null, commandListener); + getAwsOidcUri(), null, commandListener); try (MongoClient mongoClient = createMongoClient(settings)) { // we use a listener instead of a failpoint performFind(mongoClient); @@ -511,7 +380,7 @@ public void test6p1ReauthenticationSucceeds() { // #. 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, commandListener); + MongoClientSettings clientSettings = createSettings(getOidcUri(), onRequest, commandListener); try (MongoClient mongoClient = createMongoClient(clientSettings)) { // #. Perform a find operation that succeeds. @@ -525,8 +394,8 @@ public void test6p1ReauthenticationSucceeds() { "onRequest invoked", "read access token: test_user1", // jwt from onRequest: - "saslContinue started", - "saslContinue succeeded", + "saslStart started", + "saslStart succeeded", // ensuing find: "find started", "find succeeded" @@ -548,8 +417,8 @@ public void test6p1ReauthenticationSucceeds() { assertEquals(Arrays.asList( "find started", "find failed", - // find has triggered 391, and cleared the access token; fall back to refresh: - "onRefresh invoked", + // find has triggered 391, and cleared the access token; fall back to callback: + "onRequest invoked", "read access token: test_user1", "saslStart started", "saslStart succeeded", @@ -572,7 +441,7 @@ private ConcurrentLinkedQueue tokenQueue(final String... queue) { public void test6p2ReauthenticationRetriesAndSucceedsWithCache() { // #. Create request and refresh callbacks that return valid credentials that will not expire soon. TestCallback onRequest = createCallback(); - MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest); + MongoClientSettings clientSettings = createSettings(getOidcUri(), onRequest); try (MongoClient mongoClient = createMongoClient(clientSettings)) { // #. Perform a find operation that succeeds. performFind(mongoClient); @@ -592,7 +461,7 @@ public void test6p4SeparateConnectionsAvoidExtraCallbackCalls() { "test_user1", "test_user1_1"); TestCallback onRequest = createCallback().setPathSupplier(() -> tokens.remove()); - MongoClientSettings clientSettings = createSettings(OIDC_URL, onRequest); + MongoClientSettings clientSettings = createSettings(getOidcUri(), onRequest); try (MongoClient mongoClient = createMongoClient(clientSettings)) { // #. Peform a find operation on each ... that succeeds. delayNextFind(); @@ -603,8 +472,9 @@ public void test6p4SeparateConnectionsAvoidExtraCallbackCalls() { 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()); + // #. Ensure that the callback + // has been called twice: + assertEquals(2, onRequest.getInvocations()); } } @@ -675,7 +545,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() @@ -689,7 +559,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))) From e21e5e503e338bd2b124ed742bccf0210d3e51b6 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Tue, 13 Feb 2024 10:41:14 -0700 Subject: [PATCH 3/6] Conform to latest spec; remove lock around server auth --- .../src/main/com/mongodb/MongoCredential.java | 11 +- .../connection/OidcAuthenticator.java | 195 +++----- .../auth/legacy/connection-string.json | 73 +-- .../auth/mongodb-oidc-no-retry.json | 428 ++++++++++++++++++ .../auth/reauthenticate_with_retry.json | 191 -------- .../auth/reauthenticate_without_retry.json | 191 -------- .../com/mongodb/AuthConnectionStringTest.java | 6 +- .../com/mongodb/client/unified/Entities.java | 31 ++ .../mongodb/client/unified/ErrorMatcher.java | 16 +- .../unified/RunOnRequirementsMatcher.java | 9 + .../mongodb/client/unified/UnifiedTest.java | 4 +- .../OidcAuthenticationProseTests.java | 387 ++++------------ 12 files changed, 653 insertions(+), 889 deletions(-) create mode 100644 driver-core/src/test/resources/unified-test-format/auth/mongodb-oidc-no-retry.json delete mode 100644 driver-core/src/test/resources/unified-test-format/auth/reauthenticate_with_retry.json delete mode 100644 driver-core/src/test/resources/unified-test-format/auth/reauthenticate_without_retry.json diff --git a/driver-core/src/main/com/mongodb/MongoCredential.java b/driver-core/src/main/com/mongodb/MongoCredential.java index cf876206d4c..e56fbab4eac 100644 --- a/driver-core/src/main/com/mongodb/MongoCredential.java +++ b/driver-core/src/main/com/mongodb/MongoCredential.java @@ -187,7 +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} + * {@link MongoCredential#OIDC_CALLBACK_KEY} * must not be provided. * * @see #createOidcCredential(String) @@ -206,7 +206,7 @@ public final class MongoCredential { * @see #createOidcCredential(String) * @since 4.10 */ - public static final String REQUEST_TOKEN_CALLBACK_KEY = "REQUEST_TOKEN_CALLBACK"; + 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 @@ -364,7 +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 #OIDC_CALLBACK_KEY * @mongodb.server.release 7.0 */ public static MongoCredential createOidcCredential(@Nullable final String userName) { @@ -602,6 +602,11 @@ public interface OidcRequestContext { * @return The timeout that this callback must complete within. */ Duration getTimeout(); + + /** + * @return The OIDC callback version. + */ + int getVersion(); } /** 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 d610042fcb0..6d83b83e9e9 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java +++ b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java @@ -34,7 +34,6 @@ import com.mongodb.lang.Nullable; import org.bson.BsonDocument; import org.bson.BsonString; -import org.jetbrains.annotations.NotNull; import javax.security.sasl.SaslClient; import java.io.IOException; @@ -45,14 +44,12 @@ import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; import static com.mongodb.AuthenticationMechanism.MONGODB_OIDC; 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.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; @@ -70,6 +67,7 @@ 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"; + private static final int CALLBACK_API_VERSION_NUMBER = 1; @Nullable private String connectionLastAccessToken; @@ -79,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); @@ -110,7 +105,7 @@ public BsonDocument createSpeculativeAuthenticateCommand(final InternalConnectio if (isAutomaticAuthentication()) { return wrapInSpeculative(prepareAwsTokenFromFileAsJwt()); } - String cachedAccessToken = getValidCachedAccessToken(); + String cachedAccessToken = getCachedAccessToken(); if (cachedAccessToken != null) { return wrapInSpeculative(prepareTokenAsJwt(cachedAccessToken)); } else { @@ -122,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())); @@ -151,32 +145,27 @@ public void setSpeculativeAuthenticateResponse(@Nullable final BsonDocument resp 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 @@ -184,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); } @@ -228,57 +189,57 @@ 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(); + + 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]; + } - OidcRequestCallback requestCallback = assertNotNull(getRequestCallback()); + @Nullable + private String validatedCachedAccessToken() { MongoCredentialWithCache mongoCredentialWithCache = getMongoCredentialWithCache(); OidcCacheEntry cacheEntry = mongoCredentialWithCache.getOidcCacheEntry(); - String cachedAccessToken = getValidCachedAccessToken(); + String cachedAccessToken = getCachedAccessToken(); String invalidConnectionAccessToken = connectionLastAccessToken; if (cachedAccessToken != null) { @@ -288,15 +249,7 @@ private byte[] evaluate(final byte[] challenge) { cachedAccessToken = null; } } - if (cachedAccessToken != null) { - fallbackState = FallbackState.PHASE_1_CACHED_TOKEN; - return prepareTokenAsJwt(cachedAccessToken); - } else { - // cache is empty - fallbackState = FallbackState.PHASE_2_REQUEST_CALLBACK_TOKEN; - RequestCallbackResult result = requestCallback.onRequest(new OidcRequestContextImpl(CALLBACK_TIMEOUT)); - return populateCacheWithCallbackResultAndPrepareJwt(result); - } + return cachedAccessToken; } private boolean isAutomaticAuthentication() { @@ -308,26 +261,17 @@ private boolean clientIsComplete() { } 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 { - // a clean-restart failed - mongoCredentialWithCache.setOidcCacheEntry(cacheEntry - .clearAccessToken()); - 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 { @@ -354,7 +298,7 @@ private OidcCacheEntry(@Nullable final String accessToken) { } @Nullable - String getValidCachedAccessToken() { + String getCachedAccessToken() { return accessToken; } @@ -371,7 +315,7 @@ private OidcSaslClient(final MongoCredentialWithCache mongoCredentialWithCache) @Override public byte[] evaluateChallenge(final byte[] challenge) { - return assertNotNull(evaluateChallengeFunction).apply(challenge); + return evaluate(challenge); } @Override @@ -396,14 +340,6 @@ 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(@Nullable final RequestCallbackResult requestCallbackResult) { if (requestCallbackResult == null) { throw new MongoConfigurationException("Result of callback must not be null"); @@ -413,18 +349,6 @@ private byte[] populateCacheWithCallbackResultAndPrepareJwt(@Nullable final Requ return prepareTokenAsJwt(requestCallbackResult.getAccessToken()); } - @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()); - } - private byte[] prepareTokenAsJwt(final String accessToken) { connectionLastAccessToken = accessToken; return toJwtDocument(accessToken); @@ -473,12 +397,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 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 @@ -486,7 +410,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"); + throw new IllegalArgumentException(OIDC_CALLBACK_KEY + " must not be specified when " + PROVIDER_NAME_KEY + " is specified"); } } } @@ -505,14 +429,19 @@ static class OidcRequestContextImpl implements OidcRequestContext { public Duration getTimeout() { return timeout; } + + @Override + 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_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 4089531860e..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,52 +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 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": { @@ -505,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": { @@ -519,37 +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 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 when unsupported auth property is specified (MONGODB-OIDC)", "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=UnsupportedProperty:unexisted", @@ -557,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 b80fc12ddbf..4da83dc7d4f 100644 --- a/driver-core/src/test/unit/com/mongodb/AuthConnectionStringTest.java +++ b/driver-core/src/test/unit/com/mongodb/AuthConnectionStringTest.java @@ -37,7 +37,7 @@ import java.util.List; import static com.mongodb.AuthenticationMechanism.MONGODB_OIDC; -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) @@ -118,7 +118,7 @@ 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 { fail("Unsupported callback: " + string); @@ -175,7 +175,7 @@ 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; } 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..4e2fc0401aa 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java @@ -16,9 +16,11 @@ 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; @@ -70,6 +72,11 @@ 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 +102,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 +507,29 @@ 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; + case "authMechanismProperties": + MongoCredential credential = clientSettingsBuilder.build().getCredential(); + if (credential != null && credential.getAuthenticationMechanism() == AuthenticationMechanism.MONGODB_OIDC) { + MongoCredential c = credential; + clientSettingsBuilder.credential(c.withMechanismProperty( + MongoCredential.OIDC_CALLBACK_KEY, + (MongoCredential.OidcRequestCallback) context -> { + Path path = Paths.get(getenv("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; 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..32d698e9028 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")) { + 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 MongoQueryException when checking" - + " for error codes"), - e instanceof MongoCommandException || e instanceof MongoWriteException); - int errorCode = (e instanceof MongoCommandException) - ? ((MongoCommandException) e).getErrorCode() - : ((MongoWriteException) e).getCode(); + + " 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..e00df2a1d55 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 containsOIDC = getServerParameters() + .getArray("authenticationMechanisms") + .contains(curRequirement.getValue()); + if (!containsOIDC) { + 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 5ea10e7e4ed..70d8b05bcb7 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,12 +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.RequestCallbackResult; -import com.mongodb.MongoSecurityException; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; import com.mongodb.client.TestListener; @@ -40,27 +41,25 @@ import org.opentest4j.AssertionFailedError; 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.OidcRequestCallback; import static com.mongodb.MongoCredential.OidcRequestContext; -import static com.mongodb.MongoCredential.PROVIDER_NAME_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; @@ -69,7 +68,6 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue; import static util.ThreadTestHelpers.executeAll; - /** * See * Prose Tests. @@ -82,12 +80,10 @@ public static boolean oidcTestsEnabled() { private static final String AWS_WEB_IDENTITY_TOKEN_FILE = "AWS_WEB_IDENTITY_TOKEN_FILE"; - public static final String TOKEN_DIRECTORY = "/tmp/tokens/"; - private String appName; protected static String getOidcUri() { - ConnectionString cs = new ConnectionString(getenv("MONGODB_URI")); + ConnectionString cs = ClusterFixture.getConnectionString(); // remove username and password return "mongodb+srv://" + cs.getHosts().get(0) + "/?authMechanism=MONGODB-OIDC"; } @@ -100,15 +96,9 @@ 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); } @@ -119,103 +109,60 @@ public void afterEach() { } @Test - public void test1p1CallbackDrivenAuth() { - // #. 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. + 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 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().setConcurrentTracker(concurrent); - MongoClientSettings clientSettings = createSettings(getOidcUri(), onRequest); + public void test1p2CallbackCalledOnceForMultipleConnections() { + TestCallback onRequest = createCallback(); + MongoClientSettings clientSettings = createSettings(getOidcUri(), onRequest, null); try (MongoClient mongoClient = createMongoClient(clientSettings)) { - delayNextFind(); // cause both callbacks to be called - executeAll(2, () -> performFind(mongoClient)); - assertEquals(1, onRequest.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); - executeAll(() -> { - sleep(2); - assertThrows(RuntimeException.class, () -> { - refresh.onRequest(new OidcAuthenticator.OidcRequestContextImpl(Duration.ofSeconds(1234))); - }); - }, () -> { - request.onRequest(new OidcAuthenticator.OidcRequestContextImpl(Duration.ofSeconds(1234))); - }); - } - - private void sleep(final long ms) { - try { - Thread.sleep(ms); - } catch (InterruptedException e) { - throw new RuntimeException(e); + 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 test2AwsAutomaticAuth() { - String uri = getAwsOidcUri(); - - // #. 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(uri)) - .build(); - // #. Perform a find operation that succeeds. - performFind(clientSettings); - } - - @Test - public void test2p4AllowedHostsIgnored() { - MongoClientSettings settings = createSettings( - getAwsOidcUri(), null, null); - performFind(settings); - } - - @Test - public void test3p1ValidCallbacks() { + public void test2p1ValidCallbackInputs() { String connectionString = getOidcUri(); Duration expectedSeconds = Duration.ofMinutes(5); 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(expectedSeconds, context.getTimeout()); return onRequest.onRequest(context); }; 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()); } } @Test - public void test3p2RequestCallbackReturnsNull() { + public void test2p2RequestCallbackReturnsNull() { //noinspection ConstantConditions OidcRequestCallback onRequest = (context) -> null; MongoClientSettings settings = this.createSettings(getOidcUri(), onRequest, null); @@ -223,10 +170,9 @@ public void test3p2RequestCallbackReturnsNull() { } @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 RequestCallbackResult(null); @@ -243,239 +189,82 @@ public void test3p4RequestCallbackReturnsInvalidData() { } } - // 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(); - MongoClientSettings clientSettings = createSettings(getOidcUri(), onRequest); - 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()); - // the refresh invocation will fail if the cached tokens are null - // so a success implies that credentials were present in the cache + 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); } } @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_1"); - TestCallback onRequest = createCallback() - .setPathSupplier(() -> tokens.remove()) - .setEventListener(listener); - - TestCommandListener commandListener = new TestCommandListener(listener); - - MongoClientSettings clientSettings = createSettings(getOidcUri(), onRequest, 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", - "saslStart started", - "saslStart 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", - "onRequest invoked", - "read access token: test_user1_expires", - "saslStart started", - "saslStart failed" - ), listener.getEventStrings()); - listener.clear(); - - // #. Ensure that the cache value cleared. - failCommand(391, 1, "find"); - performFind(mongoClient); - assertEquals(Arrays.asList( - "find started", - "find failed", - "onRequest invoked", - "read access token: test_user1_1", - "saslStart started", - "saslStart succeeded", - // auth has finished - "find started", - "find succeeded" - ), listener.getEventStrings()); - listener.clear(); - } - } + 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(getAwsOidcUri()); - 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)); - } - } - - // Not a prose test - @Test - public void testAutomaticAuthUsesSpeculative() { - TestListener listener = new TestListener(); - TestCommandListener commandListener = new TestCommandListener(listener); - - MongoClientSettings settings = createSettings( - getAwsOidcUri(), null, 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(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)); } + assertEquals(2, onRequestWrapped.invocations.get()); } @Test - public void test6p1ReauthenticationSucceeds() { - // #. Create request callback that returns valid credentials that will not expire soon. - TestListener listener = new TestListener(); - TestCallback onRequest = 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(getOidcUri(), onRequest, 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); - - assertEquals(Arrays.asList( - // speculative: - "isMaster started", - "isMaster succeeded", - // onRequest: - "onRequest invoked", - "read access token: test_user1", - // jwt from onRequest: - "saslStart started", - "saslStart 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 callback: - "onRequest invoked", - "read access token: test_user1", - "saslStart started", - "saslStart succeeded", - // find retry succeeds: - "find started", - "find succeeded" - ), listener.getEventStrings()); + 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(); 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()); - MongoClientSettings clientSettings = createSettings(getOidcUri(), onRequest); - 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()); - - failCommand(391, 2, "find"); - executeAll(2, () -> performFind(mongoClient)); - - // #. Ensure that the callback - // has been called twice: - assertEquals(2, onRequest.getInvocations()); - } + assertEquals(2, onRequest.invocations.get()); } public MongoClientSettings createSettings( @@ -490,10 +279,11 @@ private MongoClientSettings createSettings( @Nullable final CommandListener commandListener) { ConnectionString cs = new ConnectionString(connectionString); MongoCredential credential = cs.getCredential() - .withMechanismProperty(REQUEST_TOKEN_CALLBACK_KEY, onRequest); + .withMechanismProperty(OIDC_CALLBACK_KEY, onRequest); MongoClientSettings.Builder builder = MongoClientSettings.builder() .applicationName(appName) .applyConnectionString(cs) + .retryReads(false) .credential(credential); if (commandListener != null) { builder.addCommandListener(commandListener); @@ -677,7 +467,6 @@ public TestCallback setPathSupplier(final Supplier pathSupplier) { this.testListener, pathSupplier); } - } public TestCallback createCallback() { From eedd983f098633a347ff656c5b54a463b69767b2 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Tue, 13 Feb 2024 13:40:04 -0700 Subject: [PATCH 4/6] Rebase fix (async API) --- .../mongodb/internal/connection/InternalStreamConnection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); } From de9b2469716ee2ede373176fcb6c00a2d6f92775 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Fri, 16 Feb 2024 12:42:10 -0700 Subject: [PATCH 5/6] Apply suggestions from code review Co-authored-by: Valentin Kovalenko --- driver-core/src/main/com/mongodb/MongoCredential.java | 2 +- .../com/mongodb/internal/connection/OidcAuthenticator.java | 3 +++ .../functional/com/mongodb/client/unified/ErrorMatcher.java | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/driver-core/src/main/com/mongodb/MongoCredential.java b/driver-core/src/main/com/mongodb/MongoCredential.java index e56fbab4eac..b7035a27f7b 100644 --- a/driver-core/src/main/com/mongodb/MongoCredential.java +++ b/driver-core/src/main/com/mongodb/MongoCredential.java @@ -604,7 +604,7 @@ public interface OidcRequestContext { Duration getTimeout(); /** - * @return The OIDC callback version. + * @return The OIDC callback API version. */ int getVersion(); } 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 6d83b83e9e9..12f1ad41254 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java +++ b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java @@ -235,6 +235,9 @@ private byte[] evaluate(final byte[] challenge) { return jwt[0]; } + /** + * Must be guarded by {@link MongoCredentialWithCache#getOidcLock()}. + */ @Nullable private String validatedCachedAccessToken() { MongoCredentialWithCache mongoCredentialWithCache = getMongoCredentialWithCache(); 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 32d698e9028..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 @@ -77,7 +77,7 @@ void assertErrorsMatch(final BsonDocument expectedError, final Exception e) { if (e instanceof MongoSecurityException && e.getCause() instanceof MongoCommandException) { errorCodeException = (Exception) e.getCause(); } - assertTrue(context.getMessage("Exception must be of type MongoCommandException or MongoQueryException when checking" + 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); From f54fa7adefd77c42b787c987c2be7fb4b3488386 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Fri, 16 Feb 2024 14:07:11 -0700 Subject: [PATCH 6/6] PR fixes --- .../src/main/com/mongodb/MongoCredential.java | 2 +- .../internal/connection/OidcAuthenticator.java | 2 +- .../com/mongodb/client/unified/Entities.java | 18 ++++++++++++------ .../unified/RunOnRequirementsMatcher.java | 4 ++-- .../OidcAuthenticationProseTests.java | 4 +--- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/driver-core/src/main/com/mongodb/MongoCredential.java b/driver-core/src/main/com/mongodb/MongoCredential.java index b7035a27f7b..4c10e1f640c 100644 --- a/driver-core/src/main/com/mongodb/MongoCredential.java +++ b/driver-core/src/main/com/mongodb/MongoCredential.java @@ -604,7 +604,7 @@ public interface OidcRequestContext { Duration getTimeout(); /** - * @return The OIDC callback API version. + * @return The OIDC callback API version. Currently, version 1. */ int getVersion(); } 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 12f1ad41254..70f9682476c 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java +++ b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java @@ -66,7 +66,7 @@ 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"; + public static final String AWS_WEB_IDENTITY_TOKEN_FILE = "AWS_WEB_IDENTITY_TOKEN_FILE"; private static final int CALLBACK_API_VERSION_NUMBER = 1; @Nullable 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 4e2fc0401aa..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 @@ -25,6 +25,7 @@ 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; @@ -69,6 +70,7 @@ import org.bson.BsonDouble; import org.bson.BsonInt32; import org.bson.BsonInt64; +import org.bson.BsonNumber; import org.bson.BsonString; import org.bson.BsonValue; @@ -510,16 +512,19 @@ private void initClient(final BsonDocument entity, final String id, case "authMechanism": if (value.equals(new BsonString(AuthenticationMechanism.MONGODB_OIDC.getMechanismName()))) { clientSettingsBuilder.credential(MongoCredential.createOidcCredential(null)); + break; } - break; + throw new UnsupportedOperationException("Unsupported authMechanism: " + value); case "authMechanismProperties": MongoCredential credential = clientSettingsBuilder.build().getCredential(); - if (credential != null && credential.getAuthenticationMechanism() == AuthenticationMechanism.MONGODB_OIDC) { - MongoCredential c = credential; - clientSettingsBuilder.credential(c.withMechanismProperty( + 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("AWS_WEB_IDENTITY_TOKEN_FILE")); + Path path = Paths.get(getenv(OidcAuthenticator.AWS_WEB_IDENTITY_TOKEN_FILE)); String accessToken; try { accessToken = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); @@ -528,8 +533,9 @@ private void initClient(final BsonDocument entity, final String id, } return new MongoCredential.RequestCallbackResult(accessToken); })); + break; } - 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/RunOnRequirementsMatcher.java b/driver-sync/src/test/functional/com/mongodb/client/unified/RunOnRequirementsMatcher.java index e00df2a1d55..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 @@ -75,10 +75,10 @@ public static boolean runOnRequirementsMet(final BsonArray runOnRequirements, fi } break; case "authMechanism": - boolean containsOIDC = getServerParameters() + boolean containsMechanism = getServerParameters() .getArray("authenticationMechanisms") .contains(curRequirement.getValue()); - if (!containsOIDC) { + if (!containsMechanism) { requirementMet = false; break requirementLoop; } diff --git a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java index 70d8b05bcb7..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 @@ -78,8 +78,6 @@ 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; protected static String getOidcUri() { @@ -410,7 +408,7 @@ private RequestCallbackResult 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 {