Skip to content

Commit 8c2acdd

Browse files
committed
CSHARP-1822: Username is now optional when using X509 authentication.
1 parent 48a86c5 commit 8c2acdd

File tree

12 files changed

+188
-22
lines changed

12 files changed

+188
-22
lines changed

src/MongoDB.Driver.Core/Core/Authentication/MongoDBX509Authenticator.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public static string MechanismName
5151
/// <param name="username">The username.</param>
5252
public MongoDBX509Authenticator(string username)
5353
{
54-
_username = Ensure.IsNotNullOrEmpty(username, nameof(username));
54+
_username = Ensure.IsNullOrNotEmpty(username, nameof(username));
5555
}
5656

5757
// properties
@@ -67,6 +67,7 @@ public void Authenticate(IConnection connection, ConnectionDescription descripti
6767
{
6868
Ensure.IsNotNull(connection, nameof(connection));
6969
Ensure.IsNotNull(description, nameof(description));
70+
EnsureUsernameIsNotNullOrNullIsSupported(connection);
7071

7172
try
7273
{
@@ -84,6 +85,7 @@ public async Task AuthenticateAsync(IConnection connection, ConnectionDescriptio
8485
{
8586
Ensure.IsNotNull(connection, nameof(connection));
8687
Ensure.IsNotNull(description, nameof(description));
88+
EnsureUsernameIsNotNullOrNullIsSupported(connection);
8789

8890
try
8991
{
@@ -103,7 +105,7 @@ private CommandWireProtocol<BsonDocument> CreateAuthenticateProtocol()
103105
{
104106
{ "authenticate", 1 },
105107
{ "mechanism", Name },
106-
{ "user", _username }
108+
{ "user", _username, _username != null }
107109
};
108110

109111
var protocol = new CommandWireProtocol<BsonDocument>(
@@ -121,5 +123,15 @@ private MongoAuthenticationException CreateException(IConnection connection, Exc
121123
var message = string.Format("Unable to authenticate username '{0}' using protocol '{1}'.", _username, Name);
122124
return new MongoAuthenticationException(connection.ConnectionId, message, ex);
123125
}
126+
127+
private void EnsureUsernameIsNotNullOrNullIsSupported(IConnection connection)
128+
{
129+
var serverVersion = connection.Description.ServerVersion;
130+
if (_username == null && !Feature.ServerExtractsUsernameFromX509Certificate.IsSupported(serverVersion))
131+
{
132+
var message = $"Username cannot be null for server version {serverVersion}.";
133+
throw new MongoConnectionException(connection.ConnectionId, message);
134+
}
135+
}
124136
}
125137
}

src/MongoDB.Driver.Core/Core/Misc/Feature.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public class Feature
5050
private static readonly Feature __partialIndexes = new Feature("PartialIndexes", new SemanticVersion(3, 2, 0));
5151
private static readonly ReadConcernFeature __readConcern = new ReadConcernFeature("ReadConcern", new SemanticVersion(3, 2, 0));
5252
private static readonly Feature __scramSha1Authentication = new Feature("ScramSha1Authentication", new SemanticVersion(3, 0, 0));
53+
private static readonly Feature __serverExtractsUsernameFromX509Certificate = new Feature("ServerExtractsUsernameFromX509Certificate", new SemanticVersion(3, 4, 0));
5354
private static readonly Feature __userManagementCommands = new Feature("UserManagementCommands", new SemanticVersion(2, 6, 0));
5455
private static readonly Feature __views = new Feature("Views", new SemanticVersion(3, 3, 11));
5556
private static readonly Feature __writeCommands = new Feature("WriteCommands", new SemanticVersion(2, 6, 0));
@@ -189,6 +190,11 @@ public class Feature
189190
/// </summary>
190191
public static Feature ScramSha1Authentication => __scramSha1Authentication;
191192

193+
/// <summary>
194+
/// Gets the server extracts username from X509 certificate feature.
195+
/// </summary>
196+
public static Feature ServerExtractsUsernameFromX509Certificate => __serverExtractsUsernameFromX509Certificate;
197+
192198
/// <summary>
193199
/// Gets the user management commands feature.
194200
/// </summary>

src/MongoDB.Driver.Legacy/MongoServerSettings.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -606,11 +606,7 @@ public static MongoServerSettings FromClientSettings(MongoClientSettings clientS
606606
/// <returns>A MongoServerSettings.</returns>
607607
public static MongoServerSettings FromUrl(MongoUrl url)
608608
{
609-
var credential = MongoCredential.FromComponents(
610-
url.AuthenticationMechanism,
611-
url.AuthenticationSource ?? url.DatabaseName,
612-
url.Username,
613-
url.Password);
609+
var credential = url.GetCredential();
614610

615611
var serverSettings = new MongoServerSettings();
616612
serverSettings.ApplicationName = url.ApplicationName;

src/MongoDB.Driver/MongoClientSettings.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -542,11 +542,7 @@ public UTF8Encoding WriteEncoding
542542
/// <returns>A MongoClientSettings.</returns>
543543
public static MongoClientSettings FromUrl(MongoUrl url)
544544
{
545-
var credential = MongoCredential.FromComponents(
546-
url.AuthenticationMechanism,
547-
url.AuthenticationSource ?? url.DatabaseName,
548-
url.Username,
549-
url.Password);
545+
var credential = url.GetCredential();
550546

551547
var clientSettings = new MongoClientSettings();
552548
clientSettings.ApplicationName = url.ApplicationName;

src/MongoDB.Driver/MongoCredential.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -447,11 +447,6 @@ private void ValidatePassword(string password)
447447
// private static methods
448448
private static MongoCredential FromComponents(string mechanism, string source, string username, MongoIdentityEvidence evidence)
449449
{
450-
if (string.IsNullOrEmpty(username))
451-
{
452-
return null;
453-
}
454-
455450
var defaultedMechanism = (mechanism ?? "DEFAULT").Trim().ToUpperInvariant();
456451
switch (defaultedMechanism)
457452
{
@@ -480,7 +475,7 @@ private static MongoCredential FromComponents(string mechanism, string source, s
480475

481476
return new MongoCredential(
482477
mechanism,
483-
new MongoExternalIdentity(username),
478+
new MongoX509Identity(username),
484479
evidence);
485480
case "GSSAPI":
486481
// always $external for GSSAPI.

src/MongoDB.Driver/MongoDB.Driver.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@
278278
<Compile Include="Linq\Translators\QueryableTranslator.cs" />
279279
<Compile Include="Linq\Translators\PredicateTranslator.cs" />
280280
<Compile Include="ExpressionTranslationOptions.cs" />
281+
<Compile Include="MongoX509Identity.cs" />
281282
<Compile Include="OfTypeSerializer.cs" />
282283
<Compile Include="PipelineDefinitionBuilder.cs" />
283284
<Compile Include="PipelineStageDefinition.cs" />

src/MongoDB.Driver/MongoIdentity.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,14 @@ public abstract class MongoIdentity : IEquatable<MongoIdentity>
3838
/// </summary>
3939
/// <param name="source">The source.</param>
4040
/// <param name="username">The username.</param>
41-
internal MongoIdentity(string source, string username)
41+
/// <param name="allowNullUsername">Whether to allow null usernames.</param>
42+
internal MongoIdentity(string source, string username, bool allowNullUsername = false)
4243
{
4344
if (source == null)
4445
{
4546
throw new ArgumentNullException("source");
4647
}
47-
if (username == null)
48+
if (username == null && !allowNullUsername)
4849
{
4950
throw new ArgumentNullException("username");
5051
}

src/MongoDB.Driver/MongoUrl.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,21 @@ public GuidRepresentation GuidRepresentation
203203
get { return _guidRepresentation; }
204204
}
205205

206+
/// <summary>
207+
/// Gets a value indicating whether this instance has authentication settings.
208+
/// </summary>
209+
public bool HasAuthenticationSettings
210+
{
211+
get
212+
{
213+
return
214+
_username != null ||
215+
_password != null ||
216+
_authenticationMechanism != null ||
217+
_authenticationSource != null;
218+
}
219+
}
220+
206221
/// <summary>
207222
/// Gets the heartbeat interval.
208223
/// </summary>
@@ -498,6 +513,26 @@ public override bool Equals(object obj)
498513
return Equals(obj as MongoUrl); // works even if obj is null or of a different type
499514
}
500515

516+
/// <summary>
517+
/// Gets the credential.
518+
/// </summary>
519+
/// <returns>The credential (or null if the URL has not authentication settings).</returns>
520+
public MongoCredential GetCredential()
521+
{
522+
if (HasAuthenticationSettings)
523+
{
524+
return MongoCredential.FromComponents(
525+
_authenticationMechanism,
526+
_authenticationSource ?? _databaseName,
527+
_username,
528+
_password);
529+
}
530+
else
531+
{
532+
return null;
533+
}
534+
}
535+
501536
/// <summary>
502537
/// Gets the hash code.
503538
/// </summary>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/* Copyright 2010-2014 MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
namespace MongoDB.Driver
17+
{
18+
/// <summary>
19+
/// Represents an identity defined by an X509 certificate.
20+
/// </summary>
21+
public class MongoX509Identity : MongoIdentity
22+
{
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="MongoExternalIdentity" /> class.
25+
/// </summary>
26+
/// <param name="username">The username.</param>
27+
public MongoX509Identity(string username)
28+
: base("$external", username, allowNullUsername: true)
29+
{ }
30+
}
31+
}

tests/MongoDB.Driver.Core.Tests/Core/Authentication/MongoDBX509AuthenticatorTests.cs

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
using MongoDB.Driver.Core.Connections;
2727
using System.Threading.Tasks;
2828
using MongoDB.Bson.TestHelpers.XunitExtensions;
29+
using MongoDB.Driver.Core.Misc;
2930

3031
namespace MongoDB.Driver.Core.Authentication
3132
{
@@ -39,9 +40,8 @@ public class MongoDBX509AuthenticatorTests
3940
new BuildInfoResult(new BsonDocument("version", "2.6.0")));
4041

4142
[Theory]
42-
[InlineData(null)]
4343
[InlineData("")]
44-
public void Constructor_should_throw_an_ArgumentException_when_username_is_null_or_empty(string username)
44+
public void Constructor_should_throw_an_ArgumentException_when_username_is_empty(string username)
4545
{
4646
Action act = () => new MongoDBX509Authenticator(username);
4747

@@ -58,6 +58,7 @@ public void Authenticate_should_throw_an_AuthenticationException_when_authentica
5858

5959
var reply = MessageHelper.BuildNoDocumentsReturnedReply<RawBsonDocument>();
6060
var connection = new MockConnection(__serverId);
61+
connection.Description = CreateConnectionDescription(new SemanticVersion(3, 2, 0));
6162
connection.EnqueueReplyMessage(reply);
6263

6364
Action act;
@@ -85,6 +86,7 @@ public void Authenticate_should_not_throw_when_authentication_succeeds(
8586
RawBsonDocumentHelper.FromJson("{ok: 1}"));
8687

8788
var connection = new MockConnection(__serverId);
89+
connection.Description = CreateConnectionDescription(new SemanticVersion(3, 2, 0));
8890
connection.EnqueueReplyMessage(reply);
8991

9092
Action act;
@@ -99,5 +101,75 @@ public void Authenticate_should_not_throw_when_authentication_succeeds(
99101

100102
act.ShouldNotThrow();
101103
}
104+
105+
[Theory]
106+
[ParameterAttributeData]
107+
public void Authenticate_should_throw_when_username_is_null_and_server_does_not_support_null_username(
108+
[Values(false, true)]
109+
bool async)
110+
{
111+
var subject = new MongoDBX509Authenticator(null);
112+
113+
var reply = MessageHelper.BuildReply<RawBsonDocument>(
114+
RawBsonDocumentHelper.FromJson("{ok: 1}"));
115+
116+
var connection = new MockConnection(__serverId);
117+
connection.Description = CreateConnectionDescription(new SemanticVersion(3, 2, 0));
118+
connection.EnqueueReplyMessage(reply);
119+
120+
Exception exception;
121+
if (async)
122+
{
123+
exception = Record.Exception(() => subject.AuthenticateAsync(connection, __description, CancellationToken.None).GetAwaiter().GetResult());
124+
}
125+
else
126+
{
127+
exception = Record.Exception(() => subject.Authenticate(connection, __description, CancellationToken.None));
128+
}
129+
130+
exception.Should().BeOfType<MongoConnectionException>();
131+
}
132+
133+
[Theory]
134+
[ParameterAttributeData]
135+
public void Authenticate_should_not_throw_when_username_is_null_and_server_support_null_username(
136+
[Values(false, true)]
137+
bool async)
138+
{
139+
var subject = new MongoDBX509Authenticator(null);
140+
141+
var reply = MessageHelper.BuildReply<RawBsonDocument>(
142+
RawBsonDocumentHelper.FromJson("{ok: 1}"));
143+
144+
var connection = new MockConnection(__serverId);
145+
connection.Description = CreateConnectionDescription(new SemanticVersion(3, 4, 0));
146+
connection.EnqueueReplyMessage(reply);
147+
148+
Exception exception;
149+
if (async)
150+
{
151+
exception = Record.Exception(() => subject.AuthenticateAsync(connection, __description, CancellationToken.None).GetAwaiter().GetResult());
152+
}
153+
else
154+
{
155+
exception = Record.Exception(() => subject.Authenticate(connection, __description, CancellationToken.None));
156+
}
157+
158+
exception.Should().BeNull();
159+
}
160+
161+
// private methods
162+
private ConnectionDescription CreateConnectionDescription(SemanticVersion serverVersion)
163+
{
164+
var clusterId = new ClusterId(1);
165+
var serverId = new ServerId(clusterId, new DnsEndPoint("localhost", 27017));
166+
var connectionId = new ConnectionId(serverId, 1);
167+
var isMasterResult = new IsMasterResult(new BsonDocument());
168+
var buildInfoResult = new BuildInfoResult(new BsonDocument
169+
{
170+
{ "version", serverVersion.ToString() }
171+
});
172+
return new ConnectionDescription(connectionId, isMasterResult, buildInfoResult);
173+
}
102174
}
103175
}

0 commit comments

Comments
 (0)