I have managed to make updated/rotated credentials work with a clean hack using Java Reflection. We needed to comply with a credentials rotation policy without having to manually restart our services every time the credential changes.
We use AWS DocumentDB 5.0.0 (MongoDB compatibility) with credentials stored in AWS Secrets Manager. Every time these change we contact the Secrets Manager service, retrieve and update them on the spot. Our MongoDB dependencies in our SpringBoot API app are the following;
org.springframework.data:spring-data-mongodb:3.3.3
org.mongodb:mongodb-driver-core:4.4.2
org.mongodb:mongodb-driver-sync:4.4.2
In the configuration, the MongoCredential
is injected to the MongoClientSettings
as a singleton spring bean (see definition in code below). If you debug down to the driver's Authenticator implementation you'll see the same instance is maintained there as well.
...
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
public MongoCredential mongoCredential(
final DocumentDBConnectionDetails documentDBConnectionDetails,
final ApplicationProperties applicationProperties
) {
return MongoCredential.createCredential(
documentDBConnectionDetails.getUsername(),
applicationProperties.getDatabase().getName(),
documentDBConnectionDetails.getPassword().toCharArray()
);
}
...
Then you can have another spring bean which will be responsible for updating the username and password pair. Below I've put the implementation currently working for us. All you then need to do is to inject the MongoCredentialUpdater
bean to the part where your new updated credential is made available, and call its update(String, char[])
method.
import com.mongodb.MongoCredential;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
@Slf4j
@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
public class MongoCredentialUpdater {
private final MongoCredential mongoCredential;
private final Field usernameField;
private final Field passwordField;
public MongoCredentialUpdater(
final MongoCredential mongoCredential
) throws NoSuchFieldException, IllegalAccessException {
this.mongoCredential = mongoCredential;
this.usernameField = getAccessibleField("userName");
this.passwordField = getAccessibleField("password");
}
public boolean update(final String username, final char[] password) {
try {
usernameField.set(mongoCredential, username);
passwordField.set(mongoCredential, password);
return true;
} catch (final IllegalAccessException e) {
log.error("Failed to update the MongoCredential", e);
return false;
}
}
private Field getAccessibleField(final String fieldname) throws NoSuchFieldException, IllegalAccessException {
final Field field = MongoCredential.class.getDeclaredField(fieldname);
final Field modifiers = Field.class.getDeclaredField("modifiers");
field.setAccessible(true);
modifiers.setAccessible(true);
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
return field;
}
}
Limitations
In MongoDB DRIVERS-1463 ticket you will read that:
... However, we must allow for customers to choose between two following scenarios: a) drain the existing connections ASAP and create a bunch of new ones using a new credential; b) keep the existing connections as long as needed, potentially until the next restart of the MongoDB Server instance or until the application code decides to re-authenticate using them.
The approach above covers option b) only. In our case this is fine cause the security department has appropriate mechanisms in place to trace requests outside the APIs ecosystem and drop malicious connections with leaked credentials on the spot. However, if your requirement is still to re-initialize the entire ConnectionPool
the driver uses, you'll need to somehow get access to its instance, locate all active connections and invalidate them. Then any new requests would require new connections which will be established from scratch as the pool will have none active in it. These will be created using the updated credential. You would also need to research whether a connection's invalidation could cause implications on operations/requests in progress, as you don't want requests start failing every time the credential changes.
Hope the above helps until the feature is implemented in the MongoDB's drivers.