Skip to content

Commit a5140b1

Browse files
committed
DefaultAuthenticator now negotiates mechanism
1 parent 258807e commit a5140b1

File tree

4 files changed

+190
-22
lines changed

4 files changed

+190
-22
lines changed

driver-core/src/main/com/mongodb/MongoCredential.java

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import static com.mongodb.AuthenticationMechanism.MONGODB_X509;
3030
import static com.mongodb.AuthenticationMechanism.PLAIN;
3131
import static com.mongodb.AuthenticationMechanism.SCRAM_SHA_1;
32+
import static com.mongodb.AuthenticationMechanism.SCRAM_SHA_256;
3233
import static com.mongodb.assertions.Assertions.notNull;
3334

3435
/**
@@ -148,19 +149,20 @@ public final class MongoCredential {
148149

149150
/**
150151
* Creates a MongoCredential instance with an unspecified mechanism. The client will negotiate the best mechanism based on the
151-
* version of the server that the client is authenticating to. If the server version is 3.0 or higher,
152-
* the driver will authenticate using the SCRAM-SHA-1 mechanism. Otherwise, the driver will authenticate using the MONGODB_CR
153-
* mechanism.
152+
* version of the server that the client is authenticating to.
154153
*
154+
* <p>If the server version is 4.0 or higher, the driver will negotiate with the server preferring the SCRAM-SHA-256 mechanism. 3.x
155+
* servers will authenticate using SCRAM-SHA-1, older servers will authenticate using the MONGODB_CR mechanism.</p>
155156
*
156157
* @param userName the user name
157158
* @param database the database where the user is defined
158159
* @param password the user's password
159160
* @return the credential
160161
*
161162
* @since 2.13
162-
* @mongodb.driver.manual core/authentication/#mongodb-cr-authentication MONGODB-CR
163+
* @mongodb.driver.manual core/authentication/#authentication-scram-sha-256 SCRAM-SHA-256
163164
* @mongodb.driver.manual core/authentication/#authentication-scram-sha-1 SCRAM-SHA-1
165+
* @mongodb.driver.manual core/authentication/#mongodb-cr-authentication MONGODB-CR
164166
*/
165167
public static MongoCredential createCredential(final String userName, final String database, final char[] password) {
166168
return new MongoCredential(null, userName, database, password);
@@ -187,6 +189,23 @@ public static MongoCredential createScramSha1Credential(final String userName, f
187189
return new MongoCredential(SCRAM_SHA_1, userName, source, password);
188190
}
189191

192+
/**
193+
* Creates a MongoCredential instance for the SCRAM-SHA-256 SASL mechanism.
194+
*
195+
* @param userName the non-null user name
196+
* @param source the source where the user is defined.
197+
* @param password the non-null user password
198+
* @return the credential
199+
* @see #createCredential(String, String, char[])
200+
*
201+
* @since 3.8
202+
* @mongodb.server.release 4.0
203+
* @mongodb.driver.manual core/authentication/#authentication-scram-sha-256 SCRAM-SHA-256
204+
*/
205+
public static MongoCredential createScramSha256Credential(final String userName, final String source, final char[] password) {
206+
return new MongoCredential(SCRAM_SHA_256, userName, source, password);
207+
}
208+
190209
/**
191210
* Creates a MongoCredential instance for the MongoDB Challenge Response protocol. Use this method only if you want to ensure that
192211
* the driver uses the MONGODB_CR mechanism regardless of whether the server you are connecting to supports a more secure
@@ -296,6 +315,20 @@ public <T> MongoCredential withMechanismProperty(final String key, final T value
296315
return new MongoCredential(this, key, value);
297316
}
298317

318+
/**
319+
* Creates a new MongoCredential with the set mechanism. The existing mechanism must be null.
320+
*
321+
* @param mechanism the mechanism to set
322+
* @return the credential
323+
* @since 3.8
324+
*/
325+
public MongoCredential withMechanism(final AuthenticationMechanism mechanism) {
326+
if (this.mechanism != null) {
327+
throw new IllegalArgumentException("Mechanism already set");
328+
}
329+
return new MongoCredential(mechanism, userName, source, password, mechanismProperties);
330+
}
331+
299332
/**
300333
* Constructs a new instance using the given mechanism, userName, source, and password
301334
*
@@ -306,6 +339,11 @@ public <T> MongoCredential withMechanismProperty(final String key, final T value
306339
*/
307340
MongoCredential(@Nullable final AuthenticationMechanism mechanism, @Nullable final String userName, final String source,
308341
@Nullable final char[] password) {
342+
this(mechanism, userName, source, password, Collections.<String, Object>emptyMap());
343+
}
344+
345+
MongoCredential(@Nullable final AuthenticationMechanism mechanism, @Nullable final String userName, final String source,
346+
@Nullable final char[] password, final Map<String, Object> mechanismProperties) {
309347
if (mechanism != MONGODB_X509 && userName == null) {
310348
throw new IllegalArgumentException("username can not be null");
311349
}
@@ -327,12 +365,13 @@ public <T> MongoCredential withMechanismProperty(final String key, final T value
327365
this.source = notNull("source", source);
328366

329367
this.password = password != null ? password.clone() : null;
330-
this.mechanismProperties = Collections.emptyMap();
368+
this.mechanismProperties = new HashMap<String, Object>(mechanismProperties);
331369
}
332370

333371
@SuppressWarnings("deprecation")
334372
private boolean mechanismRequiresPassword(@Nullable final AuthenticationMechanism mechanism) {
335-
return mechanism == PLAIN || mechanism == MONGODB_CR || mechanism == SCRAM_SHA_1;
373+
return mechanism == PLAIN || mechanism == MONGODB_CR || mechanism == SCRAM_SHA_1 || mechanism == SCRAM_SHA_256;
374+
336375
}
337376

338377
/**
@@ -469,12 +508,12 @@ public int hashCode() {
469508
@Override
470509
public String toString() {
471510
return "MongoCredential{"
472-
+ "mechanism=" + mechanism
473-
+ ", userName='" + userName + '\''
474-
+ ", source='" + source + '\''
475-
+ ", password=<hidden>"
476-
+ ", mechanismProperties=" + mechanismProperties
477-
+ '}';
511+
+ "mechanism=" + mechanism
512+
+ ", userName='" + userName + '\''
513+
+ ", source='" + source + '\''
514+
+ ", password=<hidden>"
515+
+ ", mechanismProperties=" + mechanismProperties
516+
+ '}';
478517
}
479518
}
480519

driver-core/src/main/com/mongodb/connection/DefaultAuthenticator.java

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,102 @@
1616

1717
package com.mongodb.connection;
1818

19-
import com.mongodb.MongoCredential;
19+
import com.mongodb.AuthenticationMechanism;
20+
import com.mongodb.MongoException;
21+
import com.mongodb.MongoSecurityException;
2022
import com.mongodb.async.SingleResultCallback;
23+
import org.bson.BsonArray;
24+
import org.bson.BsonDocument;
25+
import org.bson.BsonInt32;
26+
import org.bson.BsonString;
2127

28+
import static com.mongodb.AuthenticationMechanism.SCRAM_SHA_1;
29+
import static com.mongodb.AuthenticationMechanism.SCRAM_SHA_256;
2230
import static com.mongodb.assertions.Assertions.isTrueArgument;
31+
import static com.mongodb.connection.CommandHelper.executeCommand;
32+
import static com.mongodb.connection.CommandHelper.executeCommandAsync;
33+
import static java.lang.String.format;
2334

2435
class DefaultAuthenticator extends Authenticator {
36+
static final int USER_NOT_FOUND_CODE = 11;
37+
private static final ServerVersion FOUR_ZERO = new ServerVersion(3, 7);
38+
private static final ServerVersion THREE_ZERO = new ServerVersion(3, 0);
39+
private static final BsonString DEFAULT_MECHANISM_NAME = new BsonString(SCRAM_SHA_256.getMechanismName());
40+
2541
DefaultAuthenticator(final MongoCredentialWithCache credential) {
2642
super(credential);
2743
isTrueArgument("unspecified authentication mechanism", credential.getAuthenticationMechanism() == null);
2844
}
2945

3046
@Override
3147
void authenticate(final InternalConnection connection, final ConnectionDescription connectionDescription) {
32-
createAuthenticator(connectionDescription).authenticate(connection, connectionDescription);
48+
if (connectionDescription.getServerVersion().compareTo(FOUR_ZERO) < 0) {
49+
getLegacyDefaultAuthenticator(connectionDescription.getServerVersion())
50+
.authenticate(connection, connectionDescription);
51+
} else {
52+
try {
53+
BsonDocument isMasterResult = executeCommand("admin", createIsMasterCommand(), connection);
54+
getAuthenticatorFromIsMasterResult(isMasterResult, connectionDescription.getServerVersion())
55+
.authenticate(connection, connectionDescription);
56+
} catch (Exception e) {
57+
throw wrapException(e);
58+
}
59+
}
3360
}
3461

3562
@Override
3663
void authenticateAsync(final InternalConnection connection, final ConnectionDescription connectionDescription,
3764
final SingleResultCallback<Void> callback) {
38-
createAuthenticator(connectionDescription).authenticateAsync(connection, connectionDescription, callback);
65+
if (connectionDescription.getServerVersion().compareTo(FOUR_ZERO) < 0) {
66+
getLegacyDefaultAuthenticator(connectionDescription.getServerVersion())
67+
.authenticateAsync(connection, connectionDescription, callback);
68+
} else {
69+
executeCommandAsync("admin", createIsMasterCommand(), connection, new SingleResultCallback<BsonDocument>() {
70+
@Override
71+
public void onResult(final BsonDocument result, final Throwable t) {
72+
if (t != null) {
73+
callback.onResult(null, wrapException(t));
74+
} else {
75+
getAuthenticatorFromIsMasterResult(result, connectionDescription.getServerVersion())
76+
.authenticateAsync(connection, connectionDescription, callback);
77+
}
78+
}
79+
});
80+
}
3981
}
4082

41-
Authenticator createAuthenticator(final ConnectionDescription connectionDescription) {
42-
if (connectionDescription.getServerVersion().compareTo(new ServerVersion(2, 7)) >= 0) {
43-
return new ScramShaAuthenticator(getMongoCredentialWithCache());
83+
Authenticator getAuthenticatorFromIsMasterResult(final BsonDocument isMasterResult, final ServerVersion serverVersion) {
84+
if (isMasterResult.containsKey("saslSupportedMechs")) {
85+
BsonArray saslSupportedMechs = isMasterResult.getArray("saslSupportedMechs");
86+
AuthenticationMechanism mechanism = saslSupportedMechs.contains(DEFAULT_MECHANISM_NAME) ? SCRAM_SHA_256 : SCRAM_SHA_1;
87+
return new ScramShaAuthenticator(getMongoCredentialWithCache().withMechanism(mechanism));
88+
} else {
89+
return getLegacyDefaultAuthenticator(serverVersion);
90+
}
91+
}
92+
93+
private Authenticator getLegacyDefaultAuthenticator(final ServerVersion serverVersion) {
94+
if (serverVersion.compareTo(THREE_ZERO) >= 0) {
95+
return new ScramShaAuthenticator(getMongoCredentialWithCache().withMechanism(SCRAM_SHA_1));
4496
} else {
4597
return new NativeAuthenticator(getMongoCredentialWithCache());
4698
}
4799
}
100+
101+
private BsonDocument createIsMasterCommand() {
102+
BsonDocument isMasterCommandDocument = new BsonDocument("ismaster", new BsonInt32(1));
103+
isMasterCommandDocument.append("saslSupportedMechs",
104+
new BsonString(format("%s.%s", getMongoCredential().getSource(), getMongoCredential().getUserName())));
105+
return isMasterCommandDocument;
106+
}
107+
108+
private MongoException wrapException(final Throwable t) {
109+
if (t instanceof MongoSecurityException) {
110+
return (MongoSecurityException) t;
111+
} else if (t instanceof MongoException && ((MongoException) t).getCode() == USER_NOT_FOUND_CODE) {
112+
return new MongoSecurityException(getMongoCredential(), format("Exception authenticating %s", getMongoCredential()), t);
113+
} else {
114+
return MongoException.fromThrowable(t);
115+
}
116+
}
48117
}

driver-core/src/main/com/mongodb/connection/InternalStreamConnectionInitializer.java

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,31 +17,40 @@
1717
package com.mongodb.connection;
1818

1919
import com.mongodb.MongoCompressor;
20+
import com.mongodb.MongoCredential;
21+
import com.mongodb.MongoException;
22+
import com.mongodb.MongoSecurityException;
2023
import com.mongodb.async.SingleResultCallback;
2124
import org.bson.BsonArray;
2225
import org.bson.BsonDocument;
2326
import org.bson.BsonInt32;
2427
import org.bson.BsonString;
2528

29+
import java.util.ArrayList;
2630
import java.util.List;
2731
import java.util.concurrent.atomic.AtomicInteger;
2832

2933
import static com.mongodb.assertions.Assertions.notNull;
3034
import static com.mongodb.connection.CommandHelper.executeCommand;
3135
import static com.mongodb.connection.CommandHelper.executeCommandAsync;
3236
import static com.mongodb.connection.CommandHelper.executeCommandWithoutCheckingForFailure;
37+
import static com.mongodb.connection.DefaultAuthenticator.USER_NOT_FOUND_CODE;
3338
import static com.mongodb.connection.DescriptionHelper.createConnectionDescription;
39+
import static com.mongodb.connection.DescriptionHelper.getVersion;
40+
import static java.lang.String.format;
3441

3542
class InternalStreamConnectionInitializer implements InternalConnectionInitializer {
3643
private final List<Authenticator> authenticators;
3744
private final BsonDocument clientMetadataDocument;
3845
private final List<MongoCompressor> requestedCompressors;
46+
private final boolean checkSaslSupportedMechs;
3947

4048
InternalStreamConnectionInitializer(final List<Authenticator> authenticators, final BsonDocument clientMetadataDocument,
4149
final List<MongoCompressor> requestedCompressors) {
42-
this.authenticators = notNull("authenticators", authenticators);
50+
this.authenticators = new ArrayList<Authenticator>(notNull("authenticators", authenticators));
4351
this.clientMetadataDocument = clientMetadataDocument;
4452
this.requestedCompressors = notNull("requestedCompressors", requestedCompressors);
53+
this.checkSaslSupportedMechs = this.authenticators.size() > 0 && this.authenticators.get(0) instanceof DefaultAuthenticator;
4554
}
4655

4756
@Override
@@ -50,7 +59,6 @@ public ConnectionDescription initialize(final InternalConnection internalConnect
5059

5160
ConnectionDescription connectionDescription = initializeConnectionDescription(internalConnection);
5261
authenticateAll(internalConnection, connectionDescription);
53-
5462
return completeConnectionDescriptionInitialization(internalConnection, connectionDescription);
5563
}
5664

@@ -88,8 +96,21 @@ public void onResult(final Void result, final Throwable t) {
8896
}
8997

9098
private ConnectionDescription initializeConnectionDescription(final InternalConnection internalConnection) {
91-
BsonDocument isMasterResult = executeCommand("admin", createIsMasterCommand(), internalConnection);
99+
BsonDocument isMasterResult;
100+
BsonDocument isMasterCommandDocument = createIsMasterCommand();
101+
102+
try {
103+
isMasterResult = executeCommand("admin", isMasterCommandDocument, internalConnection);
104+
} catch (MongoException e) {
105+
if (checkSaslSupportedMechs && e.getCode() == USER_NOT_FOUND_CODE) {
106+
MongoCredential credential = authenticators.get(0).getMongoCredential();
107+
throw new MongoSecurityException(credential, format("Exception authenticating %s", credential), e);
108+
}
109+
throw e;
110+
}
92111
BsonDocument buildInfoResult = executeCommand("admin", new BsonDocument("buildinfo", new BsonInt32(1)), internalConnection);
112+
113+
setFirstAuthenticator(isMasterResult, buildInfoResult);
93114
return createConnectionDescription(internalConnection.getDescription().getConnectionId(), isMasterResult, buildInfoResult);
94115
}
95116

@@ -105,6 +126,11 @@ private BsonDocument createIsMasterCommand() {
105126
}
106127
isMasterCommandDocument.append("compression", compressors);
107128
}
129+
if (checkSaslSupportedMechs) {
130+
MongoCredential credential = authenticators.get(0).getMongoCredential();
131+
isMasterCommandDocument.append("saslSupportedMechs",
132+
new BsonString(credential.getSource() + "." + credential.getUserName()));
133+
}
108134
return isMasterCommandDocument;
109135
}
110136

@@ -131,7 +157,14 @@ private void initializeConnectionDescriptionAsync(final InternalConnection inter
131157
@Override
132158
public void onResult(final BsonDocument isMasterResult, final Throwable t) {
133159
if (t != null) {
134-
callback.onResult(null, t);
160+
if (checkSaslSupportedMechs && t instanceof MongoException
161+
&& ((MongoException) t).getCode() == USER_NOT_FOUND_CODE) {
162+
MongoCredential credential = authenticators.get(0).getMongoCredential();
163+
callback.onResult(null, new MongoSecurityException(credential,
164+
format("Exception authenticating %s", credential), t));
165+
} else {
166+
callback.onResult(null, t);
167+
}
135168
} else {
136169
executeCommandAsync("admin", new BsonDocument("buildinfo", new BsonInt32(1)), internalConnection,
137170
new SingleResultCallback<BsonDocument>() {
@@ -143,6 +176,7 @@ public void onResult(final BsonDocument buildInfoResult,
143176
} else {
144177
ConnectionId connectionId = internalConnection.getDescription()
145178
.getConnectionId();
179+
setFirstAuthenticator(isMasterResult, buildInfoResult);
146180
callback.onResult(createConnectionDescription(connectionId,
147181
isMasterResult,
148182
buildInfoResult),
@@ -155,6 +189,14 @@ public void onResult(final BsonDocument buildInfoResult,
155189
});
156190
}
157191

192+
@SuppressWarnings("unchecked")
193+
private void setFirstAuthenticator(final BsonDocument isMasterResult, final BsonDocument buildInfoResult) {
194+
if (checkSaslSupportedMechs) {
195+
authenticators.set(0, ((DefaultAuthenticator) authenticators.get(0))
196+
.getAuthenticatorFromIsMasterResult(isMasterResult, getVersion(buildInfoResult)));
197+
}
198+
}
199+
158200
private void completeConnectionDescriptionInitializationAsync(final InternalConnection internalConnection,
159201
final ConnectionDescription connectionDescription,
160202
final SingleResultCallback<ConnectionDescription> callback) {

driver-core/src/test/functional/com/mongodb/operation/UserOperationsSpecification.groovy

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,24 @@ class UserOperationsSpecification extends OperationFunctionalSpecification {
139139
executeAsync(new DropUserOperation(databaseName, credential.userName))
140140
}
141141

142+
def 'should handle user not found'() {
143+
given:
144+
def credential = createCredential('user', databaseName, 'pencil' as char[])
145+
def cluster = getCluster(credential, ClusterSettings.builder().serverSelectionTimeout(1, TimeUnit.SECONDS))
146+
147+
when:
148+
cluster.selectServer(new WritableServerSelector())
149+
150+
then:
151+
thrown(MongoTimeoutException)
152+
153+
cleanup:
154+
cluster?.close()
155+
156+
where:
157+
async << [true, false]
158+
}
159+
142160
@Category(Slow)
143161
def 'a removed user should not authenticate'() {
144162
given:

0 commit comments

Comments
 (0)