diff --git a/driver-core/src/main/com/mongodb/MongoCredential.java b/driver-core/src/main/com/mongodb/MongoCredential.java index 418863dc21c..4c10e1f640c 100644 --- a/driver-core/src/main/com/mongodb/MongoCredential.java +++ b/driver-core/src/main/com/mongodb/MongoCredential.java @@ -25,7 +25,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Objects; @@ -188,8 +187,7 @@ public final class MongoCredential { * The provider name. The value must be a string. *

* If this is provided, - * {@link MongoCredential#REQUEST_TOKEN_CALLBACK_KEY} and - * {@link MongoCredential#REFRESH_TOKEN_CALLBACK_KEY} + * {@link MongoCredential#OIDC_CALLBACK_KEY} * must not be provided. * * @see #createOidcCredential(String) @@ -208,45 +206,7 @@ public final class MongoCredential { * @see #createOidcCredential(String) * @since 4.10 */ - public static final String REQUEST_TOKEN_CALLBACK_KEY = "REQUEST_TOKEN_CALLBACK"; - - /** - * Mechanism key for invoked when the OIDC-based authenticator refreshes - * tokens from the identity provider. If this callback is not provided, - * then refresh operations will not be attempted.The type of the value - * must be {@link OidcRefreshCallback}. - *

- * If this is provided, {@link MongoCredential#PROVIDER_NAME_KEY} - * must not be provided. - * - * @see #createOidcCredential(String) - * @since 4.10 - */ - public static final String REFRESH_TOKEN_CALLBACK_KEY = "REFRESH_TOKEN_CALLBACK"; - - /** - * Mechanism key for a list of allowed hostnames or ip-addresses for MongoDB connections. Ports must be excluded. - * The hostnames may include a leading "*." wildcard, which allows for matching (potentially nested) subdomains. - * When MONGODB-OIDC authentication is attempted against a hostname that does not match any of list of allowed hosts - * the driver will raise an error. The type of the value must be {@code List}. - * - * @see MongoCredential#DEFAULT_ALLOWED_HOSTS - * @see #createOidcCredential(String) - * @since 4.10 - */ - public static final String ALLOWED_HOSTS_KEY = "ALLOWED_HOSTS"; - - /** - * The list of allowed hosts that will be used if no - * {@link MongoCredential#ALLOWED_HOSTS_KEY} value is supplied. - * The default allowed hosts are: - * {@code "*.mongodb.net", "*.mongodb-dev.net", "*.mongodbgov.net", "localhost", "127.0.0.1", "::1"} - * - * @see #createOidcCredential(String) - * @since 4.10 - */ - public static final List DEFAULT_ALLOWED_HOSTS = Collections.unmodifiableList(Arrays.asList( - "*.mongodb.net", "*.mongodb-dev.net", "*.mongodbgov.net", "localhost", "127.0.0.1", "::1")); + public static final String OIDC_CALLBACK_KEY = "OIDC_CALLBACK"; /** * Creates a MongoCredential instance with an unspecified mechanism. The client will negotiate the best mechanism based on the @@ -404,9 +364,7 @@ public static MongoCredential createAwsCredential(@Nullable final String userNam * @since 4.10 * @see #withMechanismProperty(String, Object) * @see #PROVIDER_NAME_KEY - * @see #REQUEST_TOKEN_CALLBACK_KEY - * @see #REFRESH_TOKEN_CALLBACK_KEY - * @see #ALLOWED_HOSTS_KEY + * @see #OIDC_CALLBACK_KEY * @mongodb.server.release 7.0 */ public static MongoCredential createOidcCredential(@Nullable final String userName) { @@ -639,26 +597,16 @@ public String toString() { */ @Evolving public interface OidcRequestContext { - /** - * @return The OIDC Identity Provider's configuration that can be used to acquire an Access Token. - */ - IdpInfo getIdpInfo(); /** * @return The timeout that this callback must complete within. */ Duration getTimeout(); - } - /** - * The context for the {@link OidcRefreshCallback#onRefresh(OidcRefreshContext) OIDC refresh callback}. - */ - @Evolving - public interface OidcRefreshContext extends OidcRequestContext { /** - * @return The OIDC Refresh token supplied by a prior callback invocation. + * @return The OIDC callback API version. Currently, version 1. */ - String getRefreshToken(); + int getVersion(); } /** @@ -673,72 +621,22 @@ public interface OidcRequestCallback { * @param context The context. * @return The response produced by an OIDC Identity Provider */ - IdpResponse onRequest(OidcRequestContext context); - } - - /** - * This callback is invoked when the OIDC-based authenticator refreshes - * tokens from the identity provider. If this callback is not provided, - * then refresh operations will not be attempted. - *

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