Skip to content

Commit 1354680

Browse files
committed
JAVA-2375: Make userName optional for MONGODB-X509 authentication mechanism
As of MongoDB 3.4, the user name is optional, as the server will extract it from the distinguished subject name of the client certificate
1 parent cbfae84 commit 1354680

File tree

4 files changed

+173
-13
lines changed

4 files changed

+173
-13
lines changed

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,22 @@ public static MongoCredential createMongoX509Credential(final String userName) {
194194
return new MongoCredential(MONGODB_X509, userName, "$external", null);
195195
}
196196

197+
/**
198+
* Creates a MongoCredential instance for the MongoDB X.509 protocol where the distinguished subject name of the client certificate
199+
* acts as the userName.
200+
* <p>
201+
* Available on MongoDB server versions &gt;= 3.4.
202+
* </p>
203+
* @return the credential
204+
*
205+
* @since 3.4
206+
* @mongodb.server.release 3.4
207+
* @mongodb.driver.manual core/authentication/#x-509-certificate-authentication X-509
208+
*/
209+
public static MongoCredential createMongoX509Credential() {
210+
return new MongoCredential(MONGODB_X509, null, "$external", null);
211+
}
212+
197213
/**
198214
* Creates a MongoCredential instance for the PLAIN SASL mechanism.
199215
*
@@ -262,7 +278,7 @@ public <T> MongoCredential withMechanismProperty(final String key, final T value
262278
* @param password the password
263279
*/
264280
MongoCredential(final AuthenticationMechanism mechanism, final String userName, final String source, final char[] password) {
265-
if (userName == null) {
281+
if (mechanism != MONGODB_X509 && userName == null) {
266282
throw new IllegalArgumentException("username can not be null");
267283
}
268284

@@ -279,7 +295,7 @@ public <T> MongoCredential withMechanismProperty(final String key, final T value
279295
}
280296

281297
this.mechanism = mechanism;
282-
this.userName = notNull("userName", userName);
298+
this.userName = userName;
283299
this.source = notNull("source", source);
284300

285301
this.password = password != null ? password.clone() : null;

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

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class X509Authenticator extends Authenticator {
3636
@Override
3737
void authenticate(final InternalConnection connection, final ConnectionDescription connectionDescription) {
3838
try {
39+
validateUserName(connectionDescription);
3940
BsonDocument authCommand = getAuthCommand(getCredential().getUserName());
4041
executeCommand(getCredential().getSource(), authCommand, connection);
4142
} catch (MongoCommandException e) {
@@ -46,24 +47,31 @@ void authenticate(final InternalConnection connection, final ConnectionDescripti
4647
@Override
4748
void authenticateAsync(final InternalConnection connection, final ConnectionDescription connectionDescription,
4849
final SingleResultCallback<Void> callback) {
49-
executeCommandAsync(getCredential().getSource(), getAuthCommand(getCredential().getUserName()), connection,
50-
new SingleResultCallback<BsonDocument>() {
51-
@Override
52-
public void onResult(final BsonDocument nonceResult, final Throwable t) {
53-
if (t != null) {
54-
callback.onResult(null, translateThrowable(t));
55-
} else {
56-
callback.onResult(null, null);
50+
try {
51+
validateUserName(connectionDescription);
52+
executeCommandAsync(getCredential().getSource(), getAuthCommand(getCredential().getUserName()), connection,
53+
new SingleResultCallback<BsonDocument>() {
54+
@Override
55+
public void onResult(final BsonDocument nonceResult, final Throwable t) {
56+
if (t != null) {
57+
callback.onResult(null, translateThrowable(t));
58+
} else {
59+
callback.onResult(null, null);
60+
}
5761
}
58-
}
59-
});
62+
});
63+
} catch (Throwable t) {
64+
callback.onResult(null, t);
65+
}
6066
}
6167

6268
private BsonDocument getAuthCommand(final String userName) {
6369
BsonDocument cmd = new BsonDocument();
6470

6571
cmd.put("authenticate", new BsonInt32(1));
66-
cmd.put("user", new BsonString(userName));
72+
if (userName != null) {
73+
cmd.put("user", new BsonString(userName));
74+
}
6775
cmd.put("mechanism", new BsonString(AuthenticationMechanism.MONGODB_X509.getMechanismName()));
6876

6977
return cmd;
@@ -76,4 +84,11 @@ private Throwable translateThrowable(final Throwable t) {
7684
return t;
7785
}
7886
}
87+
88+
private void validateUserName(final ConnectionDescription connectionDescription) {
89+
if (getCredential().getUserName() == null && connectionDescription.getServerVersion().compareTo(new ServerVersion(3, 4)) < 0) {
90+
throw new MongoSecurityException(getCredential(), "User name is required for the MONGODB-X509 authentication mechanism "
91+
+ "on server versions less than 3.4");
92+
}
93+
}
7994
}

driver-core/src/test/unit/com/mongodb/MongoCredentialSpecification.groovy

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,21 @@ class MongoCredentialSpecification extends Specification {
179179
MongoCredential.MONGODB_X509_MECHANISM == credential.getMechanism()
180180
}
181181

182+
def 'creating an X.509 Credential without a username should populate the correct fields'() {
183+
given:
184+
AuthenticationMechanism mechanism = AuthenticationMechanism.MONGODB_X509
185+
186+
when:
187+
MongoCredential credential = MongoCredential.createMongoX509Credential()
188+
189+
then:
190+
null == credential.getUserName()
191+
'$external' == credential.getSource()
192+
null == credential.getPassword()
193+
mechanism == credential.getAuthenticationMechanism()
194+
MongoCredential.MONGODB_X509_MECHANISM == credential.getMechanism()
195+
}
196+
182197
def 'should get default value of mechanism property when there is no mapping'() {
183198
when:
184199
def credential = MongoCredential.createGSSAPICredential('user')
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright 2016 MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package com.mongodb.connection;
19+
20+
import com.mongodb.MongoCredential;
21+
import com.mongodb.MongoSecurityException;
22+
import com.mongodb.ServerAddress;
23+
import com.mongodb.async.FutureResultCallback;
24+
import org.bson.io.BsonInput;
25+
import org.junit.Before;
26+
import org.junit.Test;
27+
28+
import java.util.List;
29+
import java.util.concurrent.ExecutionException;
30+
31+
import static com.mongodb.connection.MessageHelper.buildSuccessfulReply;
32+
import static org.junit.Assert.assertEquals;
33+
import static org.junit.Assert.fail;
34+
35+
public class X509AuthenticatorNoUserNameTest {
36+
private TestInternalConnection connection;
37+
private ConnectionDescription connectionDescriptionThreeTwo;
38+
private ConnectionDescription connectionDescriptionThreeFour;
39+
private MongoCredential credential;
40+
private X509Authenticator subject;
41+
42+
@Before
43+
public void before() {
44+
connection = new TestInternalConnection(new ServerId(new ClusterId(), new ServerAddress("localhost", 27017)));
45+
connectionDescriptionThreeTwo = new ConnectionDescription(new ConnectionId(new ServerId(new ClusterId(), new ServerAddress())),
46+
new ServerVersion(3, 2), ServerType.STANDALONE, 1000, 16000,
47+
48000);
48+
connectionDescriptionThreeFour = new ConnectionDescription(new ConnectionId(new ServerId(new ClusterId(), new ServerAddress())),
49+
new ServerVersion(3, 4), ServerType.STANDALONE, 1000, 16000,
50+
48000);
51+
credential = MongoCredential.createMongoX509Credential(
52+
"CN=client,OU=kerneluser,O=10Gen,L=New York City,ST=New York,C=US");
53+
subject = new X509Authenticator(this.credential);
54+
}
55+
56+
@Test
57+
public void testSuccessfulAuthentication() {
58+
enqueueSuccessfulAuthenticationReply();
59+
60+
new X509Authenticator(MongoCredential.createMongoX509Credential()).authenticate(connection, connectionDescriptionThreeFour);
61+
62+
validateMessages();
63+
}
64+
65+
@Test
66+
public void testUnsuccessfulAuthenticationWhenServerVersionLessThanThreeFour() {
67+
try {
68+
new X509Authenticator(MongoCredential.createMongoX509Credential()).authenticate(connection,
69+
connectionDescriptionThreeTwo);
70+
fail();
71+
} catch (MongoSecurityException e) {
72+
assertEquals("User name is required for the MONGODB-X509 authentication mechanism on server versions less than 3.4",
73+
e.getMessage());
74+
}
75+
}
76+
77+
@Test
78+
public void testSuccessfulAuthenticationAsync() throws ExecutionException, InterruptedException {
79+
enqueueSuccessfulAuthenticationReply();
80+
81+
FutureResultCallback<Void> futureCallback = new FutureResultCallback<Void>();
82+
new X509Authenticator(MongoCredential.createMongoX509Credential())
83+
.authenticateAsync(connection, connectionDescriptionThreeFour, futureCallback);
84+
85+
futureCallback.get();
86+
87+
validateMessages();
88+
}
89+
90+
@Test
91+
public void testUnsuccessfulAuthenticationWhenServerVersionLessThanThreeFourAsync() throws ExecutionException, InterruptedException {
92+
FutureResultCallback<Void> futureCallback = new FutureResultCallback<Void>();
93+
new X509Authenticator(MongoCredential.createMongoX509Credential()).authenticateAsync(connection,
94+
connectionDescriptionThreeTwo, futureCallback);
95+
96+
try {
97+
futureCallback.get();
98+
} catch (MongoSecurityException e) {
99+
assertEquals("User name is required for the MONGODB-X509 authentication mechanism on server versions less than 3.4",
100+
e.getMessage());
101+
}
102+
}
103+
104+
private void enqueueSuccessfulAuthenticationReply() {
105+
connection.enqueueReply(buildSuccessfulReply("{ok: 1}"));
106+
}
107+
108+
private void validateMessages() {
109+
List<BsonInput> sent = connection.getSent();
110+
String command = MessageHelper.decodeCommandAsJson(sent.get(0));
111+
assertEquals("{ \"authenticate\" : 1, \"mechanism\" : \"MONGODB-X509\" }", command);
112+
}
113+
114+
}

0 commit comments

Comments
 (0)